diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..43092b2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + pull_request: + push: + branches: [main, master] + +jobs: + quality: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 11.15.1 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install --config.strict-peer-dependencies=false + + - name: Lint + run: pnpm lint + + - name: Typecheck + run: pnpm typecheck + + - name: Test + run: pnpm test + + - name: Build + run: pnpm build diff --git a/app/(dashboard)/documents/page.tsx b/app/(dashboard)/documents/page.tsx index f87e412..5bb0f5d 100644 --- a/app/(dashboard)/documents/page.tsx +++ b/app/(dashboard)/documents/page.tsx @@ -25,12 +25,21 @@ export default function DocumentsPage() { // After successful payment redirect: show document upload useEffect(() => { if (typeof window === "undefined") return - const params = new URLSearchParams(window.location.search) - if (params.get("payment") === "success") { - setHasCredit(true) - setShowPaymentModal(false) - setShowUpload(true) - window.history.replaceState({}, "", "/documents") + + const url = new URL(window.location.href) + if (url.searchParams.get("payment") === "success") { + url.searchParams.delete("payment") + + const cleanUrl = `${url.pathname}${url.searchParams.toString() ? `?${url.searchParams.toString()}` : ""}${url.hash}` + window.history.replaceState({}, "", cleanUrl) + + const timer = window.setTimeout(() => { + setHasCredit(true) + setShowPaymentModal(false) + setShowUpload(true) + }, 0) + + return () => window.clearTimeout(timer) } }, []) @@ -63,12 +72,42 @@ export default function DocumentsPage() { } useEffect(() => { - fetchDocuments() + const timer = window.setTimeout(() => { + void (async () => { + try { + const response = await fetch("/api/documents") + if (response.ok) { + const data = await response.json() + setDocuments(data) + } + } catch (error) { + console.error("[v0] Failed to fetch documents:", error) + } finally { + setIsLoading(false) + } + })() + }, 0) + return () => window.clearTimeout(timer) }, []) useEffect(() => { if (showUpload || showPaymentModal) return - fetchCredits() + const timer = window.setTimeout(() => { + void (async () => { + try { + const res = await fetch("/api/payments/credits") + if (res.ok) { + const data = await res.json() + setHasCredit(data.hasCredit) + } else { + setHasCredit(false) + } + } catch { + setHasCredit(false) + } + })() + }, 0) + return () => window.clearTimeout(timer) }, [showUpload, showPaymentModal]) const handleDelete = async (doc: Document) => { diff --git a/app/p/[userId]/page.tsx b/app/p/[userId]/page.tsx index b2c46a5..a2961c3 100644 --- a/app/p/[userId]/page.tsx +++ b/app/p/[userId]/page.tsx @@ -1,3 +1,4 @@ +import Link from "next/link" import { getPublicUserProfile } from "@/lib/db" import { notFound } from "next/navigation" import { Badge } from "@/components/ui/badge" @@ -182,9 +183,9 @@ export default async function PublicProfilePage({ params }: PageProps) { diff --git a/app/payment-callback/page.tsx b/app/payment-callback/page.tsx index cb9204c..26cd656 100644 --- a/app/payment-callback/page.tsx +++ b/app/payment-callback/page.tsx @@ -3,13 +3,16 @@ import { useEffect, useState } from "react" export default function PaymentCallbackPage() { - const [status, setStatus] = useState<"verifying" | "success" | "failed">("verifying") + const [status, setStatus] = useState<"verifying" | "success" | "failed">(() => { + if (typeof window === "undefined") return "verifying" + const params = new URLSearchParams(window.location.search) + return params.get("reference") ? "verifying" : "failed" + }) useEffect(() => { const params = new URLSearchParams(typeof window !== "undefined" ? window.location.search : "") const reference = params.get("reference") if (!reference) { - setStatus("failed") return } diff --git a/components/career/career-picker.tsx b/components/career/career-picker.tsx index f2c247e..b8491dd 100644 --- a/components/career/career-picker.tsx +++ b/components/career/career-picker.tsx @@ -45,13 +45,15 @@ export function CareerPicker({ if (value) return // Already selected; don't search const trimmed = query.trim() if (trimmed.length < 2) { - setSuggestions([]) - setLoading(false) - return + const timer = window.setTimeout(() => { + setSuggestions([]) + setLoading(false) + }, 0) + return () => window.clearTimeout(timer) } const reqId = ++requestIdRef.current - setLoading(true) const t = setTimeout(async () => { + setLoading(true) try { const res = await fetch(`/api/onet/careers?q=${encodeURIComponent(trimmed)}`) if (!res.ok) { diff --git a/components/documents/payment-modal.tsx b/components/documents/payment-modal.tsx index 37a01e0..37e2b40 100644 --- a/components/documents/payment-modal.tsx +++ b/components/documents/payment-modal.tsx @@ -47,13 +47,15 @@ export function PaymentModal({ open, onClose, onSuccess }: PaymentModalProps) { // Reset state every time the modal opens useEffect(() => { - if (open) { + if (!open) return + const timer = window.setTimeout(() => { setPhase("form") setError(null) setStatusMessage("") setReference(null) setSubmitting(false) - } + }, 0) + return () => window.clearTimeout(timer) }, [open]) // Polling loop while waiting for STK push to settle diff --git a/components/landing/hero.tsx b/components/landing/hero.tsx index 12b23c6..033ee02 100644 --- a/components/landing/hero.tsx +++ b/components/landing/hero.tsx @@ -1,3 +1,4 @@ +import Image from "next/image" import Link from "next/link" import { ArrowUpRight } from "lucide-react" import { TrustedBy } from "@/components/landing/trusted-by" @@ -56,10 +57,11 @@ export function Hero() { {/* Skill Growth card */}
Skill
@@ -71,10 +73,11 @@ export function Hero() {
{/* Wide chart card — grows to align its bottom with the hero buttons */}