diff --git a/Dockerfile b/Dockerfile index 829e3d4..5887924 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \ # Stage 2: Build the application FROM oven/bun:1.3.14-alpine AS builder WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 COPY --from=deps /app/node_modules ./node_modules COPY . . # Build Next.js @@ -20,18 +21,26 @@ RUN --mount=type=cache,target=/app/.next/cache \ # Stage 3: Production runner FROM oven/bun:1.3.14-alpine AS runner +ARG TARGETARCH WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 +ENV HOSTNAME=0.0.0.0 +ENV PORT=3000 -# Copy built assets and dependencies from builder stage +# Copy the standalone server and static assets without the full dependency tree +COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/public ./public -COPY --from=builder /app/.next ./.next -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/.next/static ./.next/static + +# The standalone tracer includes both glibc and musl Sharp packages. Alpine only +# needs musl, so discard the incompatible duplicate without touching runtime deps. +RUN rm -rf \ + "./node_modules/@img/sharp-linux-${TARGETARCH}" \ + "./node_modules/@img/sharp-libvips-linux-${TARGETARCH}" EXPOSE 3000 -CMD ["bun", "start"] +CMD ["bun", "server.js"] # Stage 4: Scheduled video upload worker FROM deps AS video-upload-worker diff --git a/app/api/admin/video-file/route.ts b/app/api/admin/video-file/route.ts index e0f84f9..528ce44 100644 --- a/app/api/admin/video-file/route.ts +++ b/app/api/admin/video-file/route.ts @@ -42,10 +42,10 @@ export async function POST(request: Request) { const stagingDirectory = `${VIDEO_DIR}/staging/${discordId}`; filePath = `${stagingDirectory}/${stagedVideoId}-${safeFileName(originalFilename)}`; - await mkdir(stagingDirectory, { recursive: true }); + await mkdir(/* turbopackIgnore: true */ stagingDirectory, { recursive: true }); await pipeline( Readable.fromWeb(request.body as never), - createWriteStream(filePath, { flags: "wx" }), + createWriteStream(/* turbopackIgnore: true */ filePath, { flags: "wx" }), ); return NextResponse.json({ @@ -55,7 +55,7 @@ export async function POST(request: Request) { size: contentLength, }); } catch (error) { - if (filePath) await unlink(filePath).catch(() => undefined); + if (filePath) await unlink(/* turbopackIgnore: true */ filePath).catch(() => undefined); const message = error instanceof Error ? error.message : "Upload failed."; const status = message === "Unauthorized" || message === "Not authenticated" ? 401 : 400; diff --git a/app/globals.css b/app/globals.css index 052b7cb..73130a6 100644 --- a/app/globals.css +++ b/app/globals.css @@ -3,6 +3,42 @@ @import "tw-animate-css"; @import "shadcn/tailwind.css"; +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url("/fonts/inter-latin.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, + U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, + U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: "Anuphan"; + font-style: normal; + font-weight: 100 700; + font-display: swap; + src: url("/fonts/anuphan-thai.woff2") format("woff2"); + unicode-range: U+02D7, U+0303, U+0331, U+0E01-0E5B, U+200C-200D, U+25CC; +} + +@font-face { + font-family: "Anuphan"; + font-style: normal; + font-weight: 100 700; + font-display: swap; + src: url("/fonts/anuphan-latin.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, + U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, + U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +:root { + --font-inter: "Inter", Arial, sans-serif; + --font-anuphan: "Anuphan", Tahoma, sans-serif; +} + @custom-variant dark (&:is(.dark *)); @theme inline { diff --git a/app/layout.tsx b/app/layout.tsx index af4f367..c8efc42 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,21 +1,8 @@ import type { Metadata } from "next"; -import { Inter, Anuphan } from "next/font/google"; import "./globals.css"; import { cn } from "@/lib/utils"; import { config } from "@/lib/config"; -const inter = Inter({ - variable: "--font-inter", - subsets: ["latin"], - display: "swap", -}); - -const anuphan = Anuphan({ - variable: "--font-anuphan", - subsets: ["latin", "thai"], - display: "swap", -}); - const siteUrl = config.site.url; export const metadata: Metadata = { @@ -81,7 +68,7 @@ export default function RootLayout({ return ( diff --git a/lib/video/video-platforms.ts b/lib/video/video-platforms.ts index 0ea9713..74bbf23 100644 --- a/lib/video/video-platforms.ts +++ b/lib/video/video-platforms.ts @@ -219,7 +219,7 @@ async function publishYoutube(upload: typeof videoUploads.$inferSelect) { throw new Error(`YouTube upload initialization failed (${init.status}).`); const location = init.headers.get("location"); if (!location) throw new Error("YouTube did not return an upload URL."); - const video = await readFile(upload.videoPath); + const video = await readFile(/* turbopackIgnore: true */ upload.videoPath); const result = await fetch(location, { method: "PUT", headers: { @@ -233,7 +233,7 @@ async function publishYoutube(upload: typeof videoUploads.$inferSelect) { const data = (await result.json()) as { id?: string }; if (!data.id) throw new Error("YouTube did not return a video ID."); if (upload.thumbnailPath) { - const thumbnail = await readFile(upload.thumbnailPath); + const thumbnail = await readFile(/* turbopackIgnore: true */ upload.thumbnailPath); const thumb = await fetch( `https://www.googleapis.com/upload/youtube/v3/thumbnails/set?videoId=${encodeURIComponent(data.id)}`, { @@ -279,7 +279,7 @@ async function publishTiktok(upload: typeof videoUploads.$inferSelect) { settings.privacyLevel ?? creator.data?.privacy_level_options?.[0] ?? "SELF_ONLY"; - const video = await readFile(upload.videoPath); + const video = await readFile(/* turbopackIgnore: true */ upload.videoPath); const chunkSize = Math.min(10 * 1024 * 1024, video.byteLength); const totalChunkCount = Math.max(1, Math.floor(video.byteLength / chunkSize)); const init = await fetch( diff --git a/lib/video/video-upload-storage.ts b/lib/video/video-upload-storage.ts index a77eb16..76f4fc5 100644 --- a/lib/video/video-upload-storage.ts +++ b/lib/video/video-upload-storage.ts @@ -34,24 +34,24 @@ export async function saveVideoUpload(formData: FormData, discordId: string) { throw new Error("Invalid staged video."); } const stagingDirectory = `${VIDEO_DIR}/staging/${discordId}`; - const stagedFile = (await readdir(stagingDirectory)).find((name) => + const stagedFile = (await readdir(/* turbopackIgnore: true */ stagingDirectory)).find((name) => name.startsWith(`${stagedVideoId}-`), ); if (!stagedFile) throw new Error("Uploaded video was not found."); videoPath = `${stagingDirectory}/${stagedFile}`; originalFilename = String(formData.get("videoName") ?? stagedFile); mimeType = String(formData.get("videoMimeType") ?? ""); - videoSize = (await stat(videoPath)).size; + videoSize = (await stat(/* turbopackIgnore: true */ videoPath)).size; if (!["video/mp4", "video/quicktime", "video/webm"].includes(mimeType)) { throw new Error("Use an MP4, MOV, or WebM video."); } } else if (video instanceof File) { validateVideoFile(video); - await mkdir(VIDEO_DIR, { recursive: true }); + await mkdir(/* turbopackIgnore: true */ VIDEO_DIR, { recursive: true }); const temporaryId = randomUUID(); const safeVideoName = video.name.replace(/[^a-zA-Z0-9._-]/g, "_"); videoPath = `${VIDEO_DIR}/${temporaryId}-${safeVideoName}`; - await writeFile(videoPath, Buffer.from(await video.arrayBuffer())); + await writeFile(/* turbopackIgnore: true */ videoPath, Buffer.from(await video.arrayBuffer())); originalFilename = video.name; mimeType = video.type; videoSize = video.size; @@ -74,7 +74,7 @@ export async function saveVideoUpload(formData: FormData, discordId: string) { let thumbnailPath: string | null = null; if (thumbnail instanceof File && thumbnail.size > 0) { thumbnailPath = `${VIDEO_DIR}/${id}-thumbnail-${thumbnail.name.replace(/[^a-zA-Z0-9._-]/g, "_")}`; - await writeFile(thumbnailPath, Buffer.from(await thumbnail.arrayBuffer())); + await writeFile(/* turbopackIgnore: true */ thumbnailPath, Buffer.from(await thumbnail.arrayBuffer())); } const youtubeSettings: YoutubeSettings = JSON.parse( diff --git a/next.config.ts b/next.config.ts index a16b0b5..420bb62 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,10 +1,11 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - serverExternalPackages: ["bun"], + output: "standalone", turbopack: { - root: process.cwd(), + root: __dirname, }, + serverExternalPackages: ["bun"], allowedDevOrigins: ['test.gunshiz.top'], images: { remotePatterns: [ diff --git a/public/fonts/anuphan-latin.woff2 b/public/fonts/anuphan-latin.woff2 new file mode 100644 index 0000000..8919cba Binary files /dev/null and b/public/fonts/anuphan-latin.woff2 differ diff --git a/public/fonts/anuphan-thai.woff2 b/public/fonts/anuphan-thai.woff2 new file mode 100644 index 0000000..be99a8a Binary files /dev/null and b/public/fonts/anuphan-thai.woff2 differ diff --git a/public/fonts/inter-latin.woff2 b/public/fonts/inter-latin.woff2 new file mode 100644 index 0000000..91dc3e8 Binary files /dev/null and b/public/fonts/inter-latin.woff2 differ