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..c19a7b4b 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 @@ -29,3 +32,6 @@ next-env.d.ts nul # next-agents-md .next-docs/ + +# Editor settings +.zed/ diff --git a/.vite-hooks/pre-commit b/.vite-hooks/pre-commit new file mode 100644 index 00000000..85fb65b4 --- /dev/null +++ b/.vite-hooks/pre-commit @@ -0,0 +1 @@ +vp staged diff --git a/AGENTS.md b/AGENTS.md index b5702e80..adcdb557 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,350 +1,20 @@ -# PROJECT KNOWLEDGE BASE +# RULES -**Generated:** 2026-03-23 -**Commit:** 2d09393 -**Branch:** main +- Selalu update `README.md` jika ada perubahan yang relevan setelah mengerjakan task atau membuat perubahan apa pun. -## OVERVIEW + -VibeDev ID is a Next.js 16 App Router app backed by Supabase, `next-intl`, Bun, Biome, Vitest, and Playwright. This repo also contains `admin-kit/`, a separate Next.js 15 package with its own pnpm + ESLint + Prettier workflow. +# Using Vite+, the Unified Toolchain for the Web -This is the only repo-local `AGENTS.md`. Ignore `%TEMP%/nextjs-docs/AGENTS.md`; it is imported reference material, not repo-local policy. +This project is using Vite+, a unified toolchain built on top of Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task. Vite+ wraps runtime management, package management, and frontend tooling in a single global CLI called `vp`. Vite+ is distinct from Vite, and it invokes Vite through `vp dev` and `vp build`. Run `vp help` to print a list of commands and `vp --help` for information about a specific command. -## STRUCTURE +Docs are local at `node_modules/vite-plus/docs` or online at https://viteplus.dev/guide/. -```text -./ -├── app/ # Next.js 16 routes, layouts, API handlers -├── components/ # feature components + shared wrappers -├── lib/ # helpers, Supabase, auth, SEO, uploads -├── docs/ # operational and design documentation -├── scripts/ # SQL migrations + repo scripts -├── admin-kit/ # separate Next.js 15 package -``` +## Review Checklist -## ROOT COMMANDS +- [ ] Run `vp install` after pulling remote changes and before getting started. +- [ ] Run `vp check` and `vp test` to format, lint, type check and test changes. +- [ ] Check if there are `vite.config.ts` tasks or `package.json` scripts necessary for validation, run via `vp run - - {/* JSON-LD: WebSite */} - - - - - {tCommon('skipToMainContent')} - - - - {children} - - - - - {shouldLoadVercelInsights && ( - <> - - - - )} - - {/* Service Worker Registration untuk Mobile Performance */} - - - - ) -} diff --git a/app/not-found.tsx b/app/not-found.tsx deleted file mode 100644 index a1796db0..00000000 --- a/app/not-found.tsx +++ /dev/null @@ -1,53 +0,0 @@ -'use client' - -import { Compass, Home } from 'lucide-react' -import Link from 'next/link' -import { Button } from '@/components/ui/button' -import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from '@/components/ui/empty' - -export default function NotFound() { - return ( -
-
-
-
-
-
- - - - 404 - - Page not found - - -
- - - -
-
-
-
-
-
-
-
-
- ) -} 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/page.tsx b/app/privacy-policy/page.tsx deleted file mode 100644 index 783de79c..00000000 --- a/app/privacy-policy/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { Metadata } from 'next' -import { PrivacyPolicyClient } from './privacy-policy-client' - -export const metadata: Metadata = { - title: 'Privacy Policy', - description: - 'Kebijakan Privasi VibeDev ID untuk menjelaskan pengumpulan, penggunaan, perlindungan, dan hak pengguna atas data pribadi.', -} - -export default function PrivacyPolicyPage() { - 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..e06d2926 100644 --- a/app/project/[slug]/page.tsx +++ b/app/project/[slug]/page.tsx @@ -1,9 +1,8 @@ -// Server Component - No 'use client' directive! +// Presentational project detail page — rendered by app/routes/project.$slug.tsx 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 type { ReactNode } from 'react' import { ProjectActionsClient } from '@/components/project/ProjectActionsClient' import { ProjectEditClient } from '@/components/project/ProjectEditClient' @@ -14,15 +13,10 @@ import { CommentSection } from '@/components/ui/comment-section' import { Footer } from '@/components/ui/footer' import { ScrollReveal } from '@/components/ui/motion-wrapper' import { Navbar } from '@/components/ui/navbar' -import { OptimizedAvatar } from '@/components/ui/optimized-avatar' +import { UserAvatar } from '@/components/ui/user-avatar' import { ProjectImageCarousel } from '@/components/ui/project-image-carousel' import { ProminentLikeButton } from '@/components/ui/prominent-like-button' import { UserDisplayName } from '@/components/ui/user-display-name' -import { getProjectBySlug } from '@/lib/actions' -import { getComments } from '@/lib/actions/comments' -import { getCategories } from '@/lib/categories' -import { checkProjectOwnership, getCurrentUser } from '@/lib/server/auth' -import { getProjectByUUID, isUUID } from '@/lib/server/utils' /** * Render plain text description with proper line breaks and bullet points. @@ -117,44 +111,34 @@ function renderDescription(text: string): ReactNode { }) } -// Server Component - async function -export default async function ProjectDetailsPage({ params }: { params: Promise<{ slug: string }> }) { - const { slug } = await params - - // Handle legacy UUID redirect on server - if (isUUID(slug)) { - const legacyProject = await getProjectByUUID(slug) - if (legacyProject?.slug) { - redirect(`/project/${legacyProject.slug}`) - } - notFound() - } - - // Parallel data fetching on server - const [currentUser, { project, error: projectError }, categories] = await Promise.all([ - getCurrentUser(), - getProjectBySlug(slug), - getCategories(), - ]) - - // Handle errors with Next.js notFound() - if (projectError || !project) { - notFound() - } - - // Fetch comments using project.id (UUID) - const { comments: initialComments } = await getComments('project', project.id) - - // Check ownership on server - const isOwner = currentUser ? await checkProjectOwnership(project.author.username, currentUser.id) : false +// Presentational component — data is fetched in the route loader (app/routes/project.$slug.tsx) +import type { getCurrentUser } from '@/lib/server/auth' +import type { getProjectBySlug } from '@/lib/actions' +import type { getCategories } from '@/lib/categories' +import type { getComments } from '@/lib/actions/comments' + +type ProjectData = NonNullable>['project']> +type CurrentUser = Awaited> +type Categories = Awaited> +type InitialComments = Awaited>['comments'] + +export interface ProjectDetailsData { + slug: string + project: ProjectData + currentUser: CurrentUser + categories: Categories + initialComments: InitialComments + isOwner: boolean +} +export default function ProjectDetailsPage({ data }: { data: ProjectDetailsData }) { + const { slug, project, currentUser, categories, initialComments, isOwner } = data return (
{/* Background Gradient Overlay */}
{ + if (e.currentTarget.src !== `${window.location.origin}/default-favicon.svg`) { + e.currentTarget.src = '/default-favicon.svg' + } + }} />
@@ -329,11 +318,14 @@ export default async function ProjectDetailsPage({ params }: { params: Promise<{
-
@@ -349,7 +341,7 @@ export default async function ProjectDetailsPage({ params }: { params: Promise<{ {project.author.location}

- +
-
+
{loading ? ( ) : projects.length === 0 ? (

{t('noProjects')}