Skip to content

Latest commit

 

History

416 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VibeDev ID

When the Codes Meet the Vibes 🚀

VibeDev ID adalah komunitas vibrant developer, AI enthusiasts, dan tech innovators Indonesia yang punya visi dan passion yang sama untuk bikin produk digital yang keren. Kami menghubungkan creator yang sepikiran untuk kolaborasi, belajar, dan berkembang bareng.

Indonesia's premier community for developers, vibe coders, and AI enthusiasts. Showcase projects, collaborate, network, and level up your skills in web, mobile, and AI development.

Features

  • 🔐 User Authentication - Secure login with Better Auth (email + Google/GitHub OAuth)
  • 👤 Developer Profiles - Customizable profiles with bio, skills, and social links
  • 📝 Project Showcase - Share dan showcase project keren lo
  • 💬 Community Interaction - Comments, likes, dan diskusi project
  • 🤝 Networking & Collaboration - Connect sama developer yang sepikiran
  • 💬 Join Community - Tombol Join Community di homepage (hero dan CTA) membuka https://wa.vibedevid.com
  • 📰 Blog System - Rich text editor dengan Novel/TipTap untuk artikel teknis
  • 📊 Views Tracking - Session-based analytics untuk project insights
  • 🤖 AI Leaderboard - Ranking dan showcase AI tools favorit komunitas
  • 🗓️ Community Calendar - Event list at /event/list. /calendar redirects there.
  • 🎉 Events System - Submit dan browse community events dengan approval workflow
  • 💬 Testimonials - Unlisted /testimonial form, admin approve/reject/unpublish, lalu tampil di section review landing page
  • 📈 User Dashboard - Personal dashboard untuk manage projects dan activity
  • 🛠️ Admin Dashboard - Full admin panel dengan moderation tools
  • ⌨️ Command Palette - Quick navigation dan search (cmdk)
  • 🎬 Vibe Videos - Video content section untuk tutorial dan highlights
  • 🌙 Dark/Light Mode - UI theme yang nyaman mata
  • 📱 Responsive Design - Perfect di semua device
  • 🏷️ Project Categories - Personal Web, SaaS, Landing Page, dan lainnya
  • ❤️ Like System - Like project yang lo suka
  • 🔍 Discovery - Filter dan cari project berdasarkan kategori
  • 🖼️ Progressive Image Loading - Blur placeholders dengan lazy loading
  • 🌍 Internationalization - Full support English dan Indonesia (react-i18next). Locale comes from the NEXT_LOCALE cookie, not navigator.language. The resolved locale is also written into SSR HTML (window.__VIBEDEV_LOCALE__) so the client bundle matches the first paint without waiting for a client beforeLoad.
  • 📱 In-app browsers - Public pages load without the blank TanStack "Something went wrong!" screen. Route errors show a recovery page (reload / back to home). The leftover Next.js service worker is unregistered on purpose; /sw.js is an uninstall shim so already-installed clients drop it.
  • 🛡️ Spam Protection - Email domain whitelist dan bot protection
  • 📊 Analytics Dashboard - Charts dan data visualization (recharts)
  • FAQ System - Frequently asked questions management
  • 🚨 Content Moderation - Report dan moderation system
  • 🔎 SEO - SSR meta/Open Graph, JSON-LD, dynamic sitemap, robots.txt, canonical, dan noindex di halaman privat

