From 4756bd930154319226cf07215af9380ad8227fb3 Mon Sep 17 00:00:00 2001 From: Sonu Rauniyar Date: Wed, 21 Jan 2026 23:36:12 +0545 Subject: [PATCH 01/12] Add reusable copy-to-clipboard functionality for code snippets in AWS setup guide + Moved child components outside of the main component which prevents re-creation on Every Render (#27) --- src/app/help/aws-setup/page.tsx | 237 +++++++++++++++++++++++--------- 1 file changed, 169 insertions(+), 68 deletions(-) diff --git a/src/app/help/aws-setup/page.tsx b/src/app/help/aws-setup/page.tsx index a5b3d01..af84091 100644 --- a/src/app/help/aws-setup/page.tsx +++ b/src/app/help/aws-setup/page.tsx @@ -3,6 +3,117 @@ import { useState } from "react"; import Link from "next/link"; +// Component definitions outside main component +const CodeBlock = ({ + code, + language, + index, + copiedIndex, + onCopy, +}: { + code: string; + language: string; + index: number; + copiedIndex: number | null; + onCopy: (text: string, index: number) => void; +}) => ( +
+
+ + {language} + + +
+
+      {code}
+    
+
+); + +const InlineCode = ({ + text, + index, + copiedIndex, + onCopy, +}: { + text: string; + index: number; + copiedIndex: number | null; + onCopy: (text: string, index: number) => void; +}) => ( + + {text} + + +); + +const StepHeader = ({ + number, + title, + completedSteps, + onToggle, +}: { + number: number; + title: string; + completedSteps: Set; + onToggle: (stepNumber: number) => void; +}) => ( +
+
+ +

{title}

+
+
+); + export default function AWSSetupGuide() { const [copiedIndex, setCopiedIndex] = useState(null); const [completedSteps, setCompletedSteps] = useState>(new Set()); @@ -25,57 +136,6 @@ export default function AWSSetupGuide() { }); }; - const CodeBlock = ({ - code, - language, - index, - }: { - code: string; - language: string; - index: number; - }) => ( -
-
- - {language} - - -
-
-        {code}
-      
-
- ); - - const StepHeader = ({ - number, - title, - }: { - number: number; - title: string; - }) => ( -
-
- -

{title}

-
-
- ); - const iamPolicy = `{ "Version": "2012-10-17", "Statement": [ @@ -591,7 +651,12 @@ export default function AWSSetupGuide() {
{/* Step 1: Create S3 Bucket */}
- +

@@ -660,7 +725,13 @@ export default function AWSSetupGuide() {

  • Paste this configuration:
  • - +

    Important: Replace{" "} @@ -674,7 +745,12 @@ export default function AWSSetupGuide() { {/* Step 2: Create IAM User */}

    - +

    @@ -701,9 +777,12 @@ export default function AWSSetupGuide() {

  • Click "Create user"
  • User name:{" "} - - private-video-user - +
  • Click "Next"
  • Select "Attach policies directly"
  • @@ -747,13 +826,22 @@ export default function AWSSetupGuide() {
  • Click "Next"
  • Policy name:{" "} - - PrivateVideoPolicy - +
  • Click "Create policy"
  • - +
    @@ -818,7 +906,12 @@ export default function AWSSetupGuide() { {/* Step 3: Create MediaConvert Role */}
    - +

    @@ -856,9 +949,12 @@ export default function AWSSetupGuide() {

  • Click "Next" (skip permissions for now)
  • Role name:{" "} - - MediaConvertRole - +
  • Click "Create role"
  • @@ -891,9 +987,12 @@ export default function AWSSetupGuide() {
  • Click "Next"
  • Policy name:{" "} - - S3BucketAccess - +
  • Click "Create policy"
  • @@ -901,6 +1000,8 @@ export default function AWSSetupGuide() { code={mediaConvertPolicy} language="JSON" index={3} + copiedIndex={copiedIndex} + onCopy={copyToClipboard} />
    From bffeddc65dc436c43a3f2433c57c5fec90ae33cf Mon Sep 17 00:00:00 2001 From: Sonu Rauniyar Date: Wed, 21 Jan 2026 23:36:55 +0545 Subject: [PATCH 02/12] [dev] - Add JWT secret example to local environment configuration (#28) * add JWT secret example to local environment configuration * Added new line to the .env.local.example file --- .env.local.example | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.env.local.example b/.env.local.example index 6b1602d..f771ab5 100644 --- a/.env.local.example +++ b/.env.local.example @@ -20,3 +20,8 @@ NEXT_PUBLIC_APP_URL="http://localhost:3000" # Pre-signed URL expiry (seconds) - 3 hours PRESIGNED_URL_EXPIRY="10800" + +# JWT Secret for mobile authentication and streaming tokens +# CRITICAL SECURITY: Generate a strong secret with: openssl rand -base64 64 +# REQUIRED: Application will fail to start if not set +JWT_SECRET="your-jwt-secret-here" From 74511c65a96f0326c76e880517b830ab8e8bc438 Mon Sep 17 00:00:00 2001 From: Brandon Estrella Date: Wed, 21 Jan 2026 10:01:02 -0800 Subject: [PATCH 03/12] v1.8.6 --- CHANGELOG.md | 9 ++++++--- package.json | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73494f8..7766c64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.8.6] - 2026-01-21 +### Added +- JWT secret example to local environment configuration +- New line to the .env.local.example file +- reusable copy-to-clipboard functionality for code snippets in AWS setup guide +- Moved child components outside main component which prevents re-creation on every render ## [1.8.5] - 2026-01-15 ### Fixed - **AWS Credentials Import to Team Accounts**: Fixed blank fields preventing save after importing credentials @@ -16,7 +22,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Users can now successfully import personal credentials to team accounts or vice versa - Import flow: Click "Import" → Select source (Personal or Team) → Credentials populate form → Click "Save" to apply - Fixes issue where imported credentials showed blank/masked fields, preventing form submission - ## [1.8.4] - 2026-01-15 ### Fixed - **Web Video Recorder Upload Failures**: Fixed silent upload failures for recorded videos @@ -30,7 +35,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upload progress tracking maintained throughout the process - Network errors, S3 failures, and transcoding errors now properly caught and displayed - Fixes issue where 95MB recorded videos failed silently with `FUNCTION_PAYLOAD_TOO_LARGE` error - ## [1.8.3] - 2026-01-15 ### Added - **Team Invitation Emails**: Automatic email notifications when users are invited to teams @@ -44,7 +48,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Uses team or user email credentials (fallback to team if user has none) - Created `src/lib/email/templates/team-invitation.ts` for email generation - Email sent automatically when team member is invited via POST `/api/teams/[id]/members` - ## [1.8.2] - 2026-01-15 ### Fixed - **Theme Color Swatches Missing**: Fixed color swatches not displaying in theme switcher diff --git a/package.json b/package.json index 9ccab56..563a798 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cheesebox", - "version": "1.8.5", + "version": "1.8.6", "description": "Secure video sharing platform with HLS streaming and AWS S3 storage", "author": "Cheesebox Contributors", "license": "MIT", From 00001060067a9b2b9b217d85bdf2958cc610e4db Mon Sep 17 00:00:00 2001 From: Brandon Estrella Date: Wed, 21 Jan 2026 13:35:41 -0800 Subject: [PATCH 04/12] v1.8.7 --- CHANGELOG.md | 4 +++- middleware.ts | 13 +++++-------- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7766c64..ed4cc32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] - +## [1.8.7] - 2026-01-21 +### Fixed +- The CSRF protection is now cleaner and more robust - the middleware handles validation, and /api/csrf-token is the sole authority for token generation and distribution. ## [1.8.6] - 2026-01-21 ### Added - JWT secret example to local environment configuration diff --git a/middleware.ts b/middleware.ts index a5148ca..485690f 100644 --- a/middleware.ts +++ b/middleware.ts @@ -26,16 +26,13 @@ export async function middleware(request: NextRequest) { } } - // For GET requests to API routes, ensure CSRF token cookie is set - if (request.method === 'GET') { - const token = getCsrfToken(request); - const response = NextResponse.next(); - setCsrfTokenCookie(response, token); - return response; - } + // Don't set CSRF cookies for API routes in middleware. + // The /api/csrf-token endpoint is the sole source of truth for token generation. + // This avoids race conditions where middleware and route handlers generate different tokens. + return NextResponse.next(); } - // For all other requests, ensure CSRF token is available + // For non-API requests (pages), ensure CSRF token cookie is available const token = getCsrfToken(request); const response = NextResponse.next(); setCsrfTokenCookie(response, token); diff --git a/package-lock.json b/package-lock.json index 517826d..eae3006 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cheesebox", - "version": "1.6.3", + "version": "1.8.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cheesebox", - "version": "1.6.3", + "version": "1.8.6", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 563a798..fd59702 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cheesebox", - "version": "1.8.6", + "version": "1.8.7", "description": "Secure video sharing platform with HLS streaming and AWS S3 storage", "author": "Cheesebox Contributors", "license": "MIT", From acf683f11e04917377912cfca25a283b356a5eeb Mon Sep 17 00:00:00 2001 From: Brandon Estrella Date: Thu, 22 Jan 2026 18:16:42 -0800 Subject: [PATCH 05/12] v1.8.8 add Google Analytics --- .env.example | 3 +++ CHANGELOG.md | 1 + src/app/layout.tsx | 2 ++ src/components/GoogleAnalytics.tsx | 28 ++++++++++++++++++++++++++++ 4 files changed, 34 insertions(+) create mode 100644 src/components/GoogleAnalytics.tsx diff --git a/.env.example b/.env.example index ff24c70..c57ebb6 100644 --- a/.env.example +++ b/.env.example @@ -67,3 +67,6 @@ UPSTASH_REDIS_REST_TOKEN="your-upstash-redis-token" # - SMTP (Gmail, Outlook, custom servers) # # No environment variables needed for email! + +# Google Analytics +NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX diff --git a/CHANGELOG.md b/CHANGELOG.md index ed4cc32..623adb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + ## [1.8.7] - 2026-01-21 ### Fixed - The CSRF protection is now cleaner and more robust - the middleware handles validation, and /api/csrf-token is the sole authority for token generation and distribution. diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4247749..7d40ca0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import "./globals.css"; import SessionProvider from "@/components/SessionProvider"; import ThemeProviderWrapper from "@/components/ThemeProviderWrapper"; import { Analytics } from "@vercel/analytics/react"; +import GoogleAnalytics from "@/components/GoogleAnalytics"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; @@ -60,6 +61,7 @@ export default async function RootLayout({ {children} + ); diff --git a/src/components/GoogleAnalytics.tsx b/src/components/GoogleAnalytics.tsx new file mode 100644 index 0000000..11c81b5 --- /dev/null +++ b/src/components/GoogleAnalytics.tsx @@ -0,0 +1,28 @@ +"use client"; + +import Script from "next/script"; + +const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID; + +export default function GoogleAnalytics() { + if (!GA_MEASUREMENT_ID) { + return null; + } + + return ( + <> + + + ); +} From aa2fe4017613dbd5f7b4eb437b7ba4bacb37b400 Mon Sep 17 00:00:00 2001 From: Sonu Rauniyar Date: Mon, 26 Jan 2026 20:57:23 +0545 Subject: [PATCH 06/12] Adding logic to disable copying of video link and corrected direct video link --- src/components/ShareVideoModal.tsx | 74 ++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/components/ShareVideoModal.tsx b/src/components/ShareVideoModal.tsx index 9843541..80f4958 100644 --- a/src/components/ShareVideoModal.tsx +++ b/src/components/ShareVideoModal.tsx @@ -51,6 +51,8 @@ export default function ShareVideoModal({ const [previouslySharedUsers, setPreviouslySharedUsers] = useState([]); const [showSuggestions, setShowSuggestions] = useState(false); const [filteredSuggestions, setFilteredSuggestions] = useState([]); + const [linkCopied, setLinkCopied] = useState(false); + const [videoVisibility, setVideoVisibility] = useState(null); const inputRef = useRef(null); const suggestionsRef = useRef(null); @@ -90,6 +92,9 @@ export default function ShareVideoModal({ if (video.groupShares) { setGroupShares(video.groupShares); } + if (video.visibility) { + setVideoVisibility(video.visibility); + } } } } catch (error) { @@ -247,6 +252,17 @@ export default function ShareVideoModal({ } }; + const handleCopyLink = async () => { + const videoUrl = `${window.location.origin}/embed/${videoId}`; + try { + await navigator.clipboard.writeText(videoUrl); + setLinkCopied(true); + setTimeout(() => setLinkCopied(false), 2000); + } catch (error) { + dev.error("Failed to copy link:", error, { tag: "share-video" }); + } + }; + return (
    @@ -499,6 +515,64 @@ export default function ShareVideoModal({ )} )} + + {/* Copy Link Button */} +
    + + {videoVisibility === "PUBLIC" ? ( +

    + Use the direct video link to share the video. +

    + ) : ( +

    + To get the direct video link, set video visibility to public. +

    + )} +
    ); From 3b9d07566fb78b25885add30644012a1281494b0 Mon Sep 17 00:00:00 2001 From: Sonu Rauniyar Date: Mon, 26 Jan 2026 21:04:17 +0545 Subject: [PATCH 07/12] Revert "Adding logic to disable copying of video link and corrected direct video link" This reverts commit aa2fe4017613dbd5f7b4eb437b7ba4bacb37b400. --- src/components/ShareVideoModal.tsx | 74 ------------------------------ 1 file changed, 74 deletions(-) diff --git a/src/components/ShareVideoModal.tsx b/src/components/ShareVideoModal.tsx index 80f4958..9843541 100644 --- a/src/components/ShareVideoModal.tsx +++ b/src/components/ShareVideoModal.tsx @@ -51,8 +51,6 @@ export default function ShareVideoModal({ const [previouslySharedUsers, setPreviouslySharedUsers] = useState([]); const [showSuggestions, setShowSuggestions] = useState(false); const [filteredSuggestions, setFilteredSuggestions] = useState([]); - const [linkCopied, setLinkCopied] = useState(false); - const [videoVisibility, setVideoVisibility] = useState(null); const inputRef = useRef(null); const suggestionsRef = useRef(null); @@ -92,9 +90,6 @@ export default function ShareVideoModal({ if (video.groupShares) { setGroupShares(video.groupShares); } - if (video.visibility) { - setVideoVisibility(video.visibility); - } } } } catch (error) { @@ -252,17 +247,6 @@ export default function ShareVideoModal({ } }; - const handleCopyLink = async () => { - const videoUrl = `${window.location.origin}/embed/${videoId}`; - try { - await navigator.clipboard.writeText(videoUrl); - setLinkCopied(true); - setTimeout(() => setLinkCopied(false), 2000); - } catch (error) { - dev.error("Failed to copy link:", error, { tag: "share-video" }); - } - }; - return (
    @@ -515,64 +499,6 @@ export default function ShareVideoModal({ )} )} - - {/* Copy Link Button */} -
    - - {videoVisibility === "PUBLIC" ? ( -

    - Use the direct video link to share the video. -

    - ) : ( -

    - To get the direct video link, set video visibility to public. -

    - )} -
    ); From 90fdf45d82f7f3931dd9111366912629c5b4e868 Mon Sep 17 00:00:00 2001 From: Sonu Rauniyar Date: Wed, 28 Jan 2026 23:58:46 +0545 Subject: [PATCH 08/12] [dev] -> Fix implementing an easy copy for public video URL (#35) * Fix implementing easy copy for public video URL * Refactor video visibility handling to use a centralized VideoVisibility type * Add URL utility functions for consistent embed URL construction * Converted SVG into an icons --- src/app/api/videos/[id]/visibility/route.ts | 4 +- src/components/EmbedCodeModal.tsx | 9 +-- src/components/ShareVideoModal.tsx | 75 +++++++++++++++++++++ src/components/VideoList.tsx | 3 +- src/components/icons/CheckIcon.tsx | 21 ++++++ src/components/icons/LinkIcon.tsx | 21 ++++++ src/lib/deep-link.ts | 11 +-- src/lib/url-utils.ts | 25 +++++++ src/types/video.ts | 13 ++++ 9 files changed, 169 insertions(+), 13 deletions(-) create mode 100644 src/components/icons/CheckIcon.tsx create mode 100644 src/components/icons/LinkIcon.tsx create mode 100644 src/lib/url-utils.ts create mode 100644 src/types/video.ts diff --git a/src/app/api/videos/[id]/visibility/route.ts b/src/app/api/videos/[id]/visibility/route.ts index 055ddef..f471677 100644 --- a/src/app/api/videos/[id]/visibility/route.ts +++ b/src/app/api/videos/[id]/visibility/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getAuthUser } from "@/lib/auth-helpers"; import { prisma } from "@/lib/prisma"; +import { VideoVisibility } from "@/types/video"; interface RouteParams { params: Promise<{ @@ -24,7 +25,8 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { const body = await request.json(); const { visibility } = body; - if (!visibility || !["PRIVATE", "PUBLIC"].includes(visibility)) { + const validVisibilities = Object.values(VideoVisibility); + if (!visibility || !validVisibilities.includes(visibility)) { return NextResponse.json( { error: "Invalid visibility value" }, { status: 400 } diff --git a/src/components/EmbedCodeModal.tsx b/src/components/EmbedCodeModal.tsx index 464c9ba..5511aac 100644 --- a/src/components/EmbedCodeModal.tsx +++ b/src/components/EmbedCodeModal.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { getEmbedUrl } from "@/lib/url-utils"; interface EmbedCodeModalProps { videoId: string; @@ -16,13 +17,7 @@ export default function EmbedCodeModal({ "responsive" ); - // Get the app URL (works for both local dev and production) - const getAppUrl = () => { - if (typeof window === "undefined") return ""; - return window.location.origin; - }; - - const embedUrl = `${getAppUrl()}/embed/${videoId}`; + const embedUrl = getEmbedUrl(videoId); // Generate embed code const getEmbedCode = () => { diff --git a/src/components/ShareVideoModal.tsx b/src/components/ShareVideoModal.tsx index 9843541..bc7f3bc 100644 --- a/src/components/ShareVideoModal.tsx +++ b/src/components/ShareVideoModal.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useRef } from "react"; import { fetchWithCsrf } from "@/lib/csrf-client"; import dev from "@onamfc/developer-log"; +import type { VideoVisibility } from "@/types/video"; interface ShareVideoModalProps { videoId: string; @@ -51,6 +52,8 @@ export default function ShareVideoModal({ const [previouslySharedUsers, setPreviouslySharedUsers] = useState([]); const [showSuggestions, setShowSuggestions] = useState(false); const [filteredSuggestions, setFilteredSuggestions] = useState([]); + const [linkCopied, setLinkCopied] = useState(false); + const [videoVisibility, setVideoVisibility] = useState(null); const inputRef = useRef(null); const suggestionsRef = useRef(null); @@ -90,6 +93,9 @@ export default function ShareVideoModal({ if (video.groupShares) { setGroupShares(video.groupShares); } + if (video.visibility) { + setVideoVisibility(video.visibility); + } } } } catch (error) { @@ -247,6 +253,17 @@ export default function ShareVideoModal({ } }; + const handleCopyLink = async () => { + const videoUrl = `${window.location.origin}/embed/${videoId}`; + try { + await navigator.clipboard.writeText(videoUrl); + setLinkCopied(true); + setTimeout(() => setLinkCopied(false), 2000); + } catch (error) { + dev.error("Failed to copy link:", error, { tag: "share-video" }); + } + }; + return (
    @@ -499,6 +516,64 @@ export default function ShareVideoModal({ )} )} + + {/* Copy Link Button */} +
    + + {videoVisibility === "PUBLIC" ? ( +

    + Use the direct video link to share the video. +

    + ) : ( +

    + To get the direct video link, set video visibility to public. +

    + )} +
    ); diff --git a/src/components/VideoList.tsx b/src/components/VideoList.tsx index b6a8978..702dcc9 100644 --- a/src/components/VideoList.tsx +++ b/src/components/VideoList.tsx @@ -9,6 +9,7 @@ import VisibilityToggle from "./VisibilityToggle"; import { Button } from "./ui/Button"; import { fetchWithCsrf } from "@/lib/csrf-client"; import { useTheme } from "@/contexts/ThemeContext"; +import type { VideoVisibility } from "@/types/video"; import { theme as defaultTheme } from "@/themes/asiago/theme"; import dev from "@onamfc/developer-log"; @@ -17,7 +18,7 @@ interface Video { title: string; description: string | null; transcodingStatus: string; - visibility: "PRIVATE" | "PUBLIC"; + visibility: VideoVisibility; createdAt: string; shares?: Array<{ sharedWithEmail: string; createdAt: string }>; sharedBy?: string; diff --git a/src/components/icons/CheckIcon.tsx b/src/components/icons/CheckIcon.tsx new file mode 100644 index 0000000..6656e8c --- /dev/null +++ b/src/components/icons/CheckIcon.tsx @@ -0,0 +1,21 @@ +interface CheckIconProps { + className?: string; +} + +export default function CheckIcon({ className = "" }: CheckIconProps) { + return ( + + + + ); +} diff --git a/src/components/icons/LinkIcon.tsx b/src/components/icons/LinkIcon.tsx new file mode 100644 index 0000000..a7e36d9 --- /dev/null +++ b/src/components/icons/LinkIcon.tsx @@ -0,0 +1,21 @@ +interface LinkIconProps { + className?: string; +} + +export default function LinkIcon({ className = "" }: LinkIconProps) { + return ( + + + + ); +} diff --git a/src/lib/deep-link.ts b/src/lib/deep-link.ts index a157407..d7f734f 100644 --- a/src/lib/deep-link.ts +++ b/src/lib/deep-link.ts @@ -1,3 +1,6 @@ +import { VideoVisibility } from "@/types/video"; +import { getEmbedUrl } from "@/lib/url-utils"; + /** * Deep Link Service using LinkForty * @@ -9,7 +12,7 @@ interface DeepLinkOptions { videoId: string; recipientEmail?: string; - visibility?: 'PUBLIC' | 'PRIVATE'; + visibility?: VideoVisibility; } export class DeepLinkService { @@ -30,11 +33,11 @@ export class DeepLinkService { * TODO: Switch back to universal deep links when mobile app launches */ generateVideoShareLink(options: DeepLinkOptions): string { - const { videoId, visibility = 'PUBLIC' } = options; + const { videoId, visibility = VideoVisibility.PUBLIC } = options; // Public videos can be viewed via /embed (no authentication required) - if (visibility === 'PUBLIC') { - return `${this.webAppUrl}/embed/${videoId}`; + if (visibility === VideoVisibility.PUBLIC) { + return getEmbedUrl(videoId, this.webAppUrl); } // Private videos require authentication - direct to watch page diff --git a/src/lib/url-utils.ts b/src/lib/url-utils.ts new file mode 100644 index 0000000..2d4ca4a --- /dev/null +++ b/src/lib/url-utils.ts @@ -0,0 +1,25 @@ +/** + * URL utilities for consistent URL construction across the application + */ + +/** + * Get the application base URL + * - Client-side: Uses window.location.origin + * - Server-side: Uses NEXT_PUBLIC_APP_URL environment variable + */ +export function getAppBaseUrl(): string { + if (typeof window !== "undefined") { + return window.location.origin; + } + return process.env.NEXT_PUBLIC_APP_URL || ""; +} + +/** + * Get the embed URL for a video + * @param videoId - The video ID + * @param baseUrl - Optional base URL override (useful for server-side with custom domains) + */ +export function getEmbedUrl(videoId: string, baseUrl?: string): string { + const origin = baseUrl ?? getAppBaseUrl(); + return `${origin}/embed/${videoId}`; +} diff --git a/src/types/video.ts b/src/types/video.ts new file mode 100644 index 0000000..22c85cd --- /dev/null +++ b/src/types/video.ts @@ -0,0 +1,13 @@ +/** + * Video visibility type - ORM-agnostic definition + * + * This provides a single source of truth for video visibility values, + * independent of the database ORM (Prisma, Drizzle, etc.) + */ +export const VideoVisibility = { + PUBLIC: "PUBLIC", + PRIVATE: "PRIVATE", +} as const; + +export type VideoVisibility = + (typeof VideoVisibility)[keyof typeof VideoVisibility]; From ca294149f86aba42abe1ea21107056e45ebb2fd8 Mon Sep 17 00:00:00 2001 From: Sonu Rauniyar Date: Thu, 29 Jan 2026 00:19:21 +0545 Subject: [PATCH 09/12] =?UTF-8?q?[dev]=20->=20Implement=20ability=20to=20s?= =?UTF-8?q?ort=20videos=20by=20title=20(A=E2=86=92Z,=20Z=E2=86=92A)=20and?= =?UTF-8?q?=20date=20(newest,=20oldest)=20(#34)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add sorting functionality for video list and dashboard - Introduced SortOption enum to define sorting criteria. - Implemented sorting in VideoList component based on selected sort option. - Added sort option selector in DashboardPage for user preference. * Completed removing storing sort option in localstorage * Implement persistent sorting for video list and update sorting options * Add type guard for sort options and improve sort option handling in dashboard --------- Co-authored-by: Brandon Estrella --- src/app/dashboard/page.tsx | 38 ++++++++++++++++++++++++++++++++++++ src/components/VideoList.tsx | 30 +++++++++++++++++++++++++--- src/types/video.ts | 10 ++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index ddafce8..7addfd2 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -12,6 +12,7 @@ import { LinkButton, Button } from "@/components/ui/Button"; import { useTheme } from "@/contexts/ThemeContext"; import { theme as defaultTheme } from "@/themes/asiago/theme"; import dev from "@onamfc/developer-log"; +import { SortOption, isSortOption } from "@/types/video"; type Tab = "my-videos" | "shared"; type ViewMode = "grid" | "list"; @@ -41,6 +42,13 @@ export default function DashboardPage() { } return "grid"; }); + const [sortOption, setSortOption] = useState(() => { + if (typeof window !== "undefined") { + const saved = localStorage.getItem("videoSortOption"); + return saved && isSortOption(saved) ? saved : SortOption.NEWEST; + } + return SortOption.NEWEST; + }); // Get theme configuration const spacing = themeConfig?.spacing || defaultTheme.spacing; @@ -58,6 +66,12 @@ export default function DashboardPage() { } }, [viewMode]); + useEffect(() => { + if (typeof window !== "undefined") { + localStorage.setItem("videoSortOption", sortOption); + } + }, [sortOption]); + useEffect(() => { // Check if user has AWS credentials or is part of a team const checkCredentials = async () => { @@ -187,6 +201,29 @@ export default function DashboardPage() {
    + + +
    diff --git a/src/components/VideoList.tsx b/src/components/VideoList.tsx index 702dcc9..cc5a604 100644 --- a/src/components/VideoList.tsx +++ b/src/components/VideoList.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import VideoPlayer from "./VideoPlayer"; import ShareVideoModal from "./ShareVideoModal"; import EmbedCodeModal from "./EmbedCodeModal"; @@ -12,6 +12,7 @@ import { useTheme } from "@/contexts/ThemeContext"; import type { VideoVisibility } from "@/types/video"; import { theme as defaultTheme } from "@/themes/asiago/theme"; import dev from "@onamfc/developer-log"; +import { SortOption } from "@/types/video"; interface Video { id: string; @@ -33,9 +34,10 @@ interface VideoListProps { groupId?: string | null; viewMode?: "grid" | "list"; compact?: boolean; + sortBy?: SortOption; } -export default function VideoList({ type, teamId, groupId, viewMode = "grid", compact = false }: VideoListProps) { +export default function VideoList({ type, teamId, groupId, viewMode = "grid", compact = false, sortBy = SortOption.NEWEST }: VideoListProps) { const { themeConfig } = useTheme(); const [videos, setVideos] = useState([]); const [loading, setLoading] = useState(true); @@ -52,6 +54,28 @@ export default function VideoList({ type, teamId, groupId, viewMode = "grid", co dev.log("VideoList - Layout:", layout, {tag: 'layout'}); dev.log("VideoList - ViewMode:", viewMode, "->", effectiveViewMode, {tag: 'layout'}); + // Sort videos based on sortBy option + const sortedVideos = useMemo(() => { + if (!videos.length) return videos; + + const sorted = [...videos]; + switch (sortBy) { + case SortOption.NEWEST: + sorted.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + break; + case SortOption.OLDEST: + sorted.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); + break; + case SortOption.A_TO_Z: + sorted.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase())); + break; + case SortOption.Z_TO_A: + sorted.sort((a, b) => b.title.toLowerCase().localeCompare(a.title.toLowerCase())); + break; + } + return sorted; + }, [videos, sortBy]); + // Generate grid class names based on theme using explicit conditionals // This ensures Tailwind can detect all possible classes at build time const getGridClasses = () => { @@ -163,7 +187,7 @@ export default function VideoList({ type, teamId, groupId, viewMode = "grid", co return ( <>
    - {videos.map((video) => ( + {sortedVideos.map((video) => (
    Date: Wed, 28 Jan 2026 10:35:58 -0800 Subject: [PATCH 10/12] internal release workflow (#36) --- RELEASE_WORKFLOW.md | 109 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 RELEASE_WORKFLOW.md diff --git a/RELEASE_WORKFLOW.md b/RELEASE_WORKFLOW.md new file mode 100644 index 0000000..d896c92 --- /dev/null +++ b/RELEASE_WORKFLOW.md @@ -0,0 +1,109 @@ +# Release Workflow + +This document describes the recommended release workflow for the Cheesebox project. This is a suggestion for internal teams and contributors - adapt it to fit your needs. + +## Branch Structure + +``` +feature branches → dev (via PRs) → release branch → main (release PR) +``` + +| Branch | Purpose | +|-----------------|-----------------------------------------| +| `main` | Production-ready code, tagged releases | +| `dev` | Integration branch for ongoing work | +| `release/x.x.x` | Temporary branch for preparing releases | + +## Day-to-Day Development + +1. Create feature/fix branches from `dev` +2. Open PRs targeting `dev` +3. Review and merge PRs into `dev` +4. **Don't update changelog yet** - changes are "in flight" + +## Creating a Release + +When ready to release: + +```bash +# 1. Create release branch from dev +git checkout dev && git pull +git checkout -b release/1.9.0 + +# 2. Bump version in package.json +npm version minor --no-git-tag-version # or patch/major + +# 3. Update CHANGELOG.md +# - Review commits since last release +# - Add entries under new version header +git log v1.8.8..dev --oneline --no-merges + +# 4. Commit version bump and changelog +git add package.json CHANGELOG.md +git commit -m "v1.9.0" + +# 5. Push and create PR into main +git push -u origin release/1.9.0 +gh pr create --base main --title "v1.9.0" +``` + +## After Merging to Main + +```bash +# Tag the release +git checkout main && git pull +git tag v1.9.0 +git push --tags + +# Sync dev with main +git checkout dev +git merge main +git push +``` + +## Quick Reference + +| Step | Branch | Changelog | Version | +|-----------------------|---------------|--------------|--------------| +| Feature PRs | → dev | No | No | +| Create release branch | release/x.x.x | **Update** | **Bump** | +| Release PR | → main | Already done | Already done | +| After merge | main | Tag only | — | +| Sync back | main → dev | — | — | + +## Versioning + +This project follows [Semantic Versioning](https://semver.org/): + +- **MAJOR** (1.0.0 → 2.0.0): Breaking changes +- **MINOR** (1.0.0 → 1.1.0): New features, backwards compatible +- **PATCH** (1.0.0 → 1.0.1): Bug fixes, backwards compatible + +## Changelog Format + +The changelog follows [Keep a Changelog](https://keepachangelog.com/) format: + +```markdown +## [1.9.0] - 2026-01-28 +### Added +- New features + +### Changed +- Changes to existing functionality + +### Fixed +- Bug fixes + +### Security +- Security improvements +``` + +## Viewing Changes Since Last Release + +```bash +# See commits since last tag +git log v1.8.8..dev --oneline --no-merges + +# See file changes +git diff v1.8.8..dev --stat +``` From 6139adc70eb770790603e6003e76c019b80bc451 Mon Sep 17 00:00:00 2001 From: Sonu Rauniyar Date: Thu, 29 Jan 2026 03:56:54 +0545 Subject: [PATCH 11/12] [dev] -> Update AWS setup guide and CloudFormation template download instructions & flow (#33) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update AWS setup guide and CloudFormation template download instructions * CloudFormation Template (private-video-setup.yaml) - Fixed typo: ExposeHeaders → ExposedHeaders - Removed redundant hardcoded localhost:3000 from CORS (the AppDomain parameter with localhost default already handles this) - The StepHeader and CodeBlock components were moved outside the main component and now receive state/callbacks as props to prevent the components from being recreated on every render * 1.Synced with dev - The dev branch already had the component refactoring (including InlineCode) done properly 2.Applied URL fix - Changed the template URL from raw.githubusercontent.com to www.cheesebox.io --------- Co-authored-by: Brandon Estrella --- public/cloudformation/private-video-setup.yaml | 3 +-- src/app/help/aws-setup/page.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/public/cloudformation/private-video-setup.yaml b/public/cloudformation/private-video-setup.yaml index d9e85d3..815b0fe 100644 --- a/public/cloudformation/private-video-setup.yaml +++ b/public/cloudformation/private-video-setup.yaml @@ -49,9 +49,8 @@ Resources: - HEAD - PUT AllowedOrigins: - - 'http://localhost:3000' - !Ref AppDomain - ExposeHeaders: + ExposedHeaders: - Content-Length - Content-Range - ETag diff --git a/src/app/help/aws-setup/page.tsx b/src/app/help/aws-setup/page.tsx index af84091..bfdb67f 100644 --- a/src/app/help/aws-setup/page.tsx +++ b/src/app/help/aws-setup/page.tsx @@ -585,7 +585,7 @@ export default function AWSSetupGuide() {
    Date: Wed, 28 Jan 2026 14:15:17 -0800 Subject: [PATCH 12/12] v1.8.10 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 476b127..e343ffd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [1.8.10] - 2026-01-28 +-Update AWS setup guide and CloudFormation template download instructions +- CloudFormation Template (private-video-setup.yaml) + - Fixed typo: ExposeHeaders → ExposedHeaders + - Removed redundant hardcoded localhost:3000 from CORS (the AppDomain parameter with localhost default already handles this) + - The StepHeader and CodeBlock components were moved outside the main component and now receive state/callbacks as props to prevent the components from being recreated on every render +- 1.Synced with dev - The dev branch already had the component refactoring (including InlineCode) done properly +- Applied URL fix - Changed the template URL from raw.githubusercontent.com to www.cheesebox.io ## [1.8.9] - 2026-01-28 ### Added - Add sorting functionality for video list and dashboard diff --git a/package-lock.json b/package-lock.json index 9036b4f..7ce6dad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cheesebox", - "version": "1.8.9", + "version": "1.8.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cheesebox", - "version": "1.8.9", + "version": "1.8.10", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index d6ffd92..942dc2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cheesebox", - "version": "1.8.9", + "version": "1.8.10", "description": "Secure video sharing platform with HLS streaming and AWS S3 storage", "author": "Cheesebox Contributors", "license": "MIT",