From cbf9c134ad6a2ab76d584d2f5afbb11d06be6d6e Mon Sep 17 00:00:00 2001 From: julianromli Date: Fri, 19 Jun 2026 20:25:28 +0700 Subject: [PATCH 01/15] Migrate root app from Next.js to TanStack Start Replace App Router with TanStack Router/Start on Vite, move API routes into app/routes, switch i18n to react-i18next, and add server-side auth POST handlers so sign-in works without client hydration. --- .env.example | 106 +++ .gitignore | 3 + .../components/admin-management-board.tsx | 6 +- .../dashboard/boards/analytics/index.tsx | 4 +- .../boards/blog/components/post-actions.tsx | 4 +- .../boards/blog/components/post-filters.tsx | 6 +- .../boards/blog/components/posts-table.tsx | 4 +- .../comments/components/comment-preview.tsx | 4 +- .../components/pending-events-table.tsx | 2 +- .../projects/components/project-actions.tsx | 4 +- .../projects/components/project-filters.tsx | 6 +- .../projects/components/projects-table.tsx | 4 +- .../boards/users/components/user-actions.tsx | 4 +- .../boards/users/components/user-search.tsx | 6 +- .../boards/users/components/users-table.tsx | 4 +- app/(admin)/layout.tsx | 31 - app/[username]/page.tsx | 10 +- app/api/ai/completion/route.ts | 46 - app/api/ai/enhance-description/route.ts | 106 --- app/api/auth-check/route.ts | 36 - app/api/github-import/route.ts | 204 ---- app/api/og/route.tsx | 50 - app/api/uploadthing/core.ts | 3 - app/api/uploadthing/route.ts | 17 - app/api/vibe-videos/[id]/route.ts | 172 ---- app/api/vibe-videos/route.ts | 136 --- app/api/youtube/route.ts | 222 ----- app/auth/callback/route.ts | 153 --- app/blog/[slug]/blog-post-data.tsx | 10 +- app/blog/blog-page-client.tsx | 10 +- app/blog/editor/[slug]/page.tsx | 2 +- app/blog/editor/blog-editor-client.tsx | 8 +- app/blog/editor/page.tsx | 2 +- app/blog/layout.tsx | 47 - app/calendar/page.tsx | 2 +- app/dashboard/layout.tsx | 55 -- app/dashboard/posts/post-dashboard-client.tsx | 20 +- app/event/[slug]/event-detail-data.tsx | 14 +- app/event/[slug]/page.tsx | 2 +- app/event/list/layout.tsx | 47 - app/font.ts | 32 - app/globals.css | 71 ++ app/home-page-client.tsx | 20 +- app/layout.tsx | 241 ----- app/not-found.tsx | 53 -- app/page.tsx | 91 -- app/privacy-policy/privacy-policy-client.tsx | 6 +- app/project/[slug]/page.tsx | 14 +- app/project/list/layout.tsx | 47 - app/project/list/project-list-client.tsx | 41 +- app/project/list/project-list-data.tsx | 2 +- app/project/submit/page.tsx | 6 +- app/robots.ts | 10 - app/routeTree.gen.ts | 872 ++++++++++++++++++ app/router.tsx | 17 + app/routes/$username.tsx | 11 + app/routes/__root.tsx | 119 +++ app/routes/_admin/dashboard.tsx | 102 ++ app/routes/_admin/route.tsx | 36 + app/routes/admin.tsx | 25 + app/routes/api/ai.completion.ts | 45 + app/routes/api/ai.enhance-description.ts | 94 ++ app/routes/api/auth-check.ts | 44 + app/routes/api/auth.oauth.$provider.ts | 36 + app/routes/api/auth.reset-password.ts | 16 + app/routes/api/auth.sign-in.ts | 15 + app/routes/api/auth.sign-up.ts | 16 + app/routes/api/github-import.ts | 199 ++++ app/routes/api/og.ts | 22 + app/routes/api/uploadthing.ts | 38 + app/routes/api/vibe-videos.$id.ts | 152 +++ app/routes/api/vibe-videos.ts | 118 +++ app/routes/api/youtube.ts | 215 +++++ app/routes/auth.callback.ts | 149 +++ app/routes/blog.$slug.tsx | 77 ++ app/routes/blog.editor.$slug.tsx | 69 ++ app/routes/blog.editor.tsx | 55 ++ app/routes/blog.tsx | 57 ++ app/routes/calendar.tsx | 10 + app/routes/dashboard.posts.tsx | 16 + app/routes/event.$slug.tsx | 54 ++ app/routes/event.list.tsx | 17 + app/routes/index.tsx | 163 ++++ app/routes/privacy-policy.tsx | 20 + app/routes/project.$slug.tsx | 12 + app/routes/project.list.tsx | 99 ++ app/routes/project.submit.tsx | 56 ++ app/routes/robots[.]txt.ts | 20 + app/routes/sitemap[.]xml.ts | 40 + app/routes/terms-of-service.tsx | 20 + app/routes/terms.tsx | 10 + app/routes/user.auth.confirm-email.tsx | 10 + app/routes/user.auth.tsx | 23 + app/sitemap.ts | 26 - app/start.ts | 37 + .../terms-of-service-client.tsx | 6 +- app/terms/page.tsx | 10 +- app/user/auth/confirm-email/page.tsx | 8 +- app/user/auth/page.tsx | 558 +++-------- bun.lock | 530 +++++++---- components/admin-panel/nav-group.tsx | 8 +- components/admin-panel/nav-user.tsx | 8 +- components/admin-panel/team-switcher.tsx | 2 +- components/back-button.tsx | 2 +- components/blog/blog-card.tsx | 6 +- components/blog/floating-write-button.tsx | 4 +- components/command-menu.tsx | 6 +- components/errors/forbidden.tsx | 4 +- components/errors/general-error.tsx | 4 +- components/errors/not-found-error.tsx | 4 +- components/errors/unauthorized-error.tsx | 4 +- components/event/cover-image-uploader.tsx | 2 +- components/event/event-card.tsx | 16 +- components/event/submit-event-modal.tsx | 4 +- components/event/submit-event-section.tsx | 4 +- components/features-8.tsx | 18 +- components/layout/nav-group.tsx | 8 +- components/layout/nav-user.tsx | 8 +- components/layout/team-switcher.tsx | 2 +- components/logo.tsx | 2 +- components/profile/blog-tab.tsx | 4 +- components/profile/empty-state.tsx | 4 +- components/profile/project-tab.tsx | 10 +- components/project/ProjectActionsClient.tsx | 4 +- components/project/ProjectEditClient.tsx | 6 +- components/sections/ai-tools-section.tsx | 6 +- components/sections/cta-section.tsx | 8 +- components/sections/faq-section.tsx | 82 +- components/sections/hero-section.tsx | 103 +-- components/sections/project-showcase.tsx | 43 +- components/sections/reviews-section.tsx | 9 +- components/ui/adaptive-logo.tsx | 4 +- components/ui/animated-tooltip.tsx | 2 +- components/ui/comment-section.tsx | 4 +- components/ui/footer.tsx | 10 +- components/ui/integration-card.tsx | 4 +- components/ui/language-switcher.tsx | 2 +- components/ui/navbar.tsx | 91 +- components/ui/optimized-avatar.tsx | 2 +- components/ui/progressive-hero-image.tsx | 9 +- components/ui/progressive-image.tsx | 97 +- components/ui/project-image-carousel.tsx | 10 +- components/ui/submit-project-form.tsx | 4 +- .../steps/links-media-step.tsx | 2 +- components/ui/testimonials-columns.tsx | 2 +- components/ui/tools-columns.tsx | 4 +- components/ui/video-vibe-coding-manager.tsx | 2 +- components/ui/youtube-video-showcase.tsx | 57 +- hooks/use-locale.ts | 8 + hooks/useAuth.ts | 2 +- hooks/useProjectFilters.ts | 4 +- i18n/index.ts | 48 + i18n/request.ts | 24 - i18n/routing.ts | 10 +- lib/actions.ts | 34 +- lib/actions/admin/admins.ts | 4 +- lib/actions/admin/comments.ts | 4 +- lib/actions/admin/posts.ts | 4 +- lib/actions/admin/projects.ts | 4 +- lib/actions/admin/users.ts | 4 +- lib/actions/analytics.ts | 2 - lib/actions/blog.ts | 4 +- lib/actions/comments.ts | 4 +- lib/actions/events.ts | 4 +- lib/actions/projects.ts | 4 +- lib/actions/user.ts | 4 +- lib/auth/credentials.ts | 204 ++++ lib/auth/redirects.ts | 38 + lib/env-config.ts | 18 +- lib/i18n-server.ts | 59 ++ lib/image-types.ts | 48 + lib/image-utils.ts | 4 +- lib/navigation.ts | 36 + lib/revalidation.ts | 13 + lib/routes/helpers.ts | 21 + lib/server/blog-public.ts | 2 +- lib/server/events-public.ts | 2 +- lib/server/request-middleware.ts | 210 +++++ lib/slug.ts | 41 +- lib/supabase/middleware.ts | 46 - lib/supabase/server.ts | 16 +- lib/uploadthing.ts | 15 +- next.config.mjs | 260 ------ package.json | 28 +- postcss.config.mjs | 8 - proxy.ts | 196 ---- scripts/migrate-next-imports.mjs | 255 +++++ tsconfig.json | 12 +- vite-env.d.ts | 13 + vite.config.ts | 26 + 190 files changed, 5387 insertions(+), 3607 deletions(-) create mode 100644 .env.example delete mode 100644 app/(admin)/layout.tsx delete mode 100644 app/api/ai/completion/route.ts delete mode 100644 app/api/ai/enhance-description/route.ts delete mode 100644 app/api/auth-check/route.ts delete mode 100644 app/api/github-import/route.ts delete mode 100644 app/api/og/route.tsx delete mode 100644 app/api/uploadthing/core.ts delete mode 100644 app/api/uploadthing/route.ts delete mode 100644 app/api/vibe-videos/[id]/route.ts delete mode 100644 app/api/vibe-videos/route.ts delete mode 100644 app/api/youtube/route.ts delete mode 100644 app/auth/callback/route.ts delete mode 100644 app/blog/layout.tsx delete mode 100644 app/dashboard/layout.tsx delete mode 100644 app/event/list/layout.tsx delete mode 100644 app/font.ts delete mode 100644 app/layout.tsx delete mode 100644 app/not-found.tsx delete mode 100644 app/page.tsx delete mode 100644 app/project/list/layout.tsx delete mode 100644 app/robots.ts create mode 100644 app/routeTree.gen.ts create mode 100644 app/router.tsx create mode 100644 app/routes/$username.tsx create mode 100644 app/routes/__root.tsx create mode 100644 app/routes/_admin/dashboard.tsx create mode 100644 app/routes/_admin/route.tsx create mode 100644 app/routes/admin.tsx create mode 100644 app/routes/api/ai.completion.ts create mode 100644 app/routes/api/ai.enhance-description.ts create mode 100644 app/routes/api/auth-check.ts create mode 100644 app/routes/api/auth.oauth.$provider.ts create mode 100644 app/routes/api/auth.reset-password.ts create mode 100644 app/routes/api/auth.sign-in.ts create mode 100644 app/routes/api/auth.sign-up.ts create mode 100644 app/routes/api/github-import.ts create mode 100644 app/routes/api/og.ts create mode 100644 app/routes/api/uploadthing.ts create mode 100644 app/routes/api/vibe-videos.$id.ts create mode 100644 app/routes/api/vibe-videos.ts create mode 100644 app/routes/api/youtube.ts create mode 100644 app/routes/auth.callback.ts create mode 100644 app/routes/blog.$slug.tsx create mode 100644 app/routes/blog.editor.$slug.tsx create mode 100644 app/routes/blog.editor.tsx create mode 100644 app/routes/blog.tsx create mode 100644 app/routes/calendar.tsx create mode 100644 app/routes/dashboard.posts.tsx create mode 100644 app/routes/event.$slug.tsx create mode 100644 app/routes/event.list.tsx create mode 100644 app/routes/index.tsx create mode 100644 app/routes/privacy-policy.tsx create mode 100644 app/routes/project.$slug.tsx create mode 100644 app/routes/project.list.tsx create mode 100644 app/routes/project.submit.tsx create mode 100644 app/routes/robots[.]txt.ts create mode 100644 app/routes/sitemap[.]xml.ts create mode 100644 app/routes/terms-of-service.tsx create mode 100644 app/routes/terms.tsx create mode 100644 app/routes/user.auth.confirm-email.tsx create mode 100644 app/routes/user.auth.tsx delete mode 100644 app/sitemap.ts create mode 100644 app/start.ts create mode 100644 hooks/use-locale.ts create mode 100644 i18n/index.ts delete mode 100644 i18n/request.ts create mode 100644 lib/auth/credentials.ts create mode 100644 lib/auth/redirects.ts create mode 100644 lib/i18n-server.ts create mode 100644 lib/image-types.ts create mode 100644 lib/navigation.ts create mode 100644 lib/revalidation.ts create mode 100644 lib/routes/helpers.ts create mode 100644 lib/server/request-middleware.ts delete mode 100644 lib/supabase/middleware.ts delete mode 100644 next.config.mjs delete mode 100644 postcss.config.mjs delete mode 100644 proxy.ts create mode 100644 scripts/migrate-next-imports.mjs create mode 100644 vite-env.d.ts create mode 100644 vite.config.ts diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..8d635373 --- /dev/null +++ b/.env.example @@ -0,0 +1,106 @@ +# ============================================================================= +# VibeDev ID — Environment Variables (TanStack Start + Vite + Nitro) +# ============================================================================= +# +# Copy this file to `.env.local` for local development: +# cp .env.example .env.local +# +# Vite loads `.env`, `.env.local`, and `.env.[mode]` automatically. +# Restart `bun run dev` after changing values. +# +# Public vs server-only: +# - `NEXT_PUBLIC_*` — read on server; also used by legacy helpers +# - `VITE_*` — exposed to browser (preferred for client-side Supabase) +# - no prefix — server-only (never sent to browser) +# +# For Supabase public keys, set BOTH `NEXT_PUBLIC_*` and matching `VITE_*` +# (same values) so SSR loaders and client components both work. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Required — Supabase (database + auth) +# Get from: Supabase Dashboard → Project Settings → API +# ----------------------------------------------------------------------------- + +# Project URL (must start with https://) +NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co +VITE_SUPABASE_URL=https://your-project-ref.supabase.co + +# anon / public key (safe for browser with RLS enabled) +NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key +VITE_SUPABASE_ANON_KEY=your_supabase_anon_key + +# service role key — SERVER ONLY, bypasses RLS (admin actions, migrations) +# Never expose to client or commit real values to git +SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key + +# ----------------------------------------------------------------------------- +# Required — Site URL (SEO, auth redirects, OAuth callbacks) +# ----------------------------------------------------------------------------- + +# Canonical public URL of this deployment +NEXT_PUBLIC_SITE_URL=http://localhost:3000 + +# Optional alias (used by getSiteUrl() fallback chain) +# SITE_URL=http://localhost:3000 + +# Where Supabase should redirect after email signup / password reset in dev +# Must be listed in Supabase → Authentication → URL Configuration → Redirect URLs +NEXT_PUBLIC_DEV_SUPABASE_REDIRECT_URL=http://localhost:3000 + +# Supabase auth callback (add this URL in Supabase redirect allowlist too): +# http://localhost:3000/auth/callback + +# ----------------------------------------------------------------------------- +# File uploads — UploadThing +# Get from: https://uploadthing.com/dashboard +# Required for project/blog image uploads; app returns 503 without it +# ----------------------------------------------------------------------------- + +UPLOADTHING_TOKEN=your_uploadthing_token + +# Optional legacy UploadThing app ID (if your dashboard still shows it) +# UPLOADTHING_APP_ID=your_uploadthing_app_id + +# ----------------------------------------------------------------------------- +# AI features — OpenRouter +# Used by /api/ai/completion and /api/ai/enhance-description (blog editor) +# Get from: https://openrouter.ai/keys +# ----------------------------------------------------------------------------- + +OPENROUTER_API_KEY=sk-or-v1-your_openrouter_api_key + +# ----------------------------------------------------------------------------- +# Optional — GitHub import +# Used by /api/github-import for higher rate limits when importing repos +# Create at: GitHub → Settings → Developer settings → Personal access tokens +# ----------------------------------------------------------------------------- + +# GITHUB_TOKEN=ghp_your_github_personal_access_token +# GH_TOKEN=ghp_your_github_personal_access_token + +# ----------------------------------------------------------------------------- +# Optional — Vercel Analytics / Speed Insights +# Set on Vercel automatically; only needed if you want insights locally +# ----------------------------------------------------------------------------- + +# VITE_VERCEL=1 +# VERCEL=1 + +# ----------------------------------------------------------------------------- +# Optional — Production / hosting (usually auto-set by platform) +# Do not set manually unless you know what you're doing +# ----------------------------------------------------------------------------- + +# VERCEL_URL=your-app.vercel.app +# VERCEL_PROJECT_PRODUCTION_URL=https://www.vibedevid.com + +# Nitro production server (default: 3000) +# PORT=3000 +# HOST=0.0.0.0 + +# ----------------------------------------------------------------------------- +# CI / testing (set by CI runner, not needed locally) +# ----------------------------------------------------------------------------- + +# CI=true diff --git a/.gitignore b/.gitignore index 4688f290..c9d7ad11 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ # production /build +.output/ +.tanstack/ # debug npm-debug.log* @@ -18,6 +20,7 @@ yarn-error.log* # env files .env* +!.env.example # vercel .vercel diff --git a/app/(admin)/dashboard/boards/admin-management/components/admin-management-board.tsx b/app/(admin)/dashboard/boards/admin-management/components/admin-management-board.tsx index 1bfa15a8..15769074 100644 --- a/app/(admin)/dashboard/boards/admin-management/components/admin-management-board.tsx +++ b/app/(admin)/dashboard/boards/admin-management/components/admin-management-board.tsx @@ -1,8 +1,8 @@ 'use client' import { IconSearch, IconShield, IconShieldOff, IconUserPlus } from '@tabler/icons-react' -import Link from 'next/link' -import { useRouter } from 'next/navigation' +import { Link } from '@tanstack/react-router' +import { useRouter } from '@/lib/navigation' import { useCallback, useState } from 'react' import { toast } from 'sonner' import { @@ -292,7 +292,7 @@ export function AdminManagementBoard({ initialUsers, adminCount, moderatorCount
diff --git a/app/(admin)/dashboard/boards/analytics/index.tsx b/app/(admin)/dashboard/boards/analytics/index.tsx index 3981c17c..118ed674 100644 --- a/app/(admin)/dashboard/boards/analytics/index.tsx +++ b/app/(admin)/dashboard/boards/analytics/index.tsx @@ -9,7 +9,7 @@ import { IconStar, IconUsers, } from '@tabler/icons-react' -import Link from 'next/link' +import { Link } from '@tanstack/react-router' import { type ElementType, type ReactNode, useEffect, useState } from 'react' import { Bar, BarChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' @@ -515,7 +515,7 @@ function HealthCard({ highlight?: boolean }) { return ( - + diff --git a/app/(admin)/dashboard/boards/blog/components/post-filters.tsx b/app/(admin)/dashboard/boards/blog/components/post-filters.tsx index 0f7907a8..eb1cc63a 100644 --- a/app/(admin)/dashboard/boards/blog/components/post-filters.tsx +++ b/app/(admin)/dashboard/boards/blog/components/post-filters.tsx @@ -1,7 +1,7 @@ 'use client' import { IconFilter, IconSearch } from '@tabler/icons-react' -import { useRouter, useSearchParams } from 'next/navigation' +import { useRouter, useSearchParams } from '@/lib/navigation' import { useState } from 'react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -29,13 +29,13 @@ export function PostFilters() { params.delete('page') - router.push(`?${params.toString()}`) + router.navigate({ to: `?${params.toString()}` }) } const clearFilters = () => { setSearch('') setStatus('all') - router.push(buildDashboardBoardClearHref(BOARD_TAB)) + router.navigate({ to: buildDashboardBoardClearHref(BOARD_TAB) }) } const hasFilters = search || status !== 'all' diff --git a/app/(admin)/dashboard/boards/blog/components/posts-table.tsx b/app/(admin)/dashboard/boards/blog/components/posts-table.tsx index 410c04f5..575c911d 100644 --- a/app/(admin)/dashboard/boards/blog/components/posts-table.tsx +++ b/app/(admin)/dashboard/boards/blog/components/posts-table.tsx @@ -1,7 +1,7 @@ 'use client' import { IconClock, IconEye } from '@tabler/icons-react' -import Link from 'next/link' +import { Link } from '@tanstack/react-router' import { useState } from 'react' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' @@ -70,7 +70,7 @@ export function PostsTable({ posts, totalCount, currentPage }: PostsTableProps)
{post.title} diff --git a/app/(admin)/dashboard/boards/comments/components/comment-preview.tsx b/app/(admin)/dashboard/boards/comments/components/comment-preview.tsx index ffc420df..163afbc1 100644 --- a/app/(admin)/dashboard/boards/comments/components/comment-preview.tsx +++ b/app/(admin)/dashboard/boards/comments/components/comment-preview.tsx @@ -1,7 +1,7 @@ 'use client' import { IconFileText, IconMessageCircle, IconTrash, IconX } from '@tabler/icons-react' -import Link from 'next/link' +import { Link } from '@tanstack/react-router' import { toast } from 'sonner' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' @@ -90,7 +90,7 @@ export function CommentPreview({ report, open, onOpenChange }: CommentPreviewPro
diff --git a/app/(admin)/dashboard/boards/events-approval/components/pending-events-table.tsx b/app/(admin)/dashboard/boards/events-approval/components/pending-events-table.tsx index ecfe174f..5156010c 100644 --- a/app/(admin)/dashboard/boards/events-approval/components/pending-events-table.tsx +++ b/app/(admin)/dashboard/boards/events-approval/components/pending-events-table.tsx @@ -2,7 +2,7 @@ import { IconCalendarEvent } from '@tabler/icons-react' import { type ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table' -import { useRouter } from 'next/navigation' +import { useRouter } from '@/lib/navigation' import { useState } from 'react' import { toast } from 'sonner' import { Badge } from '@/components/ui/badge' diff --git a/app/(admin)/dashboard/boards/projects/components/project-actions.tsx b/app/(admin)/dashboard/boards/projects/components/project-actions.tsx index 5234e4d7..67901763 100644 --- a/app/(admin)/dashboard/boards/projects/components/project-actions.tsx +++ b/app/(admin)/dashboard/boards/projects/components/project-actions.tsx @@ -1,7 +1,7 @@ 'use client' import { IconDotsVertical, IconEdit, IconExternalLink, IconStar, IconStarOff, IconTrash } from '@tabler/icons-react' -import Link from 'next/link' +import { Link } from '@tanstack/react-router' import { useState } from 'react' import { toast } from 'sonner' import { @@ -83,7 +83,7 @@ export function ProjectActions({ project, onEdit }: ProjectActionsProps) { diff --git a/app/(admin)/dashboard/boards/projects/components/project-filters.tsx b/app/(admin)/dashboard/boards/projects/components/project-filters.tsx index 561ed4de..601b61e9 100644 --- a/app/(admin)/dashboard/boards/projects/components/project-filters.tsx +++ b/app/(admin)/dashboard/boards/projects/components/project-filters.tsx @@ -1,7 +1,7 @@ 'use client' import { IconFilter, IconSearch } from '@tabler/icons-react' -import { useRouter, useSearchParams } from 'next/navigation' +import { useRouter, useSearchParams } from '@/lib/navigation' import { useState } from 'react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -37,14 +37,14 @@ export function ProjectFilters({ categories }: ProjectFiltersProps) { params.delete('page') // Reset to first page on filter change - router.push(`?${params.toString()}`) + router.navigate({ to: `?${params.toString()}` }) } const clearFilters = () => { setSearch('') setStatus('all') setCategory('all') - router.push(buildDashboardBoardClearHref(BOARD_TAB)) + router.navigate({ to: buildDashboardBoardClearHref(BOARD_TAB) }) } const hasFilters = search || status !== 'all' || category !== 'all' diff --git a/app/(admin)/dashboard/boards/projects/components/projects-table.tsx b/app/(admin)/dashboard/boards/projects/components/projects-table.tsx index 7e8808d2..16f2c486 100644 --- a/app/(admin)/dashboard/boards/projects/components/projects-table.tsx +++ b/app/(admin)/dashboard/boards/projects/components/projects-table.tsx @@ -1,7 +1,7 @@ 'use client' import { IconEye, IconHeart, IconMessageCircle } from '@tabler/icons-react' -import Link from 'next/link' +import { Link } from '@tanstack/react-router' import { useState } from 'react' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' @@ -57,7 +57,7 @@ export function ProjectsTable({ projects, totalCount, currentPage }: ProjectsTab
{project.title} diff --git a/app/(admin)/dashboard/boards/users/components/user-actions.tsx b/app/(admin)/dashboard/boards/users/components/user-actions.tsx index baf00ae9..c337145f 100644 --- a/app/(admin)/dashboard/boards/users/components/user-actions.tsx +++ b/app/(admin)/dashboard/boards/users/components/user-actions.tsx @@ -1,7 +1,7 @@ 'use client' import { IconBan, IconChartBar, IconDotsVertical, IconEdit, IconShield, IconUserCheck } from '@tabler/icons-react' -import Link from 'next/link' +import { Link } from '@tanstack/react-router' import { Button } from '@/components/ui/button' import { DropdownMenu, @@ -33,7 +33,7 @@ export function UserActions({ user, onEditRole, onSuspend, onViewStats }: UserAc diff --git a/app/(admin)/dashboard/boards/users/components/user-search.tsx b/app/(admin)/dashboard/boards/users/components/user-search.tsx index af7e6d19..7804bec7 100644 --- a/app/(admin)/dashboard/boards/users/components/user-search.tsx +++ b/app/(admin)/dashboard/boards/users/components/user-search.tsx @@ -1,7 +1,7 @@ 'use client' import { IconFilter, IconSearch } from '@tabler/icons-react' -import { useRouter, useSearchParams } from 'next/navigation' +import { useRouter, useSearchParams } from '@/lib/navigation' import { useEffect, useState } from 'react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -40,14 +40,14 @@ export function UserSearch() { params.delete('page') - router.push(`?${params.toString()}`) + router.navigate({ to: `?${params.toString()}` }) } const clearFilters = () => { setSearch('') setRole('all') setStatus('all') - router.push(buildDashboardBoardClearHref(BOARD_TAB)) + router.navigate({ to: buildDashboardBoardClearHref(BOARD_TAB) }) } const hasFilters = search || role !== 'all' || status !== 'all' diff --git a/app/(admin)/dashboard/boards/users/components/users-table.tsx b/app/(admin)/dashboard/boards/users/components/users-table.tsx index 5fd22238..9272ce58 100644 --- a/app/(admin)/dashboard/boards/users/components/users-table.tsx +++ b/app/(admin)/dashboard/boards/users/components/users-table.tsx @@ -1,7 +1,7 @@ 'use client' import { IconFolder, IconHeart, IconMessageCircle, IconNews } from '@tabler/icons-react' -import Link from 'next/link' +import { Link } from '@tanstack/react-router' import { useState } from 'react' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' @@ -73,7 +73,7 @@ export function UsersTable({ users, totalCount, currentPage }: UsersTableProps)
{user.display_name} diff --git a/app/(admin)/layout.tsx b/app/(admin)/layout.tsx deleted file mode 100644 index 4c203626..00000000 --- a/app/(admin)/layout.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { redirect } from 'next/navigation' -import { getCurrentUser } from '@/lib/actions/user' -import DashboardLayoutClient from './layout-client' - -export const dynamic = 'force-dynamic' - -interface Props { - children: React.ReactNode -} - -// Role constants - Admin is role 0 -const ROLES = { - ADMIN: 0, - MODERATOR: 1, - USER: 2, -} as const - -export default async function DashboardLayout({ children }: Props) { - const { user, error } = await getCurrentUser() - - if (error || !user) { - redirect('/user/auth') - } - - // CRITICAL-3: Verify user has admin role before granting access - if (user.role !== ROLES.ADMIN) { - redirect('/') - } - - return {children} -} diff --git a/app/[username]/page.tsx b/app/[username]/page.tsx index 2ba73f17..17ca569c 100644 --- a/app/[username]/page.tsx +++ b/app/[username]/page.tsx @@ -1,9 +1,9 @@ 'use client' import { format } from 'date-fns' import { ArrowLeft, FilePenLine, FileText, FolderOpen, LayoutGrid, User } from 'lucide-react' -import Image from 'next/image' -import Link from 'next/link' -import { useParams, useRouter } from 'next/navigation' +import { Image } from '@unpic/react' +import { Link } from '@tanstack/react-router' +import { useParams, useRouter } from '@tanstack/react-router' import { useEffect, useState } from 'react' import { toast } from 'sonner' import { BlogTab } from '@/components/profile/blog-tab' @@ -433,7 +433,7 @@ export default function ProfilePage() { toast.success('Profile updated successfully') if (result.usernameChanged && result.newUsername) { - router.push(`/${result.newUsername}`) + router.navigate({ to: `/${result.newUsername}` }) } } else { toast.error(result.error || 'Failed to update profile') @@ -486,7 +486,7 @@ export default function ProfilePage() {

User Not Found

The profile you're looking for doesn't exist.

- diff --git a/app/api/ai/completion/route.ts b/app/api/ai/completion/route.ts deleted file mode 100644 index 330c0af1..00000000 --- a/app/api/ai/completion/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { streamText } from 'ai' -import { getAIModel } from '@/lib/ai/openrouter' - -export const runtime = 'edge' - -export async function POST(req: Request) { - try { - const body = await req.json() - const { prompt } = body - - if (!prompt || typeof prompt !== 'string') { - return new Response(JSON.stringify({ error: 'Invalid prompt' }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }) - } - - const result = streamText({ - model: getAIModel(), - messages: [ - { - role: 'system', - content: `You are an AI writing assistant that continues existing text based on context. -Give more weight to the later characters than the beginning ones. -Limit your response to no more than 200 characters. -Construct complete sentences. -Use Markdown formatting when appropriate.`, - }, - { - role: 'user', - content: prompt, - }, - ], - temperature: 0.7, - maxOutputTokens: 200, - }) - - return result.toTextStreamResponse() - } catch (error) { - console.error('AI completion error:', error) - return new Response(JSON.stringify({ error: 'Failed to generate completion' }), { - status: 500, - headers: { 'Content-Type': 'application/json' }, - }) - } -} diff --git a/app/api/ai/enhance-description/route.ts b/app/api/ai/enhance-description/route.ts deleted file mode 100644 index 40ff228f..00000000 --- a/app/api/ai/enhance-description/route.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { generateText } from 'ai' -import { getAIModel } from '@/lib/ai/openrouter' - -export const runtime = 'edge' - -interface EnhanceRequest { - description: string - title?: string - tagline?: string - tags?: string[] -} - -const SYSTEM_PROMPT = `Kamu adalah AI writer untuk VibeDev Indonesia community - komunitas developer Indonesia. - -TUGAS: -- Jika diberikan description mentah: Rapikan dan enhance menjadi description yang profesional -- Jika description kosong tapi ada title/tags: Generate description baru berdasarkan context - -ATURAN PENULISAN: -1. Gunakan bahasa Indonesia yang santai tapi profesional (boleh campur bahasa Inggris untuk istilah teknis) -2. Struktur yang jelas dan mudah dibaca -3. Highlight fitur utama dengan bullet points jika ada banyak fitur -4. JANGAN tambahkan informasi yang tidak ada di input (jangan mengarang) -5. JANGAN gunakan markdown headers (# atau ##) -6. Boleh gunakan emoji secukupnya untuk highlight (🚀 💻 ✨ dll) -7. Maksimal 4000 karakter -8. Langsung tulis description-nya saja, tanpa pembuka seperti "Berikut adalah..." atau "Ini adalah..." - -FORMAT OUTPUT (contoh): -[Nama Project] adalah [deskripsi singkat apa ini]. - -🚀 Fitur Utama: -• [Fitur 1] -• [Fitur 2] -• [Fitur 3] - -💻 Tech Stack: -[List teknologi yang digunakan] - -[Closing statement singkat - untuk siapa project ini cocok]` - -export async function POST(req: Request) { - try { - const body: EnhanceRequest = await req.json() - const { description, title, tagline, tags } = body - - // Validate: minimal harus ada title atau description - if (!description?.trim() && !title?.trim()) { - return new Response(JSON.stringify({ error: 'Minimal harus ada title atau description' }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }) - } - - // Build context untuk AI - const contextParts: string[] = [] - if (title?.trim()) contextParts.push(`Title: ${title.trim()}`) - if (tagline?.trim()) contextParts.push(`Tagline: ${tagline.trim()}`) - if (tags?.length) contextParts.push(`Tech Stack/Tags: ${tags.join(', ')}`) - - // Build user prompt - let userPrompt: string - if (description?.trim()) { - // Mode: Enhance existing description - userPrompt = `${contextParts.join('\n')} - -Description (tolong rapikan dan enhance): -${description.trim()}` - } else { - // Mode: Generate new description from context - userPrompt = `${contextParts.join('\n')} - -Tolong buatkan description yang menarik untuk project ini berdasarkan informasi di atas.` - } - - const result = await generateText({ - model: getAIModel(), - messages: [ - { role: 'system', content: SYSTEM_PROMPT }, - { role: 'user', content: userPrompt }, - ], - temperature: 0.7, - maxOutputTokens: 2500, // ~4000+ karakter untuk bahasa Indonesia - }) - - const enhancedDescription = result.text?.trim() || '' - - if (!enhancedDescription) { - return new Response(JSON.stringify({ error: 'AI tidak menghasilkan output' }), { - status: 500, - headers: { 'Content-Type': 'application/json' }, - }) - } - - return new Response(JSON.stringify({ description: enhancedDescription }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }) - } catch (error) { - console.error('AI enhance description error:', error) - return new Response(JSON.stringify({ error: 'Gagal generate description. Coba lagi.' }), { - status: 500, - headers: { 'Content-Type': 'application/json' }, - }) - } -} diff --git a/app/api/auth-check/route.ts b/app/api/auth-check/route.ts deleted file mode 100644 index c5cdf0cc..00000000 --- a/app/api/auth-check/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createServerClient } from '@supabase/ssr' -import { cookies } from 'next/headers' -import { NextResponse } from 'next/server' - -export async function GET(request: Request) { - const cookieStore = await cookies() - const { searchParams } = new URL(request.url) - const redirectTo = searchParams.get('redirectTo') || '/blog/editor' - - const supabase = createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - { - cookies: { - getAll() { - return cookieStore.getAll() - }, - setAll(cookiesToSet) { - cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options)) - }, - }, - }, - ) - - const { - data: { user }, - } = await supabase.auth.getUser() - - if (!user) { - // User not authenticated, redirect to login - return NextResponse.redirect(new URL(`/user/auth?redirectTo=${encodeURIComponent(redirectTo)}`, request.url)) - } - - // User is authenticated, redirect to the requested page - return NextResponse.redirect(new URL(redirectTo, request.url)) -} diff --git a/app/api/github-import/route.ts b/app/api/github-import/route.ts deleted file mode 100644 index 15dfd74e..00000000 --- a/app/api/github-import/route.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { NextResponse } from 'next/server' - -type GitHubRepo = { - name: string - full_name: string - description: string | null - homepage: string | null - html_url: string - owner?: { - login?: string | null - } | null -} - -function parseRepoUrl(input: string): { owner: string; repo: string } | null { - if (!input) return null - let s = input.trim() - // Support formats: owner/repo, https://github.com/owner/repo, git@github.com:owner/repo.git - if (s.startsWith('git@github.com:')) { - s = s.replace('git@github.com:', '') - } - try { - if (s.startsWith('http')) { - const u = new URL(s) - if (!u.hostname.endsWith('github.com')) return null - const parts = u.pathname - .replace(/^\/+|\.git$/g, '') - .split('/') - .filter(Boolean) - if (parts.length < 2) return null - return { owner: parts[0], repo: parts[1] } - } - } catch { - // fallthrough to owner/repo parse - } - const parts = s - .replace(/^\/+|\.git$/g, '') - .split('/') - .filter(Boolean) - if (parts.length === 2) return { owner: parts[0], repo: parts[1] } - return null -} - -function normalizeText(value: string | null | undefined): string { - return typeof value === 'string' ? value.trim() : '' -} - -function buildOgImage(owner: string, repo: string): string { - // Public Open Graph image endpoint (seed value can be any string) - return `https://opengraph.githubassets.com/1/${owner}/${repo}` -} - -function getDomainFromUrl(url?: string | null): string | null { - if (!url) return null - try { - const u = new URL(url.startsWith('http') ? url : `https://${url}`) - return u.hostname - } catch { - return null - } -} - -async function fetchReadmeSummary( - owner: string, - repo: string, - headers: Record, -): Promise<{ - description?: string - tagline?: string -}> { - try { - const readmeRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/readme`, { - headers: { - ...headers, - // Ask for raw content - Accept: 'application/vnd.github.v3.raw', - }, - }) - if (!readmeRes.ok) return {} - const md = await readmeRes.text() - - // Remove code blocks - let txt = md.replace(/```[\s\S]*?```/g, ' ') - // Remove images - txt = txt.replace(/!\[[^\]]*\]\([^)]*\)/g, ' ') - // Convert links [text](url) -> text - txt = txt.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') - // Strip HTML tags - txt = txt.replace(/<[^>]+>/g, ' ') - // Strip headings markup and badges lines - const lines = txt - .split(/\r?\n/) - .map((l) => l.replace(/^\s*#+\s*/, '').trim()) - .filter((l) => l && !/shields\.io|badge|ci|build status/i.test(l)) - const paragraphs = lines - .join('\n') - .split(/\n\s*\n+/) - .map((p) => p.trim()) - - let best = '' - for (const p of paragraphs) { - const plain = p.replace(/\s+/g, ' ').trim() - if (plain.length < 60) continue - if (/^license\b|^changelog\b|^installation\b/i.test(plain)) continue - best = plain - break - } - if (!best) return {} - - // Build description and tagline from the first meaningful paragraph - const description = best.slice(0, 1600) - const sentenceEnd = - description.indexOf('.') !== -1 ? description.indexOf('.') + 1 : Math.min(100, description.length) - const tagline = description.slice(0, sentenceEnd).trim() - return { description, tagline } - } catch { - return {} - } -} - -// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: One route handles fetch + normalization for a single import contract. -export async function POST(req: Request) { - try { - const { repoUrl } = await req.json() - const parsed = parseRepoUrl(String(repoUrl || '')) - if (!parsed) { - return NextResponse.json({ error: 'Invalid GitHub repository URL' }, { status: 400 }) - } - - const { owner, repo } = parsed - const headers: Record = { - Accept: 'application/vnd.github+json', - } - const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN - if (token) headers.Authorization = `Bearer ${token}` - - const repoRes = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers }) - if (!repoRes.ok) { - const msg = repoRes.status === 404 ? 'Repository not found' : 'Failed to fetch repository' - return NextResponse.json({ error: msg }, { status: repoRes.status }) - } - const repoData = (await repoRes.json()) as GitHubRepo - - const topicsRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/topics`, { - headers: { ...headers, Accept: 'application/vnd.github+json' }, - }) - const topicsJson = topicsRes.ok ? await topicsRes.json() : { names: [] as string[] } - const topics: string[] = Array.isArray(topicsJson.names) ? topicsJson.names : [] - - const langsRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/languages`, { headers }) - const langsJson = langsRes.ok ? await langsRes.json() : ({} as Record) - const languages = Object.keys(langsJson || {}) - - const title = normalizeText(repoData.name).replace(/[-_]+/g, ' ') || repo - const readmeSummary = await fetchReadmeSummary(owner, repo, headers) - const repoDescription = normalizeText(repoData.description) - const description = (normalizeText(readmeSummary.description) || repoDescription || title).slice(0, 1600) - const tagline = (normalizeText(readmeSummary.tagline) || repoDescription || title).slice(0, 120) - const website_url = normalizeText(repoData.homepage) - const preview_image_url = buildOgImage(owner, repo) - const image_url = preview_image_url - const domain = getDomainFromUrl(website_url) || 'github.com' - const favicon_url = `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=32` - - // Normalize tags from topics + languages - const tagSet = new Set() - for (const t of topics) if (t) tagSet.add(String(t).toLowerCase()) - for (const l of languages) if (l) tagSet.add(String(l).toLowerCase()) - // Common mappings - const normalized = Array.from( - new Set( - Array.from(tagSet) - .map((t) => { - if (t === 'js') return 'javascript' - if (t === 'ts') return 'typescript' - if (t === 'node') return 'nodejs' - if (t === 'next') return 'next.js' - return t - }) - .filter(Boolean), - ), - ) - - const repoMetadata = { - name: normalizeText(repoData.name) || repo, - full_name: normalizeText(repoData.full_name) || `${owner}/${repo}`, - html_url: normalizeText(repoData.html_url) || `https://github.com/${owner}/${repo}`, - owner: normalizeText(repoData.owner?.login) || owner, - } - - return NextResponse.json({ - title, - tagline, - description, - website_url, - preview_image_url, - image_url, - favicon_url, - tags: normalized, - repo: repoMetadata, - }) - } catch (_e) { - return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }) - } -} diff --git a/app/api/og/route.tsx b/app/api/og/route.tsx deleted file mode 100644 index b832181a..00000000 --- a/app/api/og/route.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { ImageResponse } from 'next/og' -import type { NextRequest } from 'next/server' - -import { siteConfig } from '@/config/site' - -export const runtime = 'edge' - -const Geist = fetch(new URL('../../../public/fonts/Geist-Regular.ttf', import.meta.url)).then((response) => - response.arrayBuffer(), -) - -export async function GET(request: NextRequest) { - try { - const fontSans = await Geist - - const { searchParams } = request.nextUrl - const title = searchParams.get('title') - - if (!title) { - return new Response('Missing title', { status: 400 }) - } - - const heading = title.length > 140 ? title.slice(0, 140) + '...' : title - return new ImageResponse( -
- Hello World -
, - ) - } catch (error) { - console.log('Failed to generate image:', error) - return new Response('Failed to generate the image', { status: 500 }) - } -} diff --git a/app/api/uploadthing/core.ts b/app/api/uploadthing/core.ts deleted file mode 100644 index c0318d7d..00000000 --- a/app/api/uploadthing/core.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ourFileRouter } from '@/lib/uploadthing' - -export { ourFileRouter as fileRouter } diff --git a/app/api/uploadthing/route.ts b/app/api/uploadthing/route.ts deleted file mode 100644 index 3b67d2e5..00000000 --- a/app/api/uploadthing/route.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createRouteHandler } from 'uploadthing/next' -import { ourFileRouter } from '@/lib/uploadthing' - -const uploadthingToken = process.env.UPLOADTHING_TOKEN?.trim() - -// Log only the existence of token for debugging, not the value -if (!uploadthingToken) { - console.error('[UploadThing] Missing required environment variable: UPLOADTHING_TOKEN') - throw new Error('UPLOADTHING_TOKEN environment variable is required') -} - -export const { GET, POST } = createRouteHandler({ - router: ourFileRouter, - config: { - token: uploadthingToken, - }, -}) diff --git a/app/api/vibe-videos/[id]/route.ts b/app/api/vibe-videos/[id]/route.ts deleted file mode 100644 index b3f37030..00000000 --- a/app/api/vibe-videos/[id]/route.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { type NextRequest, NextResponse } from 'next/server' -import { createAdminClient } from '@/lib/supabase/admin' - -interface RouteParams { - params: Promise<{ - id: string - }> -} - -// GET - Get single vibe video by ID -export async function GET(request: NextRequest, { params }: RouteParams) { - try { - const { id } = await params - - if (!id) { - return NextResponse.json({ error: 'Video ID diperlukan' }, { status: 400 }) - } - - const supabase = createAdminClient() - - const { data: video, error } = await supabase.from('vibe_videos').select('*').eq('id', id).single() - - if (error) { - if (error.code === 'PGRST116') { - // No rows found - return NextResponse.json({ error: 'Video tidak ditemukan' }, { status: 404 }) - } - - console.error('Database error:', error) - return NextResponse.json({ error: 'Gagal mengambil data video' }, { status: 500 }) - } - - // Transform data untuk compatibility dengan frontend - const transformedVideo = { - id: video.id, - title: video.title, - description: video.description, - thumbnail: video.thumbnail, - videoId: video.video_id, - publishedAt: video.published_at, - viewCount: video.view_count, - position: video.position, - createdAt: video.created_at, - updatedAt: video.updated_at, - } - - return NextResponse.json({ video: transformedVideo }) - } catch (error) { - console.error('API Error:', error) - return NextResponse.json({ error: 'Terjadi error saat mengambil data video cuy!' }, { status: 500 }) - } -} - -// PUT - Update vibe video by ID -export async function PUT(request: NextRequest, { params }: RouteParams) { - try { - const { id } = await params - const body = await request.json() - const { title, description, thumbnail, video_id, published_at, view_count, position } = body - - if (!id) { - return NextResponse.json({ error: 'Video ID diperlukan' }, { status: 400 }) - } - - // Validation - at least one field should be provided for update - if (!title && !description && !thumbnail && !video_id && !published_at && !view_count && position === undefined) { - return NextResponse.json({ error: 'Minimal satu field harus diisi untuk update!' }, { status: 400 }) - } - - const supabase = createAdminClient() - - // For admin operations, we'll use service role key - // So no need to check authentication for now - // TODO: Add proper admin authentication later - - // Prepare update object - only include provided fields - const updateData: any = { - updated_at: new Date().toISOString(), - } - - if (title !== undefined) updateData.title = title - if (description !== undefined) updateData.description = description - if (thumbnail !== undefined) updateData.thumbnail = thumbnail - if (video_id !== undefined) updateData.video_id = video_id - if (published_at !== undefined) updateData.published_at = published_at - if (view_count !== undefined) updateData.view_count = view_count - if (position !== undefined) updateData.position = position - - // Update video - const { data: updatedVideo, error } = await supabase - .from('vibe_videos') - .update(updateData) - .eq('id', id) - .select() - .single() - - if (error) { - if (error.code === 'PGRST116') { - // No rows found - return NextResponse.json({ error: 'Video tidak ditemukan' }, { status: 404 }) - } - - if (error.code === '23505') { - // Unique constraint violation - return NextResponse.json({ error: 'Video dengan ID ini sudah ada dalam database' }, { status: 409 }) - } - - console.error('Database error:', error) - return NextResponse.json({ error: 'Gagal mengupdate video' }, { status: 500 }) - } - - // Transform response - const transformedVideo = { - id: updatedVideo.id, - title: updatedVideo.title, - description: updatedVideo.description, - thumbnail: updatedVideo.thumbnail, - videoId: updatedVideo.video_id, - publishedAt: updatedVideo.published_at, - viewCount: updatedVideo.view_count, - position: updatedVideo.position, - createdAt: updatedVideo.created_at, - updatedAt: updatedVideo.updated_at, - } - - return NextResponse.json({ - message: 'Video berhasil diupdate!', - video: transformedVideo, - }) - } catch (error) { - console.error('API Error:', error) - return NextResponse.json({ error: 'Terjadi error saat mengupdate video cuy!' }, { status: 500 }) - } -} - -// DELETE - Delete vibe video by ID -export async function DELETE(request: NextRequest, { params }: RouteParams) { - try { - const { id } = await params - - if (!id) { - return NextResponse.json({ error: 'Video ID diperlukan' }, { status: 400 }) - } - - const supabase = createAdminClient() - - // For admin operations, we'll use service role key - // So no need to check authentication for now - // TODO: Add proper admin authentication later - - // Get video data first for logging - const { data: videoToDelete } = await supabase.from('vibe_videos').select('title, position').eq('id', id).single() - - // Delete video - const { error } = await supabase.from('vibe_videos').delete().eq('id', id) - - if (error) { - console.error('Database error:', error) - return NextResponse.json({ error: 'Gagal menghapus video dari database' }, { status: 500 }) - } - - // After deletion, we might want to reorder positions - // But for now, we'll keep the gaps in position numbers - - return NextResponse.json({ - message: `Video "${videoToDelete?.title || 'Unknown'}" berhasil dihapus!`, - }) - } catch (error) { - console.error('API Error:', error) - return NextResponse.json({ error: 'Terjadi error saat menghapus video cuy!' }, { status: 500 }) - } -} diff --git a/app/api/vibe-videos/route.ts b/app/api/vibe-videos/route.ts deleted file mode 100644 index da08b286..00000000 --- a/app/api/vibe-videos/route.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { cookies } from 'next/headers' -import { type NextRequest, NextResponse } from 'next/server' -import { createAdminClient } from '@/lib/supabase/admin' -import { createClient } from '@/lib/supabase/server' - -interface VibeVideo { - id?: string - title: string - description: string - thumbnail: string - video_id: string - published_at: string - view_count: string - position: number -} - -// GET - List all vibe videos ordered by position -export async function GET() { - try { - const supabase = createAdminClient() - - const { data: videos, error } = await supabase - .from('vibe_videos') - .select('*') - .order('position', { ascending: true }) - - if (error) { - console.error('Database error:', error) - return NextResponse.json({ error: 'Failed to fetch videos from database' }, { status: 500 }) - } - - // Transform data untuk compatibility dengan frontend - const transformedVideos = - videos?.map((video) => ({ - id: video.id, - title: video.title, - description: video.description, - thumbnail: video.thumbnail, - videoId: video.video_id, - publishedAt: video.published_at, - viewCount: video.view_count, - position: video.position, - createdAt: video.created_at, - updatedAt: video.updated_at, - })) || [] - - return NextResponse.json({ videos: transformedVideos }) - } catch (error) { - console.error('API Error:', error) - return NextResponse.json({ error: 'Terjadi error saat mengambil data video cuy!' }, { status: 500 }) - } -} - -// POST - Create new vibe video -export async function POST(request: NextRequest) { - try { - const body = await request.json() - const { title, description, thumbnail, video_id, published_at, view_count } = body - - // Validation - if (!title || !description || !thumbnail || !video_id || !published_at) { - return NextResponse.json( - { - error: 'Field title, description, thumbnail, video_id, dan published_at wajib diisi!', - }, - { status: 400 }, - ) - } - - const supabase = createAdminClient() - - // For admin operations, we'll use service role key - // So no need to check authentication for now - // TODO: Add proper admin authentication later - - // Get next position (max + 1) - const { data: maxPositionData } = await supabase - .from('vibe_videos') - .select('position') - .order('position', { ascending: false }) - .limit(1) - .single() - - const nextPosition = maxPositionData ? maxPositionData.position + 1 : 1 - - // Insert new video - const { data: newVideo, error } = await supabase - .from('vibe_videos') - .insert({ - title, - description, - thumbnail, - video_id, - published_at, - view_count: view_count || '0', - position: nextPosition, - }) - .select() - .single() - - if (error) { - if (error.code === '23505') { - // Unique constraint violation - return NextResponse.json({ error: 'Video dengan ID ini sudah ada dalam database' }, { status: 409 }) - } - - console.error('Database error:', error) - return NextResponse.json({ error: 'Gagal menambahkan video ke database' }, { status: 500 }) - } - - // Transform response - const transformedVideo = { - id: newVideo.id, - title: newVideo.title, - description: newVideo.description, - thumbnail: newVideo.thumbnail, - videoId: newVideo.video_id, - publishedAt: newVideo.published_at, - viewCount: newVideo.view_count, - position: newVideo.position, - createdAt: newVideo.created_at, - updatedAt: newVideo.updated_at, - } - - return NextResponse.json( - { - message: 'Video berhasil ditambahkan!', - video: transformedVideo, - }, - { status: 201 }, - ) - } catch (error) { - console.error('API Error:', error) - return NextResponse.json({ error: 'Terjadi error saat menambahkan video cuy!' }, { status: 500 }) - } -} diff --git a/app/api/youtube/route.ts b/app/api/youtube/route.ts deleted file mode 100644 index 16e3139e..00000000 --- a/app/api/youtube/route.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { cleanDescription, extractYouTubeVideoId, generateYouTubeUrl, isValidVideoId } from '@/lib/youtube-utils' - -interface YouTubeOEmbedResponse { - title: string - author_name: string - author_url: string - type: string - height: number - width: number - version: string - provider_name: string - provider_url: string - thumbnail_height: number - thumbnail_width: number - thumbnail_url: string - html: string -} - -interface VideoMetadata { - title: string - description: string - thumbnail: string - views: number - publishedAt: string - channelTitle: string - videoId: string - url: string -} - -export async function POST(request: NextRequest) { - try { - const { url } = await request.json() - - if (!url) { - return NextResponse.json({ error: 'URL YouTube diperlukan cuy!' }, { status: 400 }) - } - - // Extract video ID dari URL - const videoId = extractYouTubeVideoId(url) - if (!videoId || !isValidVideoId(videoId)) { - return NextResponse.json({ error: 'URL YouTube tidak valid. Pastiin format yang benar ya!' }, { status: 400 }) - } - - const watchUrl = generateYouTubeUrl(videoId) - - // 1. Fetch basic info dari oEmbed (tidak butuh API key) - const oembedUrl = `https://www.youtube.com/oembed?url=${encodeURIComponent(watchUrl)}&format=json` - - let oembedData: YouTubeOEmbedResponse - try { - const oembedResponse = await fetch(oembedUrl) - if (!oembedResponse.ok) { - throw new Error('Video tidak ditemukan atau private') - } - oembedData = await oembedResponse.json() - } catch (error) { - return NextResponse.json( - { - error: 'Video tidak dapat diakses. Mungkin private atau tidak tersedia.', - }, - { status: 404 }, - ) - } - - // 2. Scrape YouTube watch page untuk detailed stats - let views = 0 - let publishedAt = '' - let description = '' - - try { - const watchResponse = await fetch(watchUrl, { - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Accept-Encoding': 'gzip, deflate, br', - DNT: '1', - Connection: 'keep-alive', - 'Upgrade-Insecure-Requests': '1', - }, - }) - - if (watchResponse.ok) { - const html = await watchResponse.text() - - // Multiple patterns untuk extract views - YouTube sering ganti structure - const viewPatterns = [ - /"viewCount":\s*"(\d+)"/, - /"viewCount":{"videoViewCountRenderer":{"viewCount":{"simpleText":"([\d,]+)/, - /"videoViewCountRenderer":{"viewCount":{"simpleText":"([\d,]+)/, - /viewCount":{"runs":\[{"text":"([\d,]+)/, - /views":{"runs":\[{"text":"([\d,]+)/, - /shortViewCount":{"simpleText":"([\d,]+)/, - /"shortViewCount":{"accessibility":{"accessibilityData":{"label":"([\d,]+)/, - / 0) { - views = parsedViews - viewsFound = true - console.log(`[YouTube Debug] Views found using pattern: ${pattern.source}, value: ${views}`) - break - } - } - } - - if (!viewsFound) { - console.warn(`[YouTube Debug] No views found for video ${videoId}. HTML length: ${html.length}`) - // Log first few patterns untuk debugging - viewPatterns.slice(0, 3).forEach((pattern, i) => { - const match = html.match(pattern) - console.log(`[YouTube Debug] Pattern ${i + 1}: ${pattern.source} - Match: ${match ? match[1] : 'none'}`) - }) - } - - // Extract publish date - multiple patterns karena YouTube sering ganti structure - const publishPatterns = [ - /"publishDate":"([^"]+)"/, - /"dateText":{"simpleText":"([^"]+)"}/, - /"publishedTimeText":{"simpleText":"([^"]+)"}/, - /uploadDate":"([^"]+)"/, - /]*data-published="([^"]+)"/, - ] - - let publishFound = false - for (const pattern of publishPatterns) { - const match = html.match(pattern) - if (match && match[1]) { - publishedAt = match[1] - publishFound = true - console.log(`[YouTube Debug] Publish date found using pattern: ${pattern.source}, value: ${publishedAt}`) - break - } - } - - if (!publishFound) { - console.warn(`[YouTube Debug] No publish date found for video ${videoId}`) - // Log first few patterns untuk debugging - publishPatterns.slice(0, 3).forEach((pattern, i) => { - const match = html.match(pattern) - console.log( - `[YouTube Debug] Publish pattern ${i + 1}: ${pattern.source} - Match: ${match ? match[1] : 'none'}`, - ) - }) - } - - // Extract description dari microformat - const descriptionMatch = html.match(/"description":{"simpleText":"([^"]+)"}/) - if (descriptionMatch) { - description = descriptionMatch[1] - .replace(/\\n/g, '\n') - .replace(/\\"/g, '"') - .replace(/\\u([0-9a-fA-F]{4})/g, (match, code) => String.fromCharCode(parseInt(code, 16))) - } - - // Fallback untuk description dari meta tags - if (!description) { - const metaDescMatch = html.match(/ identity.provider !== 'email' && identity.provider !== 'phone') - - console.log( - `[Callback] User login detected - Email: ${user.email}, OAuth: ${isOAuthUser}, Provider(s): ${user.identities?.map((i) => i.provider).join(', ')}`, - ) - - // Only check email confirmation for email/password signup users - // OAuth users (Google, GitHub, etc.) are already verified by their providers - if (!isOAuthUser && !user.email_confirmed_at) { - console.log('[Callback] Email/password user email not confirmed:', user.email) - // Sign out the user and redirect to auth page with message - await supabase.auth.signOut() - return NextResponse.redirect( - `${origin}/user/auth?error=Email not confirmed. Please check your inbox and click the confirmation link.`, - ) - } - - // Create profile for all authenticated users (OAuth or email confirmed) - const { data: existingProfile } = await supabase.from('users').select('id').eq('id', user.id).single() - - if (!existingProfile) { - console.log(`[Callback] Creating profile for ${isOAuthUser ? 'OAuth' : 'email confirmed'} user:`, user.email) - - // Generate unique username with fallback for collisions - const baseUsername = - user.email - ?.split('@')[0] - ?.toLowerCase() - .replace(/[^a-z0-9]/g, '') || `user${user.id.slice(0, 8)}` - - let username = baseUsername - let attempts = 0 - const maxAttempts = 5 - - // Check for username collisions and generate unique one - while (attempts < maxAttempts) { - const { data: existingUser } = await supabase - .from('users') - .select('username') - .eq('username', username) - .single() - - if (!existingUser) { - break // Username is available - } - - attempts++ - username = `${baseUsername}${attempts}` - } - - // Final fallback if all attempts fail - if (attempts >= maxAttempts) { - username = `${baseUsername}${Math.floor(Math.random() * 1000)}` - } - - const profileData = { - id: user.id, - username: username, - display_name: - user.user_metadata?.full_name || user.user_metadata?.name || user.email?.split('@')[0] || 'User', - avatar_url: user.user_metadata?.avatar_url || user.user_metadata?.picture || '/vibedev-guest-avatar.png', - bio: null, - location: null, - website: null, - github_url: null, - twitter_url: null, - } - - console.log(`[Callback] Attempting to create profile with data:`, { - id: profileData.id, - username: profileData.username, - display_name: profileData.display_name, - email: user.email, - }) - - const { error: insertError } = await supabase.from('users').insert(profileData) - - if (insertError) { - console.error('[Callback] Profile creation error:', insertError) - console.error('[Callback] Failed profile data:', profileData) - return NextResponse.redirect( - `${origin}/user/auth?error=Failed to create user profile: ${insertError.message}`, - ) - } - - console.log(`[Callback] Profile created successfully for user: ${user.email} with username: ${username}`) - } - - console.log('[Callback] User authenticated successfully:', user.email) - - // Handle different flows based on auth method - if (isOAuthUser) { - // OAuth users: Keep them signed in and redirect to home - console.log('[Callback] OAuth user login successful, redirecting to home') - const forwardedHost = request.headers.get('x-forwarded-host') - const isLocalEnv = process.env.NODE_ENV === 'development' - - if (isLocalEnv) { - return NextResponse.redirect(`${origin}/`) - } else if (forwardedHost) { - return NextResponse.redirect(`https://${forwardedHost}/`) - } else { - return NextResponse.redirect(`${origin}/`) - } - } else { - // Email/password users: Sign out after email confirmation to force proper login - console.log('[Callback] Email confirmed user, signing out for security') - await supabase.auth.signOut() - - // Redirect email/password users to auth page with success message - const forwardedHost = request.headers.get('x-forwarded-host') - const isLocalEnv = process.env.NODE_ENV === 'development' - - if (isLocalEnv) { - return NextResponse.redirect(`${origin}${next}?success=Email confirmed successfully! You can now sign in.`) - } else if (forwardedHost) { - return NextResponse.redirect( - `https://${forwardedHost}${next}?success=Email confirmed successfully! You can now sign in.`, - ) - } else { - return NextResponse.redirect(`${origin}${next}?success=Email confirmed successfully! You can now sign in.`) - } - } - } - } else { - console.error('[Callback] Exchange code error:', error) - } - } - - // Return the user to an error page with instructions - return NextResponse.redirect(`${origin}/user/auth?error=Could not authenticate user`) -} diff --git a/app/blog/[slug]/blog-post-data.tsx b/app/blog/[slug]/blog-post-data.tsx index f6b7d5e9..f2f0d194 100644 --- a/app/blog/[slug]/blog-post-data.tsx +++ b/app/blog/[slug]/blog-post-data.tsx @@ -1,7 +1,7 @@ import { format } from 'date-fns' import { ArrowLeft, Calendar, Clock, Eye } from 'lucide-react' -import Link from 'next/link' -import { notFound } from 'next/navigation' +import { Link } from '@tanstack/react-router' +import { notFound } from '@/lib/navigation' import { BlogViewTracker } from '@/components/blog/blog-view-tracker' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' @@ -74,7 +74,7 @@ export default async function BlogPostData({ params }: Props) { .single() if (error || !post || post.status !== 'published') { - notFound() + throw notFound() } // Fetch view count for this post @@ -128,7 +128,7 @@ export default async function BlogPostData({ params }: Props) {
@@ -143,7 +143,7 @@ export default async function BlogPostData({ params }: Props) {
{authorSlug ? ( {authorContent} diff --git a/app/blog/blog-page-client.tsx b/app/blog/blog-page-client.tsx index 34c9dd7b..86ac3975 100644 --- a/app/blog/blog-page-client.tsx +++ b/app/blog/blog-page-client.tsx @@ -1,7 +1,7 @@ 'use client' import { FileText, PenSquare } from 'lucide-react' -import Link from 'next/link' +import { Link } from '@tanstack/react-router' import { BlogCard } from '@/components/blog/blog-card' import { FloatingWriteButton } from '@/components/blog/floating-write-button' import { Button } from '@/components/ui/button' @@ -77,7 +77,7 @@ export default function BlogPageClient({ isLoggedIn, user, posts }: BlogPageClie {/* Quick Actions for logged-in users */} {isLoggedIn && (
- + ) : ( - + )} diff --git a/app/blog/editor/[slug]/page.tsx b/app/blog/editor/[slug]/page.tsx index b1a2bcd9..3ce6fae7 100644 --- a/app/blog/editor/[slug]/page.tsx +++ b/app/blog/editor/[slug]/page.tsx @@ -1,4 +1,4 @@ -import { redirect } from 'next/navigation' +import { redirect } from '@/lib/navigation' import { getPostForEdit } from '@/lib/actions/blog' import { createClient } from '@/lib/supabase/server' import type { User } from '@/types/homepage' diff --git a/app/blog/editor/blog-editor-client.tsx b/app/blog/editor/blog-editor-client.tsx index 885a523c..5fd1c7ea 100644 --- a/app/blog/editor/blog-editor-client.tsx +++ b/app/blog/editor/blog-editor-client.tsx @@ -1,6 +1,6 @@ 'use client' -import { useRouter } from 'next/navigation' +import { useRouter } from '@/lib/navigation' import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' import { BlogGuideModal } from '@/components/blog/blog-guide-modal' @@ -145,13 +145,13 @@ export default function BlogEditorClient({ user, initialData, mode = 'create' }: if (mode === 'create') { if (status === 'published') { - router.push(`/blog/${finalSlug}`) + router.navigate({ to: `/blog/${finalSlug}` }) } else { - router.push('/dashboard/posts') + router.navigate({ to: '/dashboard/posts' }) } } else { if (status === 'published') { - router.push(`/blog/${finalSlug}`) + router.navigate({ to: `/blog/${finalSlug}` }) } } } else { diff --git a/app/blog/editor/page.tsx b/app/blog/editor/page.tsx index f04d9b2c..ce2771a9 100644 --- a/app/blog/editor/page.tsx +++ b/app/blog/editor/page.tsx @@ -1,4 +1,4 @@ -import { redirect } from 'next/navigation' +import { redirect } from '@/lib/navigation' import { createClient } from '@/lib/supabase/server' import type { User } from '@/types/homepage' import BlogEditorClient from './blog-editor-client' diff --git a/app/blog/layout.tsx b/app/blog/layout.tsx deleted file mode 100644 index 1683c42a..00000000 --- a/app/blog/layout.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import type { Metadata } from 'next' -import { getLocale, getTranslations } from 'next-intl/server' -import type React from 'react' -import { absoluteUrl } from '@/lib/seo/site-url' - -export async function generateMetadata(): Promise { - const locale = await getLocale() - const t = await getTranslations({ locale, namespace: 'metadata' }) - - const title = t('pages.blog.title') - const description = t('pages.blog.description') - const pathname = '/blog' - const url = absoluteUrl(pathname) - - return { - title, - description, - alternates: { - canonical: pathname, - }, - openGraph: { - title, - description, - url, - siteName: 'VibeDev ID', - images: [{ url: '/opengraph-image.png', width: 1200, height: 630, alt: t('ogImageAlt') }], - locale: locale === 'en' ? 'en_US' : 'id_ID', - type: 'website', - }, - twitter: { - card: 'summary_large_image', - title, - description, - images: ['/opengraph-image.png'], - site: '@vibedevid', - creator: '@vibedevid', - }, - } -} - -export default function BlogLayout({ - children, -}: Readonly<{ - children: React.ReactNode -}>) { - return <>{children} -} diff --git a/app/calendar/page.tsx b/app/calendar/page.tsx index 3dc677ed..e32ce637 100644 --- a/app/calendar/page.tsx +++ b/app/calendar/page.tsx @@ -1,7 +1,7 @@ 'use client' import { CalendarDays, Clock, Filter, MapPin, Plus, Search, Users } from 'lucide-react' -import { useRouter } from 'next/navigation' +import { useRouter } from '@/lib/navigation' import { useState } from 'react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx deleted file mode 100644 index d81ac979..00000000 --- a/app/dashboard/layout.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { redirect } from 'next/navigation' -import { Navbar } from '@/components/ui/navbar' -import { createClient } from '@/lib/supabase/server' -import type { User } from '@/types/homepage' - -async function getUserData(userId: string, email: string): Promise { - const supabase = await createClient() - const { data: profile } = await supabase - .from('users') - .select('id, display_name, avatar_url, username, role') - .eq('id', userId) - .single() - - if (!profile) { - return null - } - - return { - id: profile.id, - name: profile.display_name, - email, - avatar: profile.avatar_url || '/vibedev-guest-avatar.png', - username: profile.username, - role: profile.role ?? null, - } -} - -export default async function DashboardLayout({ children }: { children: React.ReactNode }) { - const supabase = await createClient() - const { - data: { user }, - } = await supabase.auth.getUser() - - if (!user) { - redirect('/user/auth?redirectTo=/dashboard') - } - - const userData = await getUserData(user.id, user.email || '') - - if (!userData) { - redirect('/user/auth?redirectTo=/dashboard') - } - - return ( -
- -
-
{children}
-
-
- ) -} diff --git a/app/dashboard/posts/post-dashboard-client.tsx b/app/dashboard/posts/post-dashboard-client.tsx index dce1efac..5108e98b 100644 --- a/app/dashboard/posts/post-dashboard-client.tsx +++ b/app/dashboard/posts/post-dashboard-client.tsx @@ -1,9 +1,9 @@ 'use client' import { Calendar, Edit, Eye, FileText, MoreHorizontal, Plus, Trash2 } from 'lucide-react' -import Image from 'next/image' -import Link from 'next/link' -import { useRouter } from 'next/navigation' +import { Image } from '@unpic/react' +import { Link } from '@tanstack/react-router' +import { useRouter } from '@/lib/navigation' import { useCallback, useEffect, useState } from 'react' import { toast } from 'sonner' @@ -109,7 +109,7 @@ export function PostDashboardClient() {

Your Posts

Manage your blog posts and track their performance.

- + @@ -150,7 +150,7 @@ export function PostDashboardClient() { )} {activeTab === 'all' && ( - + )} @@ -166,7 +166,7 @@ export function PostDashboardClient() {
{post.title} @@ -208,10 +208,8 @@ export function PostDashboardClient() {
{post.title}
)} @@ -224,7 +222,7 @@ export function PostDashboardClient() { asChild className="h-8 w-8 text-muted-foreground hover:text-foreground" > - + Edit @@ -245,7 +243,7 @@ export function PostDashboardClient() { {post.status === 'published' && ( View Live diff --git a/app/event/[slug]/event-detail-data.tsx b/app/event/[slug]/event-detail-data.tsx index a770f967..a9d16ac0 100644 --- a/app/event/[slug]/event-detail-data.tsx +++ b/app/event/[slug]/event-detail-data.tsx @@ -1,7 +1,7 @@ import { ArrowLeft, Calendar, ExternalLink, MapPin, Users } from 'lucide-react' -import Image from 'next/image' -import Link from 'next/link' -import { notFound } from 'next/navigation' +import { Image } from '@unpic/react' +import { Link } from '@tanstack/react-router' +import { notFound } from '@/lib/navigation' import { EventCard } from '@/components/event/event-card' import { EventShareButton } from '@/components/event/event-share-button' import { Badge } from '@/components/ui/badge' @@ -22,7 +22,7 @@ export default async function EventDetailData({ params }: EventDetailPageProps) const { event } = await getEventBySlug(slug) if (!event) { - notFound() + throw notFound() } const { events, error: relatedError } = await getRelatedEvents(event.category, event.id) @@ -53,7 +53,7 @@ export default async function EventDetailData({ params }: EventDetailPageProps) {/* Back Navigation */}
@@ -92,9 +92,7 @@ export default async function EventDetailData({ params }: EventDetailPageProps) alt={event.name} width={1200} height={675} - priority className="h-auto w-full object-contain transition-transform duration-700 hover:scale-105" - sizes="(max-width: 768px) 100vw, (max-width: 1200px) 66vw, 50vw" />
@@ -201,7 +199,7 @@ export default async function EventDetailData({ params }: EventDetailPageProps)

Related Events

- + - - -
- - -
-
-
-
- -
- ) -} diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index 4d28a835..00000000 --- a/app/page.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { Suspense } from 'react' -import { ProjectGridSkeleton, Skeleton } from '@/components/ui/skeleton' -import HomePageData, { type HomePageSearchParams } from './home-page-data' - -function HomeLoadingFallback() { - return ( -
-
-
-
- - -
- - -
-
-
- - -
-
-
- -
-
-
- - -
-
-
- -
- - -
-
-
-
- -
-
- -
-
- -
-
-
-
- -
-
-
- -
-
-
- - -
-
- {[1, 2].map((i) => ( -
- - - - -
- ))} -
-
-
-
- ) -} - -export default async function HomePage({ searchParams }: { searchParams: Promise }) { - return ( - }> - - - ) -} diff --git a/app/privacy-policy/privacy-policy-client.tsx b/app/privacy-policy/privacy-policy-client.tsx index 3d249daf..4b24b738 100644 --- a/app/privacy-policy/privacy-policy-client.tsx +++ b/app/privacy-policy/privacy-policy-client.tsx @@ -1,6 +1,6 @@ 'use client' -import Link from 'next/link' +import { Link } from '@tanstack/react-router' import { Footer } from '@/components/ui/footer' import { Navbar } from '@/components/ui/navbar' import { useAuth } from '@/hooks/useAuth' @@ -157,13 +157,13 @@ export function PrivacyPolicyClient() {
Lihat juga Terms of Service Kembali ke beranda VibeDev ID diff --git a/app/project/[slug]/page.tsx b/app/project/[slug]/page.tsx index 23575685..4411f5d9 100644 --- a/app/project/[slug]/page.tsx +++ b/app/project/[slug]/page.tsx @@ -1,9 +1,9 @@ // Server Component - No 'use client' directive! import { Calendar, ExternalLink, Globe, Tag, User } from 'lucide-react' -import Image from 'next/image' -import Link from 'next/link' -import { notFound, redirect } from 'next/navigation' +import { Image } from '@unpic/react' +import { Link } from '@tanstack/react-router' +import { notFound, redirect } from '@/lib/navigation' import type { ReactNode } from 'react' import { ProjectActionsClient } from '@/components/project/ProjectActionsClient' import { ProjectEditClient } from '@/components/project/ProjectEditClient' @@ -127,7 +127,7 @@ export default async function ProjectDetailsPage({ params }: { params: Promise<{ if (legacyProject?.slug) { redirect(`/project/${legacyProject.slug}`) } - notFound() + throw notFound() } // Parallel data fetching on server @@ -137,9 +137,9 @@ export default async function ProjectDetailsPage({ params }: { params: Promise<{ getCategories(), ]) - // Handle errors with Next.js notFound() + // Handle errors with Next.js throw notFound() if (projectError || !project) { - notFound() + throw notFound() } // Fetch comments using project.id (UUID) @@ -349,7 +349,7 @@ export default async function ProjectDetailsPage({ params }: { params: Promise<{ {project.author.location}

- +