diff options
Diffstat (limited to 'apps/web/src/app')
| -rw-r--r-- | apps/web/src/app/api/auth/[...all]/route.ts | 4 | ||||
| -rw-r--r-- | apps/web/src/app/api/health/route.ts | 3 | ||||
| -rw-r--r-- | apps/web/src/app/api/sounds/search/route.ts | 280 | ||||
| -rw-r--r-- | apps/web/src/app/blog/[slug]/page.tsx | 135 | ||||
| -rw-r--r-- | apps/web/src/app/blog/page.tsx | 52 | ||||
| -rw-r--r-- | apps/web/src/app/changelog/[version]/page.tsx | 2 | ||||
| -rw-r--r-- | apps/web/src/app/editor/page.tsx (renamed from apps/web/src/app/editor/[project_id]/page.tsx) | 16 | ||||
| -rw-r--r-- | apps/web/src/app/layout.tsx | 12 | ||||
| -rw-r--r-- | apps/web/src/app/projects/page.tsx | 10 | ||||
| -rw-r--r-- | apps/web/src/app/robots.ts | 2 | ||||
| -rw-r--r-- | apps/web/src/app/rss.xml/route.ts | 46 | ||||
| -rw-r--r-- | apps/web/src/app/sitemap.ts | 100 |
12 files changed, 61 insertions, 601 deletions
diff --git a/apps/web/src/app/api/auth/[...all]/route.ts b/apps/web/src/app/api/auth/[...all]/route.ts deleted file mode 100644 index edf0d0c2..00000000 --- a/apps/web/src/app/api/auth/[...all]/route.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { auth } from "@/auth/server";
-import { toNextJsHandler } from "better-auth/next-js";
-
-export const { POST, GET } = toNextJsHandler(auth);
diff --git a/apps/web/src/app/api/health/route.ts b/apps/web/src/app/api/health/route.ts deleted file mode 100644 index dbe6fdb1..00000000 --- a/apps/web/src/app/api/health/route.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function GET() {
- return new Response("OK", { status: 200 });
-}
diff --git a/apps/web/src/app/api/sounds/search/route.ts b/apps/web/src/app/api/sounds/search/route.ts deleted file mode 100644 index aa7a1b60..00000000 --- a/apps/web/src/app/api/sounds/search/route.ts +++ /dev/null @@ -1,280 +0,0 @@ -import { webEnv } from "@/env/web"; -import { type NextRequest, NextResponse } from "next/server"; -import { z } from "zod"; -import { checkRateLimit } from "@/auth/rate-limit"; - -const searchParamsSchema = z.object({ - q: z.string().max(500, "Query too long").optional(), - type: z.enum(["songs", "effects"]).optional(), - page: z.coerce.number().int().min(1).max(1000).default(1), - page_size: z.coerce.number().int().min(1).max(150).default(20), - sort: z - .enum(["downloads", "rating", "created", "score"]) - .default("downloads"), - min_rating: z.coerce.number().min(0).max(5).default(3), - commercial_only: z.coerce.boolean().default(true), -}); - -const freesoundResultSchema = z.object({ - id: z.number(), - name: z.string(), - description: z.string(), - url: z.string().url(), - previews: z - .object({ - "preview-hq-mp3": z.string().url(), - "preview-lq-mp3": z.string().url(), - "preview-hq-ogg": z.string().url(), - "preview-lq-ogg": z.string().url(), - }) - .optional(), - download: z.string().url().optional(), - duration: z.number(), - filesize: z.number(), - type: z.string(), - channels: z.number(), - bitrate: z.number(), - bitdepth: z.number(), - samplerate: z.number(), - username: z.string(), - tags: z.array(z.string()), - license: z.string(), - created: z.string(), - num_downloads: z.number().optional(), - avg_rating: z.number().optional(), - num_ratings: z.number().optional(), -}); - -const freesoundResponseSchema = z.object({ - count: z.number(), - next: z.string().url().nullable(), - previous: z.string().url().nullable(), - results: z.array(freesoundResultSchema), -}); - -const transformedResultSchema = z.object({ - id: z.number(), - name: z.string(), - description: z.string(), - url: z.string(), - previewUrl: z.string().optional(), - downloadUrl: z.string().optional(), - duration: z.number(), - filesize: z.number(), - type: z.string(), - channels: z.number(), - bitrate: z.number(), - bitdepth: z.number(), - samplerate: z.number(), - username: z.string(), - tags: z.array(z.string()), - license: z.string(), - created: z.string(), - downloads: z.number().optional(), - rating: z.number().optional(), - ratingCount: z.number().optional(), -}); - -const apiResponseSchema = z.object({ - count: z.number(), - next: z.string().nullable(), - previous: z.string().nullable(), - results: z.array(transformedResultSchema), - query: z.string().optional(), - type: z.string(), - page: z.number(), - pageSize: z.number(), - sort: z.string(), - minRating: z.number().optional(), -}); - -function buildSortParameter({ query, sort }: { query?: string; sort: string }) { - if (!query) return `${sort}_desc`; - return sort === "score" ? "score" : `${sort}_desc`; -} - -function applyEffectsFilters({ - params, - min_rating, - commercial_only, -}: { - params: URLSearchParams; - min_rating: number; - commercial_only: boolean; -}) { - params.append("filter", "duration:[* TO 30.0]"); - params.append("filter", `avg_rating:[${min_rating} TO *]`); - - if (commercial_only) { - params.append( - "filter", - 'license:("Attribution" OR "Creative Commons 0" OR "Attribution Noncommercial" OR "Attribution Commercial")', - ); - } - - params.append( - "filter", - "tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion", - ); -} - -function transformFreesoundResult( - result: z.infer<typeof freesoundResultSchema>, -) { - return { - id: result.id, - name: result.name, - description: result.description, - url: result.url, - previewUrl: - result.previews?.["preview-hq-mp3"] || - result.previews?.["preview-lq-mp3"], - downloadUrl: result.download, - duration: result.duration, - filesize: result.filesize, - type: result.type, - channels: result.channels, - bitrate: result.bitrate, - bitdepth: result.bitdepth, - samplerate: result.samplerate, - username: result.username, - tags: result.tags, - license: result.license, - created: result.created, - downloads: result.num_downloads || 0, - rating: result.avg_rating || 0, - ratingCount: result.num_ratings || 0, - }; -} - -export async function GET(request: NextRequest) { - try { - const { limited } = await checkRateLimit({ request }); - if (limited) { - return NextResponse.json({ error: "Too many requests" }, { status: 429 }); - } - - const { searchParams } = new URL(request.url); - - const validationResult = searchParamsSchema.safeParse({ - q: searchParams.get("q") || undefined, - type: searchParams.get("type") || undefined, - page: searchParams.get("page") || undefined, - page_size: searchParams.get("page_size") || undefined, - sort: searchParams.get("sort") || undefined, - min_rating: searchParams.get("min_rating") || undefined, - }); - - if (!validationResult.success) { - return NextResponse.json( - { - error: "Invalid parameters", - details: validationResult.error.flatten().fieldErrors, - }, - { status: 400 }, - ); - } - - const { - q: query, - type, - page, - page_size: pageSize, - sort, - min_rating, - commercial_only, - } = validationResult.data; - - if (type === "songs") { - return NextResponse.json( - { - error: "Songs are not available yet", - message: - "Song search functionality is coming soon. Try searching for sound effects instead.", - }, - { status: 501 }, - ); - } - - const baseUrl = "https://freesound.org/apiv2/search/text/"; - - const sortParam = buildSortParameter({ query, sort }); - - const params = new URLSearchParams({ - query: query || "", - token: webEnv.FREESOUND_API_KEY, - page: page.toString(), - page_size: pageSize.toString(), - sort: sortParam, - fields: - "id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings", - }); - - const isEffectsSearch = type === "effects" || !type; - if (isEffectsSearch) { - applyEffectsFilters({ params, min_rating, commercial_only }); - } - - const response = await fetch(`${baseUrl}?${params.toString()}`); - - if (!response.ok) { - const errorText = await response.text(); - console.error("Freesound API error:", response.status, errorText); - return NextResponse.json( - { error: "Failed to search sounds" }, - { status: response.status }, - ); - } - - const rawData = await response.json(); - - const freesoundValidation = freesoundResponseSchema.safeParse(rawData); - if (!freesoundValidation.success) { - console.error( - "Invalid Freesound API response:", - freesoundValidation.error, - ); - return NextResponse.json( - { error: "Invalid response from Freesound API" }, - { status: 502 }, - ); - } - - const data = freesoundValidation.data; - - const transformedResults = data.results.map(transformFreesoundResult); - - const responseData = { - count: data.count, - next: data.next, - previous: data.previous, - results: transformedResults, - query: query || "", - type: type || "effects", - page, - pageSize, - sort, - minRating: min_rating, - }; - - const responseValidation = apiResponseSchema.safeParse(responseData); - if (!responseValidation.success) { - console.error( - "Invalid API response structure:", - responseValidation.error, - ); - return NextResponse.json( - { error: "Internal response formatting error" }, - { status: 500 }, - ); - } - - return NextResponse.json(responseValidation.data); - } catch (error) { - console.error("Error searching sounds:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 }, - ); - } -} diff --git a/apps/web/src/app/blog/[slug]/page.tsx b/apps/web/src/app/blog/[slug]/page.tsx deleted file mode 100644 index dba9b504..00000000 --- a/apps/web/src/app/blog/[slug]/page.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import type { Metadata } from "next";
-import Image from "next/image";
-import { notFound } from "next/navigation";
-import { BasePage } from "@/app/base-page";
-import Prose from "@/components/ui/prose";
-import { Separator } from "@/components/ui/separator";
-import { getPosts, getSinglePost, processHtmlContent } from "@/blog/query";
-import type { Author, Post } from "@/blog/types";
-
-type PageProps = {
- params: Promise<{ slug: string }>;
- searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
-};
-
-export async function generateMetadata({
- params,
-}: PageProps): Promise<Metadata> {
- const slug = (await params).slug;
-
- const data = await getSinglePost({ slug });
-
- if (!data || !data.post) return {};
-
- return {
- title: data.post.title,
- description: data.post.description,
- twitter: {
- title: `${data.post.title}`,
- description: `${data.post.description}`,
- card: "summary_large_image",
- images: [
- {
- url: data.post.coverImage,
- width: "1200",
- height: "630",
- alt: data.post.title,
- },
- ],
- },
- openGraph: {
- type: "article",
- images: [
- {
- url: data.post.coverImage,
- width: "1200",
- height: "630",
- alt: data.post.title,
- },
- ],
- title: data.post.title,
- description: data.post.description,
- publishedTime: new Date(data.post.publishedAt).toISOString(),
- authors: data.post.authors.map((author: Author) => author.name),
- },
- };
-}
-
-export async function generateStaticParams() {
- const data = await getPosts();
- if (!data || !data.posts.length) return [];
-
- return data.posts.map((post) => ({
- slug: post.slug,
- }));
-}
-
-export default async function BlogPostPage({ params }: PageProps) {
- const slug = (await params).slug;
- const data = await getSinglePost({ slug });
- if (!data || !data.post) return notFound();
-
- const html = await processHtmlContent({ html: data.post.content });
-
- return (
- <BasePage>
- <PostHeader post={data.post} />
- <Separator />
- <PostContent html={html} />
- </BasePage>
- );
-}
-
-function PostHeader({ post }: { post: Post }) {
- const formattedDate = new Date(post.publishedAt).toLocaleDateString("en-US", {
- day: "numeric",
- month: "long",
- year: "numeric",
- });
-
- return (
- <div className="flex flex-col items-center justify-center gap-8">
- <PostMeta date={formattedDate} publishedAt={post.publishedAt} />
- <PostTitle title={post.title} />
- {post.coverImage && <PostCoverImage post={post} />}
- </div>
- );
-}
-
-function PostCoverImage({ post }: { post: Post }) {
- return (
- <div className="relative aspect-video overflow-hidden rounded-lg w-full mt-4">
- <Image
- src={post.coverImage}
- alt={post.title}
- loading="eager"
- fill
- className="rounded-lg object-cover"
- />
- </div>
- );
-}
-
-function PostMeta({ date, publishedAt }: { date: string; publishedAt: Date }) {
- return (
- <div className="flex items-center justify-center">
- <time dateTime={publishedAt.toString()}>{date}</time>
- </div>
- );
-}
-
-function PostTitle({ title }: { title: string }) {
- return (
- <h1 className="text-5xl font-bold tracking-tight md:text-4xl text-center">
- {title}
- </h1>
- );
-}
-
-function PostContent({ html }: { html: string }) {
- return (
- <section className="">
- <Prose html={html} />
- </section>
- );
-}
diff --git a/apps/web/src/app/blog/page.tsx b/apps/web/src/app/blog/page.tsx deleted file mode 100644 index b40bc2fc..00000000 --- a/apps/web/src/app/blog/page.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import type { Metadata } from "next";
-import Link from "next/link";
-import { BasePage } from "@/app/base-page";
-import { Separator } from "@/components/ui/separator";
-import { getPosts } from "@/blog/query";
-import type { Post } from "@/blog/types";
-
-export const metadata: Metadata = {
- title: "Blog - OpenCut",
- description:
- "Read the latest news and updates about OpenCut, the free and open-source video editor.",
- openGraph: {
- title: "Blog - OpenCut",
- description:
- "Read the latest news and updates about OpenCut, the free and open-source video editor.",
- type: "website",
- },
-};
-
-export default async function BlogPage() {
- const data = await getPosts().catch(() => null);
- if (!data || !data.posts) return <div>No posts yet</div>;
-
- return (
- <BasePage
- title="Blog"
- description="Read the latest news and updates about OpenCut, the free and open-source video editor."
- >
- <div className="flex flex-col">
- {data.posts.map((post) => (
- <div key={post.id} className="flex flex-col">
- <BlogPostItem post={post} />
- <Separator />
- </div>
- ))}
- </div>
- </BasePage>
- );
-}
-
-function BlogPostItem({ post }: { post: Post }) {
- return (
- <Link href={`/blog/${post.slug}`}>
- <div className="flex h-auto w-full items-center justify-between py-6 opacity-100 hover:opacity-75">
- <div className="flex flex-col gap-2">
- <h2 className="text-xl font-semibold">{post.title}</h2>
- <p className="text-muted-foreground">{post.description}</p>
- </div>
- </div>
- </Link>
- );
-}
diff --git a/apps/web/src/app/changelog/[version]/page.tsx b/apps/web/src/app/changelog/[version]/page.tsx index 6c529ce1..b6623cf3 100644 --- a/apps/web/src/app/changelog/[version]/page.tsx +++ b/apps/web/src/app/changelog/[version]/page.tsx @@ -15,6 +15,8 @@ import { CopyMarkdownButton } from "@/changelog/components/copy-markdown-button" type Props = { params: Promise<{ version: string }> };
+export const dynamicParams = false;
+
export async function generateStaticParams() {
return getSortedReleases().map((release) => ({ version: release.version }));
}
diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/page.tsx index b759b9c2..8229774a 100644 --- a/apps/web/src/app/editor/[project_id]/page.tsx +++ b/apps/web/src/app/editor/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useParams } from "next/navigation"; +import { useSearchParams } from "next/navigation"; import { ResizablePanelGroup, ResizablePanel, @@ -17,7 +17,7 @@ import { MigrationDialog } from "@/project/components/migration-dialog"; import { usePanelStore } from "@/editor/panel-store"; import { usePasteMedia } from "@/media/use-paste-media"; import { MobileGate } from "@/components/editor/mobile-gate"; -import { useMemo, useState } from "react"; +import { Suspense, useMemo, useState } from "react"; import { useEditor } from "@/editor/use-editor"; import { Cancel01Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; @@ -36,8 +36,16 @@ import { } from "@/timeline/bookmarks/index"; export default function Editor() { - const params = useParams<{ project_id: string }>(); - const projectId = params.project_id; + return ( + <Suspense> + <EditorView /> + </Suspense> + ); +} + +function EditorView() { + const searchParams = useSearchParams(); + const projectId = searchParams.get("project") ?? ""; return ( <MobileGate> diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 558ae538..9713d9d5 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -5,21 +5,12 @@ import { Toaster } from "../components/ui/sonner"; import { ChangelogNotification } from "@/changelog/components/changelog-notification"; import { TooltipProvider } from "../components/ui/tooltip"; import { baseMetaData } from "./metadata"; -import { BotIdClient } from "botid/client"; -import { webEnv } from "@/env/web"; import { Inter } from "next/font/google"; const siteFont = Inter({ subsets: ["latin"] }); export const metadata = baseMetaData; -const protectedRoutes = [ - { - path: "/none", - method: "GET", - }, -]; - export default function RootLayout({ children, }: Readonly<{ @@ -28,7 +19,6 @@ export default function RootLayout({ return ( <html lang="en" suppressHydrationWarning> <head> - <BotIdClient protect={protectedRoutes} /> {process.env.NODE_ENV === "development" && ( <> <Script @@ -52,7 +42,7 @@ export default function RootLayout({ strategy="afterInteractive" async data-client-id="UP-Wcoy5arxFeK7oyjMMZ" - data-disabled={webEnv.NODE_ENV === "development"} + data-disabled={process.env.NODE_ENV === "development"} data-track-attributes={false} data-track-errors={true} data-track-outgoing-links={false} diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx index d238af9e..7cf50c93 100644 --- a/apps/web/src/app/projects/page.tsx +++ b/apps/web/src/app/projects/page.tsx @@ -515,7 +515,7 @@ function NewProjectButton() { const projectId = await editor.project.createNewProject({ name: "New project", }); - router.push(`/editor/${projectId}`); + router.push(`/editor?project=${projectId}`); }; return ( @@ -553,7 +553,7 @@ function ImportProjectButton() { try { const projectId = await editor.project.importArchive({ file }); if (projectId) { - router.push(`/editor/${projectId}`); + router.push(`/editor?project=${projectId}`); } } finally { setIsImporting(false); @@ -727,7 +727,7 @@ function ProjectItem({ className="size-5 shrink-0" /> - <Link href={`/editor/${project.id}`} className="flex-1 min-w-0"> + <Link href={`/editor?project=${project.id}`} className="flex-1 min-w-0"> {listRowContent} </Link> @@ -753,7 +753,7 @@ function ProjectItem({ <div className="group relative"> {isGridView ? ( <> - <Link href={`/editor/${project.id}`} className="block"> + <Link href={`/editor?project=${project.id}`} className="block"> {gridContent} </Link> @@ -1043,7 +1043,7 @@ function EmptyState() { const projectId = await editor.project.createNewProject({ name: "New project", }); - router.push(`/editor/${projectId}`); + router.push(`/editor?project=${projectId}`); } catch (error) { toast.error("Failed to create project", { description: diff --git a/apps/web/src/app/robots.ts b/apps/web/src/app/robots.ts index 1b1ca865..6f316d98 100644 --- a/apps/web/src/app/robots.ts +++ b/apps/web/src/app/robots.ts @@ -1,6 +1,8 @@ import type { MetadataRoute } from "next";
import { SITE_URL } from "@/site/brand";
+export const dynamic = "force-static";
+
export default function robots(): MetadataRoute.Robots {
return {
rules: {
diff --git a/apps/web/src/app/rss.xml/route.ts b/apps/web/src/app/rss.xml/route.ts deleted file mode 100644 index d4e9570a..00000000 --- a/apps/web/src/app/rss.xml/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Feed } from "feed";
-import { getPosts } from "@/blog/query";
-import { SITE_INFO, SITE_URL } from "@/site/brand";
-
-export async function GET() {
- try {
- const { posts } = await getPosts();
-
- const feed = new Feed({
- title: `${SITE_INFO.title} Blog`,
- description: SITE_INFO.description,
- id: `${SITE_URL}`,
- link: `${SITE_URL}/blog/`,
- language: "en",
- image: `${SITE_INFO.openGraphImage}`,
- favicon: `${SITE_INFO.favicon}`,
- copyright: `All rights reserved ${new Date().getFullYear()}, ${
- SITE_INFO.title
- }`,
- });
-
- for (const post of posts) {
- feed.addItem({
- title: post.title,
- id: `${SITE_URL}/blog/${post.slug}`,
- link: `${SITE_URL}/blog/${post.slug}`,
- description: post.description,
- author: post.authors.map((author) => ({
- name: author.name,
- })),
- date: new Date(post.publishedAt),
- image: post.coverImage || SITE_INFO.openGraphImage,
- });
- }
-
- return new Response(feed.rss2(), {
- headers: {
- "Content-Type": "text/xml",
- "Cache-Control": "public, max-age=86400, stale-while-revalidate",
- },
- });
- } catch (error) {
- console.error("Error generating RSS feed", error);
- return new Response("Internal Server Error", { status: 500 });
- }
-}
diff --git a/apps/web/src/app/sitemap.ts b/apps/web/src/app/sitemap.ts index 73b4180f..a473d2d9 100644 --- a/apps/web/src/app/sitemap.ts +++ b/apps/web/src/app/sitemap.ts @@ -1,61 +1,39 @@ -import { SITE_URL } from "@/site/brand";
-import { getPosts } from "@/blog/query";
-import type { MetadataRoute } from "next";
-
-export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
- const data = await getPosts();
-
- const postPages: MetadataRoute.Sitemap =
- data?.posts?.map((post) => ({
- url: `${SITE_URL}/blog/${post.slug}`,
- lastModified: new Date(post.publishedAt),
- changeFrequency: "weekly",
- priority: 0.8,
- })) ?? [];
-
- return [
- {
- url: SITE_URL,
- lastModified: new Date(),
- changeFrequency: "weekly",
- priority: 1,
- },
- {
- url: `${SITE_URL}/contributors`,
- lastModified: new Date(),
- changeFrequency: "daily",
- priority: 0.5,
- },
- {
- url: `${SITE_URL}/roadmap`,
- lastModified: new Date(),
- changeFrequency: "weekly",
- priority: 1,
- },
- {
- url: `${SITE_URL}/privacy`,
- lastModified: new Date(),
- changeFrequency: "monthly",
- priority: 0.5,
- },
- {
- url: `${SITE_URL}/terms`,
- lastModified: new Date(),
- changeFrequency: "monthly",
- priority: 0.5,
- },
- {
- url: `${SITE_URL}/why-not-capcut`,
- lastModified: new Date(),
- changeFrequency: "yearly",
- priority: 1,
- },
- {
- url: `${SITE_URL}/blog`,
- lastModified: new Date(),
- changeFrequency: "weekly",
- priority: 1,
- },
- ...postPages,
- ];
-}
+import { SITE_URL } from "@/site/brand"; +import type { MetadataRoute } from "next"; + +export const dynamic = "force-static"; + +export default function sitemap(): MetadataRoute.Sitemap { + return [ + { + url: SITE_URL, + lastModified: new Date(), + changeFrequency: "weekly", + priority: 1, + }, + { + url: `${SITE_URL}/contributors`, + lastModified: new Date(), + changeFrequency: "daily", + priority: 0.5, + }, + { + url: `${SITE_URL}/roadmap`, + lastModified: new Date(), + changeFrequency: "weekly", + priority: 1, + }, + { + url: `${SITE_URL}/privacy`, + lastModified: new Date(), + changeFrequency: "monthly", + priority: 0.5, + }, + { + url: `${SITE_URL}/terms`, + lastModified: new Date(), + changeFrequency: "monthly", + priority: 0.5, + }, + ]; +} |