Tech Stack

  • Framework: TanStack Start (Vite + Nitro) with @tanstack/react-router file-based routing
  • Build/Dev: Vite 8 + Nitro server output
  • Language: TypeScript 5.x
  • Database: Neon Postgres in aws-ap-southeast-1 (Drizzle ORM). See Neon Postgres.
  • Authentication: Better Auth (/api/auth/*) with email/password + Google/GitHub OAuth
  • Styling: Tailwind CSS v4
  • UI Components: Radix UI + shadcn/ui (50+ components)
  • Animations: Motion (Framer Motion) — shared scroll-reveal primitives in components/ui/motion-wrapper.tsx (ScrollReveal, StaggerContainer/StaggerItem, ScaleIn) applied across the landing page and the /project/list, /blog, /event/list, and /[username] profile pages (header/stats entrance reveal, staggered project/blog card grids, scaled tab-content reveals); Radix dropdown menus (e.g. avatar menu) and the FAQ accordion animate open/close via Framer Motion, all with prefers-reduced-motion support
  • Rich Text: Novel + TipTap
  • Icons: Lucide React + Tabler Icons + LobeHub Icons
  • Internationalization: react-i18next
  • Forms: React Hook Form + Zod
  • AI Integration: AI SDK + OpenRouter Provider
  • File Uploads: UploadThing + Better Upload
  • Command Palette: cmdk
  • Charts: Recharts
  • Dates: date-fns + date-fns-tz
  • Testing: Playwright (E2E) + Vitest (unit)
  • Code Quality: Biome (linter + formatter)
  • Hosting: Cloudflare Workers (Nitro cloudflare_module preset)
  • Toast: Sonner

Note: this app was migrated from Next.js 16 App Router to TanStack Start. Some app/ subfolders still use Next.js-style names (page.tsx, [slug], (admin)) but are now plain view/component modules imported by route files in app/routes/. A full rename of those folders is high-churn with little runtime benefit, so they stay as live view modules.

Leftovers we keep on purpose

Leftover Why it stays
NEXT_LOCALE cookie Live locale source. Public pages and tests still read this name.
NEXT_PUBLIC_SITE_URL Live server/SEO site URL. Also a Worker secret. VITE_SITE_URL is the browser twin.
next-themes Theme library. It is not the Next.js framework.
public/sw.js + root unregister script Uninstall shim for the old Next.js service worker. Already-installed clients still need it.
Next-named folders under app/ (except app/routes/) Live view modules. Routes import them.

Getting Started

This path is for a local clone. You apply the schema to your Neon project, then load demo rows. Do not point .env.local at production.

Prerequisites

  • Node.js 18+ or Bun (recommended)
  • Vite+ (vp CLI) — this project uses the Vite+ unified toolchain
  • A free Neon Postgres project (not the production database)

OAuth, UploadThing, Resend, and OpenRouter are optional. You can browse the app and log in with seed email accounts without those keys.

Installation

  1. Clone the repository:
git clone https://github.com/julianromli/vibedevid.git
cd vibedevid
  1. Install dependencies:
vp install

Vite+ ships vite/vitest in devDependencies as npm aliases to @voidzero-dev/vite-plus-core / @voidzero-dev/vite-plus-test, and the overrides field in package.json pins both to those same alias specs. The vitest override must match the devDependencies.vitest spec exactly (i.e. the npm:@voidzero-dev/vite-plus-test@latest alias, not a plain version like 4.1.9). Bun tolerates a mismatch, but npm/npx (used by npx wrangler deploy on Cloudflare) rejects it with EOVERRIDE: Override for vitest conflicts with direct dependency. If you upgrade vite-plus and vp install changes the alias, re-sync overrides.vitest to match devDependencies.vitest. Do not route this pin through a catalog: reference.

  1. Create a Neon project at neon.tech (free plan). Copy the pooled connection string (-pooler in the host) and the direct connection string.

    Optional throwaway database: npx neon-new creates a Claimable Neon project without an account. Unclaimed projects expire after 72 hours.

  2. Copy env and set the required local values:

cp .env.example .env.local
DATABASE_URL=postgresql://user:pass@ep-xxx-pooler.ap-southeast-1.aws.neon.tech/neondb?sslmode=require
DATABASE_URL_UNPOOLED=postgresql://user:pass@ep-xxx.ap-southeast-1.aws.neon.tech/neondb?sslmode=require
BETTER_AUTH_SECRET=replace-with-openssl-rand-base64-32
BETTER_AUTH_URL=http://localhost:3000
VITE_BETTER_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_SITE_URL=http://localhost:3000
VITE_SITE_URL=http://localhost:3000

Keep VITE_SITE_URL and NEXT_PUBLIC_SITE_URL on localhost. The seed script refuses vibedevid.com.

  1. Apply the schema and load demo data:
bun run db:setup

This runs migrate:schema then db:seed. You can run the steps apart:

bun run migrate:schema
bun run db:seed
# bun run db:seed -- --reset   # delete seed-owned rows, then insert again

db:seed is idempotent. It upserts seed rows only and does not wipe rows you create after that.

One-time Supabase → Neon import is not part of this path. See docs/migrations/neon-better-auth.md.

  1. Start the app:
bun run dev

Open http://localhost:3000. Log in with a seed account below. New sign-up still needs email verification. Without Resend, the verification link prints in the server log.

Local seed accounts

Shared password: VibeDevLocal1!

Role Email Profile
Admin seed.admin@gmail.com /seedadmin
Moderator seed.moderator@gmail.com /seedmoderator
Member seed.sarah@gmail.com /sarahchen
Member seed.marcus@gmail.com /marcusrodriguez

Admin dashboard: http://localhost:3000/dashboard.

The seed includes categories, projects, published and draft posts, comments, likes, events (including one pending), FAQs, vibe videos, testimonials, and one comment report. Images use files in public/optimized/, so UploadThing is not required to browse.

Commands

# Install dependencies
vp install

# Apply schema + demo rows to your local Neon project
bun run db:setup
bun run db:seed
bun run db:seed -- --reset

# Development server (vite dev, port 3000)
bun run dev

# Build for production (vite build — Nitro server output in .output/)
# Pre-generates responsive AVIF/WebP image variants via scripts/optimize-images.mjs first.
# Needs the sharp build-time dep (devDependency). sharp does not run in the Worker.
bun run build

# Regenerate optimized image variants only (public/optimized/). Needs sharp.
bun run optimize:images

# Recompress existing UploadThing project/video images (dry run by default;
# add --apply to write). Needs DATABASE_URL + UPLOADTHING_TOKEN.
bun run backfill:images

# Start the production server
bun run start

# Type checking (required for type safety)
bunx tsc --noEmit

# Linting & Formatting (Biome)
bun run lint        # changed files only
bun run lint:all    # full repo
bun run format

# Unit tests (Vitest)
bun run test
bun run test:watch

# E2E tests (Playwright)
bun run test:e2e
bun run test:e2e:headed   # see browser
bun run test:e2e:debug    # step through

# Run a single Playwright spec / test by name
bunx playwright test tests/views-tracking.spec.ts
bunx playwright test -g "should track views when visiting project page"

Environment Variables

Variable Description Required for local
DATABASE_URL Neon pooled URL (hostname includes -pooler) Yes
DATABASE_URL_UNPOOLED Neon direct URL for drizzle-kit, migrate:schema, and db:seed For schema/seed
BETTER_AUTH_SECRET Random secret for Better Auth (min 32 chars) Yes
BETTER_AUTH_URL Public app URL for auth callbacks Yes
VITE_BETTER_AUTH_URL Same URL, exposed to browser Yes
NEXT_PUBLIC_SITE_URL Canonical site URL (server + SEO) Yes
VITE_SITE_URL Same URL, exposed to browser Yes
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET Google OAuth Optional
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET GitHub OAuth Optional
UPLOADTHING_TOKEN UploadThing API token (uploads only) Optional
OPENROUTER_API_KEY AI blog features Optional
RESEND_API_KEY Resend API key for auth verification/reset Optional
EMAIL_FROM Verified sender address for Resend Optional

Deployment (Cloudflare Workers)

The app deploys to Cloudflare Workers using the Nitro cloudflare_module preset (configured in vite.config.ts) plus wrangler.jsonc at the repo root. Production site: https://vibedevid.com.

# 1. Build with production client URLs baked in (VITE_* are compile-time)
NEXT_PUBLIC_SITE_URL=https://vibedevid.com \
VITE_SITE_URL=https://vibedevid.com \
VITE_BETTER_AUTH_URL=https://vibedevid.com \
bun run build

# 2. Preview locally on Workers runtime (copy .env.local → .dev.vars)
bunx wrangler dev

# 3. Set Worker secrets (runtime — see list below), then deploy
bun run scripts/sync-wrangler-secrets.ts   # reads .env.local → wrangler secret bulk
bunx wrangler deploy

Notes:

  • compatibility_flags: ["nodejs_compat"] and compatibility date 2024-09-19 (required for Workers Static Assets) are set in wrangler.jsonc.
  • Server-only secrets must be read via getServerRuntimeSecrets() (lib/server/runtime-secrets.ts), not process.env directly. On Workers, bindings are exposed on globalThis.__env__ per request.
  • VITE_* values are inlined at build time — rebuild before deploy whenever VITE_BETTER_AUTH_URL or VITE_SITE_URL changes.
  • Worker secrets (runtime): pooled DATABASE_URL (Singapore -pooler host), BETTER_AUTH_SECRET, BETTER_AUTH_URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, NEXT_PUBLIC_SITE_URL, UPLOADTHING_TOKEN, OPENROUTER_API_KEY, RESEND_API_KEY, and EMAIL_FROM. Do not put DATABASE_URL_UNPOOLED on the Worker. Keep it in local .env.local for schema tools.
  • For wrangler dev, create a gitignored .dev.vars file with the same keys.
  • Add OAuth redirect URLs in Google/GitHub developer consoles:
    • https://vibedevid.com/api/auth/callback/google
    • https://vibedevid.com/api/auth/callback/github
  • After cutover, remove legacy Supabase secrets from the Worker if still present.

Neon Postgres

Production data is Neon Postgres in aws-ap-southeast-1 (Singapore). All server reads and writes go through Drizzle getDb() plus Better Auth checks (requireUser, requireAdminOrModeratorUser).

  • App / Workers use the pooled DATABASE_URL. The host includes -pooler.
  • Schema tools use DATABASE_URL_UNPOOLED (direct). drizzle.config.ts, bun run migrate:schema, and bun run db:seed prefer this URL when it is set.
  • Local demo databun run db:seed upserts seed-owned rows (users, projects, posts, events, and related tables). It refuses vibedevid.com site URLs. Use --reset to delete seed-owned rows and insert them again.
  • Branch policy is in neon.ts: new non-default branches get a 7-day TTL, 0.25–1 CU, and a 5-minute suspend.
  • Harden migration scripts/migrations/neon/03_harden_schema.sql runs after 01_schema.sql. It is idempotent. It adds unique likes and post slugs, parent XOR checks, extra indexes, expired session/verification cleanup, and drops supabase_auth_staging.
  • Homepage cache (lib/server/short-ttl-cache.ts) stores active categories and home vibe videos for 5 minutes (Cloudflare Cache API on Workers, memory locally). Admin vibe-video mutations clear the video cache key.
  • Auth cleanupgetServerSession deletes expired session and verification rows at most once per isolate every 10 minutes.
  • One-time Supabase import scripts live in scripts/migrate-to-neon.ts. Staging schema supabase_auth_staging holds password hashes. Drop it after migrate:users succeeds.

Workspace link file .neon is gitignored. Do not commit connection strings.

Database Schema

Core Tables

users - Extended auth profiles with role system (0=admin, 1=moderator, 2=user), social links, bio, and location

projects - Project showcase with slug-based URLs for SEO, tags, and category

comments - Unified comments for Blog and Projects. XOR check: exactly one parent.

likes - User likes. Unique per (user_id, project_id) or (user_id, post_id). XOR check: exactly one parent.

views - Session-based views tracking with 30-minute timeout, IP + User Agent fingerprinting. XOR check: exactly one parent.

posts - Blog posts with rich text content (JSON), featured flag, read time, unique slug

post_tags - Blog tag categorization

blog_post_tags - Many-to-many relationship between posts and tags

events - Community events with approval workflow, status (upcoming/past), location details

categories - Project categories with icon and color

faqs - FAQ content with sort order and active status

blog_reports - Comment/report moderation system

vibe_videos - Video content for tutorials and highlights

Security

  • Authorization enforced in application code (Drizzle queries + requireUser / requireAdminOrModeratorUser) — no Supabase RLS on Neon
  • Authenticated insert/update untuk data milik user sendiri
  • Guest comments diizinkan dengan author_name field
  • Admin/moderator role diperlukan untuk moderation
  • Email domain whitelist untuk registration

Project Structure

├── app/
│   ├── routes/             # TanStack Router file-based routes (source of truth)
│   │   ├── __root.tsx      # Root layout, head/meta, providers
│   │   ├── index.tsx       # Homepage
│   │   ├── $username.tsx   # Dynamic user profile pages (skips reserved slugs)
│   │   ├── testimonial.tsx # Unlisted public testimonial form
│   │   ├── project.list.tsx / project.$slug.tsx / project.submit.tsx
│   │   ├── blog.tsx (layout) / blog.index.tsx / blog.$slug.tsx / blog.editor*.tsx
│   │   ├── event.list.tsx / event.$slug.tsx
│   │   ├── _admin/         # Protected admin route group (role-gated layout)
│   │   ├── admin.tsx       # Standalone admin page (role-gated via beforeLoad)
│   │   ├── api/            # API route handlers (server.handlers blocks)
│   │   └── auth.callback.ts
│   ├── routeTree.gen.ts    # Generated route tree (do not hand-edit)
│   ├── router.tsx          # Router factory
│   ├── start.ts            # TanStack Start instance + request middleware
│   └── <feature>/          # Legacy Next.js-named folders, now view/component
│                           #   modules imported by routes (blog, project,
│                           #   event, [username], (admin), user/auth, ...)
├── components/
│   ├── logo.tsx            # VibeDev V mark (currentColor SVG for small wells)
│   ├── ui/                 # 50+ shadcn/ui components
│   ├── sections/           # Page sections (hero, showcase, faq)
│   ├── blog/               # Blog-specific components
│   ├── project/            # Project-specific components
│   ├── admin-panel/        # Admin dashboard components
│   ├── event/              # Event-specific components
│   └── profile/            # Profile-specific components
├── hooks/                  # Custom React hooks
├── lib/
│   ├── actions/            # Server data/mutations + *.functions.ts (createServerFn)
│   │                       #   rule: foo.functions.ts wraps ONLY foo.ts or its
│   │                       #   read partner in lib/server/
│   │                       #   Dashboard client boards import *.functions.ts only.
│   │                       #   Direct imports of action modules pull
│   │                       #   @tanstack/react-start/server into the browser
│   │                       #   chunk and throw AsyncLocalStorage is not a constructor.
│   ├── db/                 # Drizzle schema + `getDb()` (Neon serverless)
│   ├── auth/               # Better Auth server/client config
│   ├── server/             # Server-only utilities (auth, runtime secrets,
│   │                       #   short-ttl-cache.ts);
│   │                       #   public read modules: project-public.ts,
│   │                       #   blog-public.ts (list + detail reads)
│   ├── routes/             # Route helpers (server locale/translations)
│   │                       #   locale: lib/locale.ts owns registry + cookie
│   ├── uploadthing.ts      # UploadThing server router
│   ├── uploadthing-client.ts   # Client upload helpers
│   ├── uploadthing-router.ts   # Client-safe router types
│   └── ai/                 # AI integration (OpenRouter)
├── i18n/                   # react-i18next config (index.ts, routing.ts)
├── types/                  # TypeScript type definitions
├── neon.ts                 # Neon branch compute policy
├── scripts/                # migrate-to-neon.ts + scripts/migrations/neon/
├── tests/                  # Vitest unit tests + Playwright E2E tests
├── messages/               # i18n messages (en.json, id.json)
├── docs/                   # Documentation (security, database, deployment)
├── plans/                  # Self-contained React improvement plans (improve-react)
├── biome.json              # Biome configuration
├── vite.config.ts          # Vite + TanStack Start + Nitro configuration
└── tsconfig.json           # TypeScript configuration

React improvement plans

plans/ holds one implementation plan per confirmed finding from the 2026-08-29 React audit (commit c829824). All 17 plans are DONE. See plans/README.md for status.

Key Features Deep Dive

Project Submission Validation

Aturan validasi field Project (batas panjang, jumlah tag/gambar, format URL, kategori aktif) hidup di satu modul: lib/project-submission.ts. Modul ini murni zod + helper tanpa dependensi server, jadi form submit (submit-project-form) dan form edit (ProjectEditClient) bisa menjalankan schema yang sama persis dengan yang di-enforce server (lib/actions/projects.ts) — pesan error klien dan server tidak bisa drift karena keduanya membaca satu sumber.

  • PROJECT_LIMITS — satu-satunya tempat konstanta min/max field hidup.
  • PROJECT_FORM_FIELDS — nama field FormData yang melintasi seam (snake_case wire keys).
  • buildProjectSubmissionSchema(activeCategoryNames?) — schema lengkap; kategori di-check terhadap daftar kategori aktif pada submit maupun edit (bila query kategori gagal, edit menurun ke tanpa-check kategori, sama seperti create).
  • readProjectFormData / buildProjectFieldErrors / formatProjectFieldErrors — baca FormData, flatten zod issues ke wire contract, dan format ke string error. Klien memvalidasi per-step saat navigasi plus full-gate saat submit; server selalu mengeksekusi schema penuh.

Project Filtering & Sorting

Project list (/project/list) dan homepage memakai fetchProjectsWithSorting (lib/server/project-public.ts) lewat server function fetchProjectsWithSortingFn.

Filter kategori bersifat resilient terhadap dua representasi nilai yang tersimpan di kolom projects.category:

  • Project baru menyimpan category name (slug, mis. landing-page).
  • Project lama / seed menyimpan display text (mis. Landing Page).

Nilai filter dari UI di-resolve ke kedua bentuk lalu dimatch dengan in(...), sehingga semua project pada satu kategori tetap muncul.

Sorting tersedia dalam tiga mode:

  • newest — urut created_at desc (limit langsung di SQL). Default.
  • top — all-time best, murni berdasarkan total likes (tiebreak terbaru).
  • trending — likes diberi bobot recency (likes / umur-hari).

Untuk top/trending, likes dihitung terpisah dan tidak bisa di-order di SQL, jadi query mengambil candidate window yang lebih lebar dulu, lalu sort + truncate ke limit di JS agar project lama dengan banyak likes tidak terpotong.

Pilihan filter & sort di UI tidak mengubah URL. Nilai awal tetap di-seed dari search params saat load pertama (deep link tetap jalan), tapi mengganti dropdown setelahnya hanya mengubah state lokal tanpa menyentuh query string.

Event Submission

Event submission via submitEventFn (lib/actions/events.functions.ts) → submitEvent (lib/actions/events.ts). Server read path canonical: lib/server/events-public.ts (detail + list). Slugs memakai slug dari form (fallback: diturunkan dari nama) lalu di-uniquify dengan retry saat 23505 (maksimal 100 percobaan) — dua event dengan slug sama tidak lagi bentrok.

Testimonials

The public form lives at /testimonial. It is a form route, not a profile URL. /$username skips reserved first-segment slugs (testimonial, calendar, admin, and the other static app paths in lib/reserved-profile-slugs.ts). Sign-up suffixes a reserved base name (testimonial_1); profile edit rejects a reserved username. Git push does not publish the Worker — after merge, run bun run build then bunx wrangler deploy.

The form is not in the navbar, footer, or sitemap. robots.txt disallows the path and the route emits noindex. Anyone with the URL can submit (no login). Fields: full name, role, testimonial text, required photo. The server accepts FormData, checks JPEG/PNG/WebP magic bytes (not the client MIME string), uploads the photo with UploadThing UTApi, and inserts a pending row. If the insert fails, the uploaded file is deleted. Server-side UTApi must receive UPLOADTHING_TOKEN from getServerRuntimeSecrets() (same source as /api/uploadthing). Cloudflare Workers do not expose that secret on process.env, so new UTApi() with no options fails the submit. The value must be the UploadThing dashboard token (base64 JSON with apiKey, appId, and regions), not a placeholder and not a raw sk_live_ API key.

Admin dashboard tab testimonials (/dashboard?tab=testimonials) lets role-0 admins approve, reject, or unpublish. Reject and unpublish set status=rejected and keep the row. Approve sets status=approved and refreshes approved_at.

The landing reviews section shows approved rows (newest first). Each marquee column gets the full list, rotated, so a small set still shows on mobile (columns 2 and 3 stay hidden below md / lg). If approved count is 0, it falls back to the i18n seed in messages/*/reviews.testimonials. Schema: testimonials table + scripts/migrations/neon/04_testimonials.sql. Apply with bun run migrate:schema.

Admin Board Loader

loadDashboardBoardData (app/(admin)/dashboard/dashboard-data.ts) returns a discriminated union DashboardBoardData (kind: "projects" | "blog" | ...), so DashboardTabPanel narrows per tab — no any payload crosses the seam. Tambahan board = satu member union + satu case dengan narrowing.

Dashboard board UI modules call mutations through *.functions.ts (createServerFn). They must not import lib/actions/admin/*.ts or lib/actions/events.ts as values. Those modules import @tanstack/react-start/server, which constructs AsyncLocalStorage at load time and crashes the browser dashboard chunk.

Comments System

Centralized comments component yang works untuk both Blog dan Project.

import { CommentSection } from "@/components/ui/comment-section";

<CommentSection
  entityType="post" // or "project"
  entityId={id}
  initialComments={comments}
  isLoggedIn={!!user}
/>;

Views Tracking

Session-based analytics dengan:

  • 30-minute session timeout
  • IP + User Agent fingerprinting
  • Bot protection (user agent filtering)
  • Unique visitor counting

Progressive Image Loading

  • SVG blur placeholders
  • Intersection Observer lazy loading
  • AVIF/WebP automatic optimization
  • Client-safe processing (no sharp in client bundle)

Performance Optimization

Homepage performance is tuned for Core Web Vitals (LCP/TBT):

  • Build-time responsive images - scripts/optimize-images.mjs (sharp) pre-generates AVIF + WebP variants of large public images into public/optimized/ at multiple widths. Runs automatically before bun run build. sharp is a required build-time devDependency so Cloudflare bun install --frozen-lockfile can resolve it. It is not imported from app or Worker code. The hero (the LCP element) drops from a ~660KB 2880×1800 PNG to ~90KB at its 1200px breakpoint.
  • OptimizedImage component (components/ui/optimized-image.tsx) - Renders a <picture> with AVIF/WebP srcset pointing at the generated variants. The hero uses priority (eager load + fetchpriority="high") and is preloaded in the home route head().
  • Right-sized remote avatars - GitHub avatars request ?s=64; testimonial avatars use the 128px optimized variants instead of full-size source PNGs.
  • Code-split below-the-fold sections - The homepage lazy-loads non-critical sections (video showcase, community features, AI tools, reviews, FAQ, CTA, footer) with React.lazy + Suspense so they no longer block initial hydration (reduces Total Blocking Time).
  • No service worker cache - The app does not register a service worker. The previous worker precached / and used cache-first for .js, which served stale HTML/chunks in in-app browsers after deploys. Visiting any page unregisters existing workers and clears Cache Storage.
  • Long-lived asset caching - routeRules in vite.config.ts emit cache-control headers (written to the generated .output/public/_headers) for /optimized/* and /fonts/* (immutable). Only non-overlapping directory rules are used: Cloudflare _headers wildcards match across / and concatenate every matching rule's value, so broad extension rules like /*.avif are avoided (they would corrupt the /optimized/* header).
  • Faster server response (TTFB) - The homepage previously made several redundant per-request DB/auth roundtrips. getServerSession() (lib/server/auth.ts) is now memoized per request (keyed on the request object via a WeakMap), so the session resolves once instead of being re-fetched by the root beforeLoad, route loaders, and getBatchLikeStatus. The home route loader also reuses the user already resolved in the root beforeLoad instead of re-querying it, and verbose per-request console.log calls in the hot data path were removed.
  • Short-TTL homepage cache - Active categories, home vibe videos, and approved testimonials are cached for 5 minutes (lib/server/short-ttl-cache.ts) so list pages do not scan those tables on every request.
  • SQL aggregates - getBatchLikeStatus and category project counts use GROUP BY / count() in Postgres instead of loading full like or project rows into the Worker.
  • Client-side upload compression - User-uploaded project images go through lib/image-compression.ts (onBeforeUploadBegin on the UploadThing buttons), which downscales to 1600px and re-encodes to WebP in the browser before upload. UploadThing (ufs.sh) has no on-the-fly resizing and sharp cannot run on Cloudflare Workers, so compressing client-side is the only no-cost option — it typically turns a 380KB+ upload into ~80-120KB. Falls back to the original file if compression fails or does not reduce size.
  • Backfill for existing images - scripts/backfill-image-compression.ts (run with bun run backfill:images, or --apply to write) recompresses already-uploaded project/video images: download → WebP via sharp → re-upload to UploadThing → update the DB row → delete the old file. Runs on Node/Bun (needs DATABASE_URL + UPLOADTHING_TOKEN), defaults to a dry run. Use this once to fix images uploaded before client-side compression was added.

SEO

Search-engine optimization is handled at the route level:

  • Server-rendered meta - Per-route head() blocks emit title, description, Open Graph, and Twitter Card tags (rendered in SSR HTML, verifiable with a Googlebot user-agent).
  • Structured data - Organization + WebSite JSON-LD in the root route.
  • Dynamic sitemap - app/routes/sitemap[.]xml.ts queries Neon (Drizzle) for published posts, projects, approved events, and public profiles, plus static routes. Auth-gated pages are excluded; lastmod uses real content timestamps with a fallback.
  • robots.txt - app/routes/robots[.]txt.ts serves a single User-agent: * group, disallows private/API paths, and references the sitemap. Note: if Cloudflare's managed robots.txt is enabled it will shadow this route — keep only one source of truth.
  • Canonical URLs - Self-referencing canonicals on content and list pages; the homepage and /project/list consolidate ?filter/?sort variants onto their clean URLs.
  • noindex - Admin, dashboard, blog editor, project submit, /testimonial, and auth routes emit robots: noindex, nofollow via the shared NOINDEX_META helper in lib/seo/site-url.ts.
  • Dynamic OG image - app/routes/api/og.ts renders a branded 1200×630 SVG from a title query param (dependency-free, Cloudflare Workers-safe).
  • LCP-friendly images - Below-the-fold images use loading="lazy"; preload is reserved for the hero/logo (above the fold).

Email Domain Whitelist

Registration dibatasi ke domain terpercaya:

  • gmail.com, yahoo.com, outlook.com
  • dan 20+ domain edukasi/tech lain

AI Integration

Built-in AI features using OpenRouter:

  • AI-powered content suggestions
  • Leaderboard for AI tool rankings
  • Integration with Agentic workflow

Events System

Full-featured events management:

  • Event submission with approval workflow
  • Status tracking (upcoming, past)
  • Location types (online, offline, hybrid)
  • Organizer profiles
  • Cover image support

Admin Dashboard

Comprehensive admin tools:

  • User management (role: admin, moderator, user)
  • Content moderation (comments, reports)
  • Event approval workflow
  • Analytics dashboard

Skills System

This project includes AI agent skills for enhanced development:

  • motion-design - Motion design principles for emotionally-driven, technically sound animations

See .agents/skills/ for workspace skills.

Contributing

Kami welcome kontribusi dari semua developer! 🎉

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes
  4. Test thoroughly
  5. Commit your changes (git commit -m 'feat: Add amazing feature')
  6. Push to the branch (git push origin feature/amazing-feature)
  7. Submit a pull request

Development Guidelines

  • Follow TypeScript strict mode
  • Use Tailwind CSS for styling
  • Biome will auto-format on commit
  • Write descriptive commit messages (Conventional Commits)
  • Test your changes on both desktop and mobile
  • Maintain the informal but professional Indonesian tone in UI copy

Code Style

  • 2-space indentation, no semicolons, single quotes
  • Biome enforces unified linting + formatting
  • @/ prefix untuk absolute imports
  • Group imports: React → Third-party → Internal

Security

For detailed security documentation, see:

License

This project is licensed under the MIT License.


Built with ❤️ by VibeDev ID Community

About

Resources

Security policy

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages