First Commit
This commit is contained in:
parent
3ad762e261
commit
7b10378ee4
21
components.json
Normal file
21
components.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
1072
package-lock.json
generated
1072
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@ -9,19 +9,34 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/font": "^14.2.15",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.539.0",
|
||||
"next": "15.4.6",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"next": "15.4.6"
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.4.6",
|
||||
"@eslint/eslintrc": "^3"
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/analytics-dashboard-preview.png
Normal file
BIN
public/analytics-dashboard-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 543 KiB |
BIN
public/blog-platform-preview.png
Normal file
BIN
public/blog-platform-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/landing-page-preview.png
Normal file
BIN
public/landing-page-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 669 KiB |
BIN
public/marketing-website-preview.png
Normal file
BIN
public/marketing-website-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
public/saas-platform-preview.png
Normal file
BIN
public/saas-platform-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/seo-blog-preview.png
Normal file
BIN
public/seo-blog-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 609 KiB |
5
src/app/architecture/page.tsx
Normal file
5
src/app/architecture/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import ArchitectureGenerator from "@/components/architecture/architecture-generator"
|
||||
|
||||
export default function ArchitecturePage() {
|
||||
return <ArchitectureGenerator />
|
||||
}
|
||||
5
src/app/auth/page.tsx
Normal file
5
src/app/auth/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { AuthPage } from "@/components/auth/auth-page"
|
||||
|
||||
export default function AuthPageRoute() {
|
||||
return <AuthPage />
|
||||
}
|
||||
5
src/app/business-context/page.tsx
Normal file
5
src/app/business-context/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import BusinessContextGenerator from "@/components/business-context/business-context-generator"
|
||||
|
||||
export default function BusinessContextPage() {
|
||||
return <BusinessContextGenerator />
|
||||
}
|
||||
5
src/app/features/page.tsx
Normal file
5
src/app/features/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { FeaturesPage } from "@/components/features/features-page"
|
||||
|
||||
export default function FeaturesPageRoute() {
|
||||
return <FeaturesPage />
|
||||
}
|
||||
@ -1,26 +1,122 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,34 +1,44 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import type React from "react"
|
||||
import type { Metadata } from "next"
|
||||
import { Poppins } from "next/font/google"
|
||||
import { AuthProvider } from "@/contexts/auth-context"
|
||||
import { AppLayout } from "@/components/layout/app-layout"
|
||||
import "./globals.css"
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
const poppins = Poppins({
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
weight: ["300", "400", "500", "600", "700"],
|
||||
variable: "--font-poppins",
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
title: "Codenuk - AI-Powered Project Builder",
|
||||
description: "Build scalable applications with AI-generated architecture and code",
|
||||
generator: "v0.dev",
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
<head>
|
||||
<style>{`
|
||||
html {
|
||||
font-family: ${poppins.style.fontFamily};
|
||||
--font-sans: ${poppins.variable};
|
||||
}
|
||||
`}</style>
|
||||
</head>
|
||||
<body className="font-sans antialiased">
|
||||
<AuthProvider>
|
||||
<AppLayout>
|
||||
<main>{children}</main>
|
||||
</AppLayout>
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
104
src/app/page.tsx
104
src/app/page.tsx
@ -1,103 +1,5 @@
|
||||
import Image from "next/image";
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
||||
src/app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
export default function HomePage() {
|
||||
redirect("/project-builder")
|
||||
}
|
||||
|
||||
10
src/app/project-builder/page.tsx
Normal file
10
src/app/project-builder/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { Suspense } from "react"
|
||||
import { MainDashboard } from "@/components/main-dashboard"
|
||||
|
||||
export default function ProjectBuilderPage() {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<MainDashboard />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
5
src/app/templates/page.tsx
Normal file
5
src/app/templates/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { TemplatesPage } from "@/components/templates/template-page"
|
||||
|
||||
export default function TemplatesPageRoute() {
|
||||
return <TemplatesPage />
|
||||
}
|
||||
191
src/components/architecture/architecture-generator.tsx
Normal file
191
src/components/architecture/architecture-generator.tsx
Normal file
@ -0,0 +1,191 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Code, Database, Server, Shield, Zap, Layers, GitBranch } from "lucide-react"
|
||||
|
||||
export default function ArchitectureGenerator() {
|
||||
const [selectedArchitecture, setSelectedArchitecture] = useState<string>("")
|
||||
const [projectName, setProjectName] = useState("")
|
||||
|
||||
const architecturePatterns = [
|
||||
{
|
||||
id: "monolithic",
|
||||
name: "Monolithic Architecture",
|
||||
description: "Single application with all components tightly coupled",
|
||||
icon: Layers,
|
||||
complexity: "Low",
|
||||
bestFor: "Small to medium projects",
|
||||
pros: ["Simple to develop", "Easy to deploy", "Lower initial cost"],
|
||||
cons: ["Harder to scale", "Technology lock-in", "Difficult to maintain"],
|
||||
},
|
||||
{
|
||||
id: "microservices",
|
||||
name: "Microservices Architecture",
|
||||
description: "Loosely coupled services that can be developed and deployed independently",
|
||||
icon: GitBranch,
|
||||
complexity: "High",
|
||||
bestFor: "Large, complex applications",
|
||||
pros: ["Independent scaling", "Technology diversity", "Easier maintenance"],
|
||||
cons: ["Distributed complexity", "Network overhead", "Data consistency challenges"],
|
||||
},
|
||||
{
|
||||
id: "serverless",
|
||||
name: "Serverless Architecture",
|
||||
description: "Event-driven, auto-scaling functions without server management",
|
||||
icon: Zap,
|
||||
complexity: "Medium",
|
||||
bestFor: "Event-driven applications",
|
||||
pros: ["Auto-scaling", "Pay-per-use", "No server management"],
|
||||
cons: ["Cold start latency", "Vendor lock-in", "Limited execution time"],
|
||||
},
|
||||
{
|
||||
id: "layered",
|
||||
name: "Layered Architecture",
|
||||
description: "Separation of concerns with distinct layers for different responsibilities",
|
||||
icon: Database,
|
||||
complexity: "Medium",
|
||||
bestFor: "Business applications",
|
||||
pros: ["Clear separation", "Easy to test", "Maintainable"],
|
||||
cons: ["Performance overhead", "Tight coupling between layers"],
|
||||
},
|
||||
]
|
||||
|
||||
const generateArchitecture = () => {
|
||||
if (!selectedArchitecture || !projectName) return
|
||||
|
||||
// Here you would typically call an API to generate the architecture
|
||||
console.log(`Generating ${selectedArchitecture} architecture for ${projectName}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-4 mb-12">
|
||||
<h1 className="text-4xl font-bold text-gray-900">Architecture Generator</h1>
|
||||
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
Generate optimal architecture patterns for your project based on requirements and scale
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Project Name Input */}
|
||||
<div className="max-w-md mx-auto mb-8">
|
||||
<label htmlFor="projectName" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Project Name
|
||||
</label>
|
||||
<Input
|
||||
id="projectName"
|
||||
placeholder="Enter your project name"
|
||||
value={projectName}
|
||||
onChange={(e) => setProjectName(e.target.value)}
|
||||
className="text-center"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Architecture Patterns */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
|
||||
{architecturePatterns.map((pattern) => {
|
||||
const Icon = pattern.icon
|
||||
return (
|
||||
<Card
|
||||
key={pattern.id}
|
||||
className={`cursor-pointer transition-all duration-300 hover:shadow-lg ${
|
||||
selectedArchitecture === pattern.id
|
||||
? "ring-2 ring-blue-500 bg-blue-50"
|
||||
: "hover:border-blue-300"
|
||||
}`}
|
||||
onClick={() => setSelectedArchitecture(pattern.id)}
|
||||
>
|
||||
<CardHeader className="text-center pb-4">
|
||||
<div className="mx-auto w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mb-3">
|
||||
<Icon className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
<CardTitle className="text-lg">{pattern.name}</CardTitle>
|
||||
<Badge variant="outline" className="w-fit mx-auto">
|
||||
{pattern.complexity} Complexity
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-sm text-gray-600 text-center">{pattern.description}</p>
|
||||
<div className="text-center">
|
||||
<p className="text-xs font-medium text-gray-700">Best for:</p>
|
||||
<p className="text-xs text-gray-600">{pattern.bestFor}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Generate Button */}
|
||||
<div className="text-center">
|
||||
<Button
|
||||
onClick={generateArchitecture}
|
||||
disabled={!selectedArchitecture || !projectName}
|
||||
className="bg-blue-600 hover:bg-blue-700 px-8 py-3 text-lg"
|
||||
>
|
||||
<Code className="mr-2 h-5 w-5" />
|
||||
Generate Architecture
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Selected Architecture Details */}
|
||||
{selectedArchitecture && (
|
||||
<div className="mt-12 max-w-4xl mx-auto">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Layers className="mr-2 h-5 w-5" />
|
||||
Architecture Details
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{(() => {
|
||||
const pattern = architecturePatterns.find(p => p.id === selectedArchitecture)
|
||||
if (!pattern) return null
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h4 className="font-semibold text-green-700 mb-3 flex items-center">
|
||||
<Shield className="mr-2 h-4 w-4" />
|
||||
Advantages
|
||||
</h4>
|
||||
<ul className="space-y-2">
|
||||
{pattern.pros.map((pro, index) => (
|
||||
<li key={index} className="flex items-center text-sm text-gray-700">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full mr-3"></div>
|
||||
{pro}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-red-700 mb-3 flex items-center">
|
||||
<Server className="mr-2 h-4 w-4" />
|
||||
Considerations
|
||||
</h4>
|
||||
<ul className="space-y-2">
|
||||
{pattern.cons.map((con, index) => (
|
||||
<li key={index} className="flex items-center text-sm text-gray-700">
|
||||
<div className="w-2 h-2 bg-red-500 rounded-full mr-3"></div>
|
||||
{con}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
25
src/components/auth/auth-page.tsx
Normal file
25
src/components/auth/auth-page.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { SignInForm } from "./signin-form"
|
||||
import { SignUpForm } from "./signup-form"
|
||||
|
||||
export function AuthPage() {
|
||||
const [isSignIn, setIsSignIn] = useState(true)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">Codenuk</h1>
|
||||
<p className="text-gray-600">AI-Powered Project Builder</p>
|
||||
</div>
|
||||
{isSignIn ? (
|
||||
<SignInForm onToggleMode={() => setIsSignIn(false)} />
|
||||
) : (
|
||||
<SignUpForm onToggleMode={() => setIsSignIn(true)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
117
src/components/auth/signin-form.tsx
Normal file
117
src/components/auth/signin-form.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Eye, EyeOff, Loader2 } from "lucide-react"
|
||||
import { useAuth } from "@/contexts/auth-context"
|
||||
|
||||
interface SignInFormProps {
|
||||
onToggleMode: () => void
|
||||
}
|
||||
|
||||
export function SignInForm({ onToggleMode }: SignInFormProps) {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const [formData, setFormData] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
})
|
||||
|
||||
const { login } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const success = await login(formData.email, formData.password)
|
||||
if (success) {
|
||||
router.push("/")
|
||||
} else {
|
||||
setError("Invalid email or password")
|
||||
}
|
||||
} catch (err) {
|
||||
setError("An error occurred. Please try again.")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-2xl font-bold">Sign In</CardTitle>
|
||||
<CardDescription>Enter your credentials to access your account</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Enter your password"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="text-red-600 text-sm text-center bg-red-50 p-2 rounded-md">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full cursor-pointer" disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Signing In...
|
||||
</>
|
||||
) : (
|
||||
"Sign In"
|
||||
)}
|
||||
</Button>
|
||||
<div className="text-center">
|
||||
<Button type="button" variant="link" onClick={onToggleMode} className="text-sm cursor-pointer">
|
||||
Don't have an account? Sign up
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
159
src/components/auth/signup-form.tsx
Normal file
159
src/components/auth/signup-form.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Eye, EyeOff, Loader2 } from "lucide-react"
|
||||
import { useAuth } from "@/contexts/auth-context"
|
||||
|
||||
interface SignUpFormProps {
|
||||
onToggleMode: () => void
|
||||
}
|
||||
|
||||
export function SignUpForm({ onToggleMode }: SignUpFormProps) {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
})
|
||||
|
||||
const { signup } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
setError("Passwords don't match")
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const success = await signup(formData.name, formData.email, formData.password)
|
||||
if (success) {
|
||||
router.push("/")
|
||||
} else {
|
||||
setError("Failed to create account. Please try again.")
|
||||
}
|
||||
} catch (err) {
|
||||
setError("An error occurred. Please try again.")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-2xl font-bold">Sign Up</CardTitle>
|
||||
<CardDescription>Create your account to get started</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Full Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Enter your full name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="Create a password"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
placeholder="Confirm your password"
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
>
|
||||
{showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="text-red-600 text-sm text-center bg-red-50 p-2 rounded-md">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full cursor-pointer" disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Creating Account...
|
||||
</>
|
||||
) : (
|
||||
"Sign Up"
|
||||
)}
|
||||
</Button>
|
||||
<div className="text-center">
|
||||
<Button type="button" variant="link" onClick={onToggleMode} className="text-sm cursor-pointer">
|
||||
Already have an account? Sign in
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
546
src/components/business-context/business-context-generator.tsx
Normal file
546
src/components/business-context/business-context-generator.tsx
Normal file
@ -0,0 +1,546 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Users, Server, DollarSign, Shield } from "lucide-react"
|
||||
|
||||
interface BusinessContext {
|
||||
userScale: {
|
||||
expectedUsers: string
|
||||
growthRate: string
|
||||
peakTraffic: string
|
||||
globalReach: boolean
|
||||
}
|
||||
technical: {
|
||||
performance: string
|
||||
availability: string
|
||||
security: string[]
|
||||
integrations: string[]
|
||||
}
|
||||
business: {
|
||||
model: string
|
||||
revenue: string
|
||||
budget: string
|
||||
timeline: string
|
||||
}
|
||||
operational: {
|
||||
team: string
|
||||
maintenance: string
|
||||
monitoring: string[]
|
||||
compliance: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export default function BusinessContextGenerator() {
|
||||
const [currentSection, setCurrentSection] = useState(0)
|
||||
const [context, setContext] = useState<BusinessContext>({
|
||||
userScale: {
|
||||
expectedUsers: "",
|
||||
growthRate: "",
|
||||
peakTraffic: "",
|
||||
globalReach: false,
|
||||
},
|
||||
technical: {
|
||||
performance: "",
|
||||
availability: "",
|
||||
security: [],
|
||||
integrations: [],
|
||||
},
|
||||
business: {
|
||||
model: "",
|
||||
revenue: "",
|
||||
budget: "",
|
||||
timeline: "",
|
||||
},
|
||||
operational: {
|
||||
team: "",
|
||||
maintenance: "",
|
||||
monitoring: [],
|
||||
compliance: [],
|
||||
},
|
||||
})
|
||||
|
||||
const sections = [
|
||||
{ title: "User Scale & Growth", icon: Users, color: "bg-blue-500" },
|
||||
{ title: "Technical Requirements", icon: Server, color: "bg-green-500" },
|
||||
{ title: "Business Model", icon: DollarSign, color: "bg-purple-500" },
|
||||
{ title: "Operational Context", icon: Shield, color: "bg-orange-500" },
|
||||
]
|
||||
|
||||
const progress = ((currentSection + 1) / sections.length) * 100
|
||||
|
||||
const updateContext = (section: keyof BusinessContext, field: string, value: any) => {
|
||||
setContext((prev) => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section],
|
||||
[field]: value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
const toggleArrayValue = (section: keyof BusinessContext, field: string, value: string) => {
|
||||
setContext((prev) => {
|
||||
const currentArray = (prev[section] as any)[field] || []
|
||||
const newArray = currentArray.includes(value)
|
||||
? currentArray.filter((item: string) => item !== value)
|
||||
: [...currentArray, value]
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section],
|
||||
[field]: newArray,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const renderUserScaleSection = () => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label htmlFor="expectedUsers">Expected Number of Users</Label>
|
||||
<Select
|
||||
value={context.userScale.expectedUsers}
|
||||
onValueChange={(value) => updateContext("userScale", "expectedUsers", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select user range" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1-100">1-100 users</SelectItem>
|
||||
<SelectItem value="100-1000">100-1,000 users</SelectItem>
|
||||
<SelectItem value="1000-10000">1,000-10,000 users</SelectItem>
|
||||
<SelectItem value="10000-100000">10,000-100,000 users</SelectItem>
|
||||
<SelectItem value="100000+">100,000+ users</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="growthRate">Expected Growth Rate</Label>
|
||||
<Select
|
||||
value={context.userScale.growthRate}
|
||||
onValueChange={(value) => updateContext("userScale", "growthRate", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select growth rate" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="slow">Slow (10-25% annually)</SelectItem>
|
||||
<SelectItem value="moderate">Moderate (25-50% annually)</SelectItem>
|
||||
<SelectItem value="fast">Fast (50-100% annually)</SelectItem>
|
||||
<SelectItem value="explosive">Explosive (100%+ annually)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="peakTraffic">Peak Traffic Expectations</Label>
|
||||
<Select
|
||||
value={context.userScale.peakTraffic}
|
||||
onValueChange={(value) => updateContext("userScale", "peakTraffic", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select peak traffic" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="2x">2x normal traffic</SelectItem>
|
||||
<SelectItem value="5x">5x normal traffic</SelectItem>
|
||||
<SelectItem value="10x">10x normal traffic</SelectItem>
|
||||
<SelectItem value="50x">50x+ normal traffic</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="globalReach"
|
||||
checked={context.userScale.globalReach}
|
||||
onCheckedChange={(checked) => updateContext("userScale", "globalReach", checked)}
|
||||
/>
|
||||
<Label htmlFor="globalReach">Global user base (multiple regions)</Label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderTechnicalSection = () => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label>Performance Requirements</Label>
|
||||
<RadioGroup
|
||||
value={context.technical.performance}
|
||||
onValueChange={(value) => updateContext("technical", "performance", value)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="basic" id="perf-basic" />
|
||||
<Label htmlFor="perf-basic">Basic (3-5s load time)</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="good" id="perf-good" />
|
||||
<Label htmlFor="perf-good">Good (1-3s load time)</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="excellent" id="perf-excellent" />
|
||||
<Label htmlFor="perf-excellent">Excellent (<1s load time)</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Availability Requirements</Label>
|
||||
<RadioGroup
|
||||
value={context.technical.availability}
|
||||
onValueChange={(value) => updateContext("technical", "availability", value)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="99" id="avail-99" />
|
||||
<Label htmlFor="avail-99">99% uptime (8.76 hours downtime/year)</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="99.9" id="avail-999" />
|
||||
<Label htmlFor="avail-999">99.9% uptime (8.76 hours downtime/year)</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="99.99" id="avail-9999" />
|
||||
<Label htmlFor="avail-9999">99.99% uptime (52.56 minutes downtime/year)</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Security Requirements</Label>
|
||||
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||
{[
|
||||
"Authentication",
|
||||
"Authorization",
|
||||
"Data Encryption",
|
||||
"GDPR Compliance",
|
||||
"SOC2 Compliance",
|
||||
"PCI Compliance",
|
||||
].map((security) => (
|
||||
<div key={security} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={security}
|
||||
checked={context.technical.security.includes(security)}
|
||||
onCheckedChange={() => toggleArrayValue("technical", "security", security)}
|
||||
/>
|
||||
<Label htmlFor={security}>{security}</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Required Integrations</Label>
|
||||
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||
{[
|
||||
"Payment Processing",
|
||||
"Email Service",
|
||||
"SMS Service",
|
||||
"Analytics",
|
||||
"CRM",
|
||||
"Social Media",
|
||||
"File Storage",
|
||||
"CDN",
|
||||
].map((integration) => (
|
||||
<div key={integration} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={integration}
|
||||
checked={context.technical.integrations.includes(integration)}
|
||||
onCheckedChange={() => toggleArrayValue("technical", "integrations", integration)}
|
||||
/>
|
||||
<Label htmlFor={integration}>{integration}</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderBusinessSection = () => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label>Business Model</Label>
|
||||
<RadioGroup value={context.business.model} onValueChange={(value) => updateContext("business", "model", value)}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="saas" id="model-saas" />
|
||||
<Label htmlFor="model-saas">SaaS (Subscription)</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="ecommerce" id="model-ecommerce" />
|
||||
<Label htmlFor="model-ecommerce">E-commerce</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="marketplace" id="model-marketplace" />
|
||||
<Label htmlFor="model-marketplace">Marketplace</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="freemium" id="model-freemium" />
|
||||
<Label htmlFor="model-freemium">Freemium</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="enterprise" id="model-enterprise" />
|
||||
<Label htmlFor="model-enterprise">Enterprise B2B</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="revenue">Expected Revenue (Year 1)</Label>
|
||||
<Select value={context.business.revenue} onValueChange={(value) => updateContext("business", "revenue", value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select revenue range" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0-10k">$0 - $10K</SelectItem>
|
||||
<SelectItem value="10k-100k">$10K - $100K</SelectItem>
|
||||
<SelectItem value="100k-1m">$100K - $1M</SelectItem>
|
||||
<SelectItem value="1m+">$1M+</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="budget">Development Budget</Label>
|
||||
<Select value={context.business.budget} onValueChange={(value) => updateContext("business", "budget", value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select budget range" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0-5k">$0 - $5K</SelectItem>
|
||||
<SelectItem value="5k-25k">$5K - $25K</SelectItem>
|
||||
<SelectItem value="25k-100k">$25K - $100K</SelectItem>
|
||||
<SelectItem value="100k+">$100K+</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="timeline">Launch Timeline</Label>
|
||||
<Select
|
||||
value={context.business.timeline}
|
||||
onValueChange={(value) => updateContext("business", "timeline", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select timeline" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1-3months">1-3 months</SelectItem>
|
||||
<SelectItem value="3-6months">3-6 months</SelectItem>
|
||||
<SelectItem value="6-12months">6-12 months</SelectItem>
|
||||
<SelectItem value="12months+">12+ months</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderOperationalSection = () => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label>Team Size</Label>
|
||||
<RadioGroup
|
||||
value={context.operational.team}
|
||||
onValueChange={(value) => updateContext("operational", "team", value)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="solo" id="team-solo" />
|
||||
<Label htmlFor="team-solo">Solo developer</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="small" id="team-small" />
|
||||
<Label htmlFor="team-small">Small team (2-5 people)</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="medium" id="team-medium" />
|
||||
<Label htmlFor="team-medium">Medium team (5-15 people)</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="large" id="team-large" />
|
||||
<Label htmlFor="team-large">Large team (15+ people)</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Maintenance Approach</Label>
|
||||
<RadioGroup
|
||||
value={context.operational.maintenance}
|
||||
onValueChange={(value) => updateContext("operational", "maintenance", value)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="minimal" id="maint-minimal" />
|
||||
<Label htmlFor="maint-minimal">Minimal maintenance</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="regular" id="maint-regular" />
|
||||
<Label htmlFor="maint-regular">Regular updates</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="continuous" id="maint-continuous" />
|
||||
<Label htmlFor="maint-continuous">Continuous deployment</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Monitoring Requirements</Label>
|
||||
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||
{[
|
||||
"Error Tracking",
|
||||
"Performance Monitoring",
|
||||
"User Analytics",
|
||||
"Security Monitoring",
|
||||
"Uptime Monitoring",
|
||||
"Log Management",
|
||||
].map((monitoring) => (
|
||||
<div key={monitoring} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={monitoring}
|
||||
checked={context.operational.monitoring.includes(monitoring)}
|
||||
onCheckedChange={() => toggleArrayValue("operational", "monitoring", monitoring)}
|
||||
/>
|
||||
<Label htmlFor={monitoring}>{monitoring}</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Compliance Requirements</Label>
|
||||
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||
{["GDPR", "CCPA", "HIPAA", "SOX", "PCI DSS", "ISO 27001"].map((compliance) => (
|
||||
<div key={compliance} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={compliance}
|
||||
checked={context.operational.compliance.includes(compliance)}
|
||||
onCheckedChange={() => toggleArrayValue("operational", "compliance", compliance)}
|
||||
/>
|
||||
<Label htmlFor={compliance}>{compliance}</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderSection = () => {
|
||||
switch (currentSection) {
|
||||
case 0:
|
||||
return renderUserScaleSection()
|
||||
case 1:
|
||||
return renderTechnicalSection()
|
||||
case 2:
|
||||
return renderBusinessSection()
|
||||
case 3:
|
||||
return renderOperationalSection()
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Business Context Generator</h1>
|
||||
<p className="text-gray-600">
|
||||
Help us understand your business requirements to generate the perfect architecture
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-sm font-medium text-gray-700">Progress</span>
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{currentSection + 1} of {sections.length}
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={progress} className="h-2" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 mb-8">
|
||||
{sections.map((section, index) => {
|
||||
const Icon = section.icon
|
||||
return (
|
||||
<Card
|
||||
key={index}
|
||||
className={`cursor-pointer transition-all ${
|
||||
index === currentSection
|
||||
? "ring-2 ring-blue-500 shadow-lg"
|
||||
: index < currentSection
|
||||
? "bg-green-50 border-green-200"
|
||||
: "hover:shadow-md"
|
||||
}`}
|
||||
onClick={() => setCurrentSection(index)}
|
||||
>
|
||||
<CardContent className="p-4 text-center">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-full ${section.color} flex items-center justify-center mx-auto mb-2`}
|
||||
>
|
||||
<Icon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-sm">{section.title}</h3>
|
||||
{index < currentSection && (
|
||||
<Badge variant="secondary" className="mt-2">
|
||||
Completed
|
||||
</Badge>
|
||||
)}
|
||||
{index === currentSection && <Badge className="mt-2">Current</Badge>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
{React.createElement(sections[currentSection].icon, { className: "w-5 h-5" })}
|
||||
{sections[currentSection].title}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{currentSection === 0 && "Define your expected user base and growth patterns"}
|
||||
{currentSection === 1 && "Specify technical performance and security requirements"}
|
||||
{currentSection === 2 && "Outline your business model and financial expectations"}
|
||||
{currentSection === 3 && "Configure operational and compliance requirements"}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>{renderSection()}</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-between mt-8">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCurrentSection(Math.max(0, currentSection - 1))}
|
||||
disabled={currentSection === 0}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (currentSection < sections.length - 1) {
|
||||
setCurrentSection(currentSection + 1)
|
||||
} else {
|
||||
// Generate architecture
|
||||
console.log("Business Context:", context)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{currentSection === sections.length - 1 ? "Generate Architecture" : "Next"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
684
src/components/features/features-page.tsx
Normal file
684
src/components/features/features-page.tsx
Normal file
@ -0,0 +1,684 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState, useRef } from "react"
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import {
|
||||
Search,
|
||||
Plus,
|
||||
Trash2,
|
||||
GripVertical,
|
||||
Database,
|
||||
Shield,
|
||||
CreditCard,
|
||||
BarChart3,
|
||||
Bell,
|
||||
Mail,
|
||||
Globe,
|
||||
Smartphone,
|
||||
Zap,
|
||||
Settings,
|
||||
FileText,
|
||||
ImageIcon,
|
||||
Video,
|
||||
MessageSquare,
|
||||
Star,
|
||||
Code,
|
||||
Palette,
|
||||
Layers,
|
||||
Save,
|
||||
Download,
|
||||
} from "lucide-react"
|
||||
import { ArrowRight } from "lucide-react" // Added import for ArrowRight
|
||||
|
||||
interface Feature {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
icon: any
|
||||
complexity: number
|
||||
timeImpact: string
|
||||
dependencies: string[]
|
||||
conflicts: string[]
|
||||
techStack: string[]
|
||||
businessQuestions: string[]
|
||||
isCore?: boolean
|
||||
isPopular?: boolean
|
||||
}
|
||||
|
||||
interface SelectedFeature extends Feature {
|
||||
order: number
|
||||
customConfig?: any
|
||||
}
|
||||
|
||||
export function FeaturesPage() {
|
||||
const [activeTab, setActiveTab] = useState("browse")
|
||||
const [selectedCategory, setSelectedCategory] = useState("all")
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [selectedFeatures, setSelectedFeatures] = useState<SelectedFeature[]>([])
|
||||
const [draggedFeature, setDraggedFeature] = useState<Feature | null>(null)
|
||||
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null)
|
||||
const dropZoneRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const features: Feature[] = [
|
||||
// Core Features
|
||||
{
|
||||
id: "user-auth",
|
||||
name: "User Authentication",
|
||||
description: "Secure user registration, login, and session management",
|
||||
category: "core",
|
||||
icon: Shield,
|
||||
complexity: 3,
|
||||
timeImpact: "2-3 days",
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
techStack: ["NextAuth.js", "JWT", "OAuth"],
|
||||
businessQuestions: [
|
||||
"How many users do you expect in the first year?",
|
||||
"Do you need social login (Google, Facebook)?",
|
||||
"Will you have different user roles?",
|
||||
],
|
||||
isCore: true,
|
||||
isPopular: true,
|
||||
},
|
||||
{
|
||||
id: "database",
|
||||
name: "Database Integration",
|
||||
description: "Data storage and management system",
|
||||
category: "core",
|
||||
icon: Database,
|
||||
complexity: 4,
|
||||
timeImpact: "3-5 days",
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
techStack: ["PostgreSQL", "Prisma", "Redis"],
|
||||
businessQuestions: [
|
||||
"What's your expected data volume?",
|
||||
"Do you need real-time data updates?",
|
||||
"What's your data backup strategy?",
|
||||
],
|
||||
isCore: true,
|
||||
isPopular: true,
|
||||
},
|
||||
{
|
||||
id: "api-management",
|
||||
name: "API Management",
|
||||
description: "RESTful API with rate limiting and documentation",
|
||||
category: "core",
|
||||
icon: Code,
|
||||
complexity: 3,
|
||||
timeImpact: "2-4 days",
|
||||
dependencies: ["database"],
|
||||
conflicts: [],
|
||||
techStack: ["Next.js API Routes", "Swagger", "Rate Limiting"],
|
||||
businessQuestions: [
|
||||
"Will you have external API integrations?",
|
||||
"Do you need API versioning?",
|
||||
"What's your expected API call volume?",
|
||||
],
|
||||
isCore: true,
|
||||
},
|
||||
|
||||
// Business Features
|
||||
{
|
||||
id: "payment-processing",
|
||||
name: "Payment Processing",
|
||||
description: "Secure payment handling with multiple providers",
|
||||
category: "business",
|
||||
icon: CreditCard,
|
||||
complexity: 5,
|
||||
timeImpact: "1-2 weeks",
|
||||
dependencies: ["user-auth", "database"],
|
||||
conflicts: [],
|
||||
techStack: ["Stripe", "PayPal", "Webhook Handling"],
|
||||
businessQuestions: [
|
||||
"What payment methods do you need?",
|
||||
"What's your expected transaction volume?",
|
||||
"Do you need subscription billing?",
|
||||
],
|
||||
isPopular: true,
|
||||
},
|
||||
{
|
||||
id: "analytics",
|
||||
name: "Analytics & Tracking",
|
||||
description: "User behavior tracking and business metrics",
|
||||
category: "business",
|
||||
icon: BarChart3,
|
||||
complexity: 3,
|
||||
timeImpact: "2-3 days",
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
techStack: ["Google Analytics", "Mixpanel", "Custom Events"],
|
||||
businessQuestions: [
|
||||
"What metrics are most important to track?",
|
||||
"Do you need real-time analytics?",
|
||||
"What's your privacy policy regarding data?",
|
||||
],
|
||||
isPopular: true,
|
||||
},
|
||||
{
|
||||
id: "notifications",
|
||||
name: "Notification System",
|
||||
description: "Email, SMS, and push notifications",
|
||||
category: "business",
|
||||
icon: Bell,
|
||||
complexity: 4,
|
||||
timeImpact: "3-5 days",
|
||||
dependencies: ["user-auth"],
|
||||
conflicts: [],
|
||||
techStack: ["SendGrid", "Twilio", "Push API"],
|
||||
businessQuestions: [
|
||||
"What types of notifications do you need?",
|
||||
"How frequently will you send notifications?",
|
||||
"Do you need notification preferences?",
|
||||
],
|
||||
},
|
||||
|
||||
// UI/UX Features
|
||||
{
|
||||
id: "responsive-design",
|
||||
name: "Responsive Design",
|
||||
description: "Mobile-first responsive layout",
|
||||
category: "ui",
|
||||
icon: Smartphone,
|
||||
complexity: 2,
|
||||
timeImpact: "1-2 days",
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
techStack: ["Tailwind CSS", "CSS Grid", "Flexbox"],
|
||||
businessQuestions: ["What devices will your users primarily use?", "Do you need a mobile app later?"],
|
||||
isCore: true,
|
||||
},
|
||||
{
|
||||
id: "dark-mode",
|
||||
name: "Dark Mode",
|
||||
description: "Toggle between light and dark themes",
|
||||
category: "ui",
|
||||
icon: Palette,
|
||||
complexity: 2,
|
||||
timeImpact: "1 day",
|
||||
dependencies: ["responsive-design"],
|
||||
conflicts: [],
|
||||
techStack: ["CSS Variables", "Theme Provider"],
|
||||
businessQuestions: ["Is this important for your user base?"],
|
||||
},
|
||||
{
|
||||
id: "animations",
|
||||
name: "Animations & Transitions",
|
||||
description: "Smooth animations and micro-interactions",
|
||||
category: "ui",
|
||||
icon: Zap,
|
||||
complexity: 3,
|
||||
timeImpact: "2-3 days",
|
||||
dependencies: ["responsive-design"],
|
||||
conflicts: [],
|
||||
techStack: ["Framer Motion", "CSS Animations"],
|
||||
businessQuestions: ["What's your performance priority?"],
|
||||
},
|
||||
|
||||
// Content Features
|
||||
{
|
||||
id: "content-management",
|
||||
name: "Content Management",
|
||||
description: "CMS for managing dynamic content",
|
||||
category: "content",
|
||||
icon: FileText,
|
||||
complexity: 4,
|
||||
timeImpact: "1 week",
|
||||
dependencies: ["database"],
|
||||
conflicts: [],
|
||||
techStack: ["Sanity", "Strapi", "MDX"],
|
||||
businessQuestions: ["Who will manage content?", "How often will content change?"],
|
||||
},
|
||||
{
|
||||
id: "media-upload",
|
||||
name: "Media Upload",
|
||||
description: "File and image upload with optimization",
|
||||
category: "content",
|
||||
icon: ImageIcon, // Updated from Image to ImageIcon
|
||||
complexity: 3,
|
||||
timeImpact: "2-3 days",
|
||||
dependencies: ["database"],
|
||||
conflicts: [],
|
||||
techStack: ["Cloudinary", "AWS S3", "Image Optimization"],
|
||||
businessQuestions: ["What file types do you need?", "What's your storage budget?"],
|
||||
},
|
||||
{
|
||||
id: "video-streaming",
|
||||
name: "Video Streaming",
|
||||
description: "Video upload and streaming capabilities",
|
||||
category: "content",
|
||||
icon: Video,
|
||||
complexity: 5,
|
||||
timeImpact: "1-2 weeks",
|
||||
dependencies: ["media-upload"],
|
||||
conflicts: [],
|
||||
techStack: ["Vimeo API", "YouTube API", "Video.js"],
|
||||
businessQuestions: ["What video quality do you need?", "Expected video volume?"],
|
||||
},
|
||||
|
||||
// Communication Features
|
||||
{
|
||||
id: "chat-system",
|
||||
name: "Real-time Chat",
|
||||
description: "Live messaging and communication",
|
||||
category: "communication",
|
||||
icon: MessageSquare,
|
||||
complexity: 4,
|
||||
timeImpact: "1 week",
|
||||
dependencies: ["user-auth", "database"],
|
||||
conflicts: [],
|
||||
techStack: ["WebSockets", "Socket.io", "Real-time DB"],
|
||||
businessQuestions: ["How many concurrent users?", "Do you need group chats?"],
|
||||
},
|
||||
{
|
||||
id: "email-integration",
|
||||
name: "Email Integration",
|
||||
description: "Email sending and template management",
|
||||
category: "communication",
|
||||
icon: Mail,
|
||||
complexity: 3,
|
||||
timeImpact: "2-3 days",
|
||||
dependencies: [],
|
||||
conflicts: [],
|
||||
techStack: ["SendGrid", "Mailgun", "Email Templates"],
|
||||
businessQuestions: ["What types of emails will you send?", "Expected email volume?"],
|
||||
},
|
||||
|
||||
// Advanced Features
|
||||
{
|
||||
id: "ai-integration",
|
||||
name: "AI Integration",
|
||||
description: "Machine learning and AI-powered features",
|
||||
category: "advanced",
|
||||
icon: Layers,
|
||||
complexity: 5,
|
||||
timeImpact: "1-3 weeks",
|
||||
dependencies: ["api-management"],
|
||||
conflicts: [],
|
||||
techStack: ["OpenAI API", "TensorFlow", "Custom Models"],
|
||||
businessQuestions: ["What AI capabilities do you need?", "What's your AI budget?", "Do you have training data?"],
|
||||
},
|
||||
{
|
||||
id: "real-time",
|
||||
name: "Real-time Features",
|
||||
description: "Live updates and collaboration",
|
||||
category: "advanced",
|
||||
icon: Zap,
|
||||
complexity: 4,
|
||||
timeImpact: "1 week",
|
||||
dependencies: ["database"],
|
||||
conflicts: [],
|
||||
techStack: ["WebSockets", "Socket.io", "Redis Pub/Sub"],
|
||||
businessQuestions: ["How many concurrent users?", "What needs to be real-time?"],
|
||||
},
|
||||
]
|
||||
|
||||
const categories = [
|
||||
{ id: "all", name: "All Features", icon: Globe, count: features.length },
|
||||
{ id: "core", name: "Core", icon: Settings, count: features.filter((f) => f.category === "core").length },
|
||||
{
|
||||
id: "business",
|
||||
name: "Business",
|
||||
icon: BarChart3,
|
||||
count: features.filter((f) => f.category === "business").length,
|
||||
},
|
||||
{ id: "ui", name: "UI/UX", icon: Palette, count: features.filter((f) => f.category === "ui").length },
|
||||
{ id: "content", name: "Content", icon: FileText, count: features.filter((f) => f.category === "content").length },
|
||||
{
|
||||
id: "communication",
|
||||
name: "Communication",
|
||||
icon: MessageSquare,
|
||||
count: features.filter((f) => f.category === "communication").length,
|
||||
},
|
||||
{
|
||||
id: "advanced",
|
||||
name: "Advanced",
|
||||
icon: Layers,
|
||||
count: features.filter((f) => f.category === "advanced").length,
|
||||
},
|
||||
]
|
||||
|
||||
const filteredFeatures = features.filter((feature) => {
|
||||
const matchesCategory = selectedCategory === "all" || feature.category === selectedCategory
|
||||
const matchesSearch =
|
||||
feature.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
feature.description.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
const notAlreadySelected = !selectedFeatures.some((sf) => sf.id === feature.id)
|
||||
return matchesCategory && matchesSearch && notAlreadySelected
|
||||
})
|
||||
|
||||
const getComplexityColor = (complexity: number) => {
|
||||
if (complexity <= 2) return "bg-green-100 text-green-800"
|
||||
if (complexity <= 3) return "bg-yellow-100 text-yellow-800"
|
||||
return "bg-red-100 text-red-800"
|
||||
}
|
||||
|
||||
const getComplexityLabel = (complexity: number) => {
|
||||
if (complexity <= 2) return "Simple"
|
||||
if (complexity <= 3) return "Moderate"
|
||||
return "Complex"
|
||||
}
|
||||
|
||||
const handleDragStart = (feature: Feature) => {
|
||||
setDraggedFeature(feature)
|
||||
}
|
||||
|
||||
const handleDragOver = (e: React.DragEvent, index?: number) => {
|
||||
e.preventDefault()
|
||||
if (typeof index === "number") {
|
||||
setDragOverIndex(index)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDrop = (e: React.DragEvent, index?: number) => {
|
||||
e.preventDefault()
|
||||
if (!draggedFeature) return
|
||||
|
||||
const newFeature: SelectedFeature = {
|
||||
...draggedFeature,
|
||||
order: typeof index === "number" ? index : selectedFeatures.length,
|
||||
}
|
||||
|
||||
if (typeof index === "number") {
|
||||
const newFeatures = [...selectedFeatures]
|
||||
newFeatures.splice(index, 0, newFeature)
|
||||
// Update order for all features
|
||||
newFeatures.forEach((f, i) => (f.order = i))
|
||||
setSelectedFeatures(newFeatures)
|
||||
} else {
|
||||
setSelectedFeatures([...selectedFeatures, newFeature])
|
||||
}
|
||||
|
||||
setDraggedFeature(null)
|
||||
setDragOverIndex(null)
|
||||
}
|
||||
|
||||
const removeFeature = (featureId: string) => {
|
||||
setSelectedFeatures(selectedFeatures.filter((f) => f.id !== featureId))
|
||||
}
|
||||
|
||||
const reorderFeatures = (fromIndex: number, toIndex: number) => {
|
||||
const newFeatures = [...selectedFeatures]
|
||||
const [movedFeature] = newFeatures.splice(fromIndex, 1)
|
||||
newFeatures.splice(toIndex, 0, movedFeature)
|
||||
// Update order for all features
|
||||
newFeatures.forEach((f, i) => (f.order = i))
|
||||
setSelectedFeatures(newFeatures)
|
||||
}
|
||||
|
||||
const calculateTotalComplexity = () => {
|
||||
return selectedFeatures.reduce((total, feature) => total + feature.complexity, 0)
|
||||
}
|
||||
|
||||
const calculateEstimatedTime = () => {
|
||||
const totalDays = selectedFeatures.reduce((total, feature) => {
|
||||
const days = feature.timeImpact.match(/(\d+)/g)?.map(Number) || [1]
|
||||
return total + Math.max(...days)
|
||||
}, 0)
|
||||
|
||||
if (totalDays < 7) return `${totalDays} days`
|
||||
if (totalDays < 30) return `${Math.ceil(totalDays / 7)} weeks`
|
||||
return `${Math.ceil(totalDays / 30)} months`
|
||||
}
|
||||
|
||||
const FeatureCard = ({ feature, isDraggable = true }: { feature: Feature; isDraggable?: boolean }) => {
|
||||
const Icon = feature.icon
|
||||
return (
|
||||
<Card
|
||||
className={`group hover:shadow-lg transition-all duration-300 border-2 hover:border-blue-200 ${
|
||||
isDraggable ? "cursor-grab active:cursor-grabbing" : ""
|
||||
}`}
|
||||
draggable={isDraggable}
|
||||
onDragStart={() => isDraggable && handleDragStart(feature)}
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 bg-blue-100 rounded-lg">
|
||||
<Icon className="h-5 w-5 text-blue-600" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-lg group-hover:text-blue-600 transition-colors">{feature.name}</CardTitle>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge className={getComplexityColor(feature.complexity)}>
|
||||
{getComplexityLabel(feature.complexity)}
|
||||
</Badge>
|
||||
{feature.isCore && <Badge variant="secondary">Core</Badge>}
|
||||
{feature.isPopular && (
|
||||
<Badge variant="outline" className="text-yellow-600 border-yellow-300">
|
||||
<Star className="h-3 w-3 mr-1 fill-current" />
|
||||
Popular
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isDraggable && <GripVertical className="h-5 w-5 text-gray-400 opacity-0 group-hover:opacity-100" />}
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm mt-2">{feature.description}</p>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex items-center justify-between text-sm text-gray-500">
|
||||
<span>Time Impact: {feature.timeImpact}</span>
|
||||
<span>Complexity: {feature.complexity}/5</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-sm text-gray-700 mb-2">Tech Stack:</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{feature.techStack.slice(0, 3).map((tech, index) => (
|
||||
<Badge key={index} variant="outline" className="text-xs">
|
||||
{tech}
|
||||
</Badge>
|
||||
))}
|
||||
{feature.techStack.length > 3 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{feature.techStack.length - 3}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{feature.dependencies.length > 0 && (
|
||||
<div className="p-2 bg-yellow-50 border border-yellow-200 rounded text-sm">
|
||||
<span className="font-medium text-yellow-800">Dependencies:</span>
|
||||
<span className="text-yellow-700 ml-1">
|
||||
{feature.dependencies
|
||||
.map((depId) => {
|
||||
const dep = features.find((f) => f.id === depId)
|
||||
return dep?.name
|
||||
})
|
||||
.join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const SelectedFeatureCard = ({ feature, index }: { feature: SelectedFeature; index: number }) => {
|
||||
const Icon = feature.icon
|
||||
return (
|
||||
<Card
|
||||
className="group border-2 border-blue-200 bg-blue-50 hover:shadow-lg transition-all duration-300"
|
||||
draggable
|
||||
onDragStart={() => handleDragStart(feature)}
|
||||
onDragOver={(e) => handleDragOver(e, index)}
|
||||
onDrop={(e) => handleDrop(e, index)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<GripVertical className="h-4 w-4 text-gray-400 cursor-grab" />
|
||||
<div className="p-2 bg-blue-200 rounded-lg">
|
||||
<Icon className="h-4 w-4 text-blue-700" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-blue-900">{feature.name}</h4>
|
||||
<p className="text-sm text-blue-700">{feature.timeImpact}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge className={getComplexityColor(feature.complexity)}>{feature.complexity}</Badge>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => removeFeature(feature.id)}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Features Library */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold text-gray-900">Features Library</h2>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Search features..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 w-80"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Filters */}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{categories.map((category) => {
|
||||
const Icon = category.icon
|
||||
return (
|
||||
<button
|
||||
key={category.id}
|
||||
onClick={() => setSelectedCategory(category.id)}
|
||||
className={`flex items-center space-x-2 px-4 py-2 rounded-lg border transition-all ${
|
||||
selectedCategory === category.id
|
||||
? "bg-blue-600 text-white border-blue-600"
|
||||
: "bg-white text-gray-700 border-gray-200 hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="font-medium">{category.name}</span>
|
||||
<Badge variant="secondary" className="ml-1">
|
||||
{category.count}
|
||||
</Badge>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Features Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{filteredFeatures.map((feature) => (
|
||||
<FeatureCard key={feature.id} feature={feature} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selected Features Panel */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card className="sticky top-8">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>Selected Features</span>
|
||||
<Badge variant="secondary">{selectedFeatures.length}</Badge>
|
||||
</CardTitle>
|
||||
<p className="text-sm text-gray-600">Drag features here to build your project</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Project Summary */}
|
||||
<div className="p-4 bg-gray-50 rounded-lg space-y-3">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">Total Complexity:</span>
|
||||
<Badge
|
||||
variant={
|
||||
calculateTotalComplexity() > 20
|
||||
? "destructive"
|
||||
: calculateTotalComplexity() > 15
|
||||
? "default"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
{calculateTotalComplexity()}/50
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">Estimated Time:</span>
|
||||
<span className="font-medium">{calculateEstimatedTime()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Drop Zone */}
|
||||
<div
|
||||
ref={dropZoneRef}
|
||||
className={`min-h-[200px] border-2 border-dashed rounded-lg p-4 transition-all ${
|
||||
draggedFeature
|
||||
? "border-blue-400 bg-blue-50"
|
||||
: selectedFeatures.length === 0
|
||||
? "border-gray-300"
|
||||
: "border-transparent"
|
||||
}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{selectedFeatures.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<Plus className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-500 text-sm">
|
||||
Drag features from the library to start building your project
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{selectedFeatures
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map((feature, index) => (
|
||||
<SelectedFeatureCard key={feature.id} feature={feature} index={index} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
{selectedFeatures.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Button className="w-full bg-blue-600 hover:bg-blue-700">
|
||||
Continue to Business Context
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full bg-transparent" onClick={() => setSelectedFeatures([])}>
|
||||
Clear All Features
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
49
src/components/layout/app-layout.tsx
Normal file
49
src/components/layout/app-layout.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
"use client"
|
||||
|
||||
import { useAuth } from "@/contexts/auth-context"
|
||||
import Header from "@/components/navigation/header"
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
interface AppLayoutProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function AppLayout({ children }: AppLayoutProps) {
|
||||
const { isAuthenticated, isLoading } = useAuth()
|
||||
const pathname = usePathname()
|
||||
|
||||
// Don't show header on auth pages
|
||||
const isAuthPage = pathname === "/auth"
|
||||
|
||||
// Show loading state while checking auth
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// For auth pages, don't show header
|
||||
if (isAuthPage) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
// For authenticated users on other pages, show header
|
||||
if (isAuthenticated) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// For unauthenticated users on non-auth pages, redirect to auth
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
806
src/components/main-dashboard.tsx
Normal file
806
src/components/main-dashboard.tsx
Normal file
@ -0,0 +1,806 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers } from "lucide-react"
|
||||
|
||||
interface Template {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
category: string
|
||||
features: string[]
|
||||
complexity: number
|
||||
timeEstimate: string
|
||||
techStack: string[]
|
||||
popularity?: number
|
||||
lastUpdated?: string
|
||||
}
|
||||
|
||||
function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => void }) {
|
||||
const [selectedCategory, setSelectedCategory] = useState("all")
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
|
||||
const templates: Template[] = [
|
||||
// Marketing Templates (10)
|
||||
{
|
||||
id: "marketing-website",
|
||||
title: "Marketing Website",
|
||||
description: "Professional marketing site with CMS and lead generation",
|
||||
category: "marketing",
|
||||
features: ["Content Management", "Contact Forms", "SEO Optimization", "Analytics Integration"],
|
||||
complexity: 2,
|
||||
timeEstimate: "1-2 weeks",
|
||||
techStack: ["Next.js", "Sanity CMS", "Tailwind CSS", "Vercel"],
|
||||
popularity: 95,
|
||||
lastUpdated: "2024-01-15",
|
||||
},
|
||||
{
|
||||
id: "landing-page",
|
||||
title: "Landing Page",
|
||||
description: "High-converting landing page with A/B testing capabilities",
|
||||
category: "marketing",
|
||||
features: ["A/B Testing", "Conversion Tracking", "Lead Capture", "Mobile Optimization"],
|
||||
complexity: 2,
|
||||
timeEstimate: "3-5 days",
|
||||
techStack: ["Next.js", "Tailwind CSS", "Google Analytics", "Mailchimp"],
|
||||
popularity: 88,
|
||||
lastUpdated: "2024-01-10",
|
||||
},
|
||||
{
|
||||
id: "blog-platform",
|
||||
title: "Blog Platform",
|
||||
description: "Content-rich blog with SEO optimization and social sharing",
|
||||
category: "marketing",
|
||||
features: ["Content Management", "SEO Tools", "Social Sharing", "Comment System"],
|
||||
complexity: 3,
|
||||
timeEstimate: "1-2 weeks",
|
||||
techStack: ["Next.js", "MDX", "Tailwind CSS", "Disqus"],
|
||||
popularity: 82,
|
||||
lastUpdated: "2024-01-12",
|
||||
},
|
||||
{
|
||||
id: "portfolio-site",
|
||||
title: "Portfolio Website",
|
||||
description: "Personal or agency portfolio with project showcase",
|
||||
category: "marketing",
|
||||
features: ["Project Gallery", "Contact Forms", "Blog", "Responsive Design"],
|
||||
complexity: 2,
|
||||
timeEstimate: "1-2 weeks",
|
||||
techStack: ["Next.js", "MDX", "Tailwind CSS", "Framer Motion"],
|
||||
popularity: 79,
|
||||
lastUpdated: "2024-01-08",
|
||||
},
|
||||
{
|
||||
id: "agency-website",
|
||||
title: "Agency Website",
|
||||
description: "Full-service agency site with team profiles and case studies",
|
||||
category: "marketing",
|
||||
features: ["Team Profiles", "Case Studies", "Service Pages", "Client Testimonials"],
|
||||
complexity: 3,
|
||||
timeEstimate: "2-3 weeks",
|
||||
techStack: ["Next.js", "Strapi", "Tailwind CSS", "Framer Motion"],
|
||||
popularity: 76,
|
||||
lastUpdated: "2024-01-14",
|
||||
},
|
||||
{
|
||||
id: "event-website",
|
||||
title: "Event Website",
|
||||
description: "Event promotion site with registration and ticketing",
|
||||
category: "marketing",
|
||||
features: ["Event Registration", "Ticketing", "Speaker Profiles", "Schedule Management"],
|
||||
complexity: 4,
|
||||
timeEstimate: "2-3 weeks",
|
||||
techStack: ["Next.js", "Stripe", "Calendar API", "Email Integration"],
|
||||
popularity: 73,
|
||||
lastUpdated: "2024-01-11",
|
||||
},
|
||||
{
|
||||
id: "restaurant-website",
|
||||
title: "Restaurant Website",
|
||||
description: "Restaurant site with menu, reservations, and online ordering",
|
||||
category: "marketing",
|
||||
features: ["Menu Display", "Online Reservations", "Order System", "Location Info"],
|
||||
complexity: 3,
|
||||
timeEstimate: "1-2 weeks",
|
||||
techStack: ["Next.js", "Reservation API", "Payment Processing", "Google Maps"],
|
||||
popularity: 70,
|
||||
lastUpdated: "2024-01-09",
|
||||
},
|
||||
{
|
||||
id: "nonprofit-website",
|
||||
title: "Nonprofit Website",
|
||||
description: "Nonprofit organization site with donation and volunteer management",
|
||||
category: "marketing",
|
||||
features: ["Donation Processing", "Volunteer Registration", "Event Management", "Impact Tracking"],
|
||||
complexity: 3,
|
||||
timeEstimate: "2-3 weeks",
|
||||
techStack: ["Next.js", "Stripe", "Volunteer API", "Analytics"],
|
||||
popularity: 67,
|
||||
lastUpdated: "2024-01-13",
|
||||
},
|
||||
{
|
||||
id: "real-estate-website",
|
||||
title: "Real Estate Website",
|
||||
description: "Property listing site with search and contact features",
|
||||
category: "marketing",
|
||||
features: ["Property Listings", "Search Filters", "Contact Forms", "Virtual Tours"],
|
||||
complexity: 4,
|
||||
timeEstimate: "2-4 weeks",
|
||||
techStack: ["Next.js", "Property API", "Map Integration", "Image Gallery"],
|
||||
popularity: 64,
|
||||
lastUpdated: "2024-01-07",
|
||||
},
|
||||
{
|
||||
id: "personal-brand",
|
||||
title: "Personal Brand Site",
|
||||
description: "Personal branding site for professionals and creators",
|
||||
category: "marketing",
|
||||
features: ["About Page", "Services", "Testimonials", "Contact Integration"],
|
||||
complexity: 2,
|
||||
timeEstimate: "1 week",
|
||||
techStack: ["Next.js", "Tailwind CSS", "Contact Forms", "Social Links"],
|
||||
popularity: 61,
|
||||
lastUpdated: "2024-01-06",
|
||||
},
|
||||
|
||||
// Software Templates (10)
|
||||
{
|
||||
id: "saas-platform",
|
||||
title: "SaaS Platform",
|
||||
description: "Complete SaaS application with user management, billing, and analytics",
|
||||
category: "software",
|
||||
features: ["User Authentication", "Payment Processing", "Analytics Integration", "API Management"],
|
||||
complexity: 5,
|
||||
timeEstimate: "4-6 weeks",
|
||||
techStack: ["Next.js", "PostgreSQL", "Stripe", "NextAuth.js"],
|
||||
popularity: 92,
|
||||
lastUpdated: "2024-01-15",
|
||||
},
|
||||
{
|
||||
id: "dashboard-app",
|
||||
title: "Analytics Dashboard",
|
||||
description: "Data visualization dashboard with real-time updates",
|
||||
category: "software",
|
||||
features: ["Data Visualization", "Real-time Updates", "User Authentication", "Export Features"],
|
||||
complexity: 4,
|
||||
timeEstimate: "3-4 weeks",
|
||||
techStack: ["Next.js", "Chart.js", "WebSockets", "PostgreSQL"],
|
||||
popularity: 89,
|
||||
lastUpdated: "2024-01-14",
|
||||
},
|
||||
{
|
||||
id: "mobile-app",
|
||||
title: "Mobile App (PWA)",
|
||||
description: "Progressive web app with mobile-first design",
|
||||
category: "software",
|
||||
features: ["Offline Support", "Push Notifications", "Mobile Optimization", "App Install"],
|
||||
complexity: 4,
|
||||
timeEstimate: "2-3 weeks",
|
||||
techStack: ["Next.js", "PWA", "Service Workers", "Push API"],
|
||||
popularity: 86,
|
||||
lastUpdated: "2024-01-13",
|
||||
},
|
||||
{
|
||||
id: "project-management",
|
||||
title: "Project Management Tool",
|
||||
description: "Team collaboration and project tracking application",
|
||||
category: "software",
|
||||
features: ["Task Management", "Team Collaboration", "Time Tracking", "Reporting"],
|
||||
complexity: 5,
|
||||
timeEstimate: "4-5 weeks",
|
||||
techStack: ["Next.js", "PostgreSQL", "Real-time Updates", "File Upload"],
|
||||
popularity: 83,
|
||||
lastUpdated: "2024-01-12",
|
||||
},
|
||||
{
|
||||
id: "crm-system",
|
||||
title: "CRM System",
|
||||
description: "Customer relationship management with sales pipeline",
|
||||
category: "software",
|
||||
features: ["Contact Management", "Sales Pipeline", "Email Integration", "Reporting"],
|
||||
complexity: 5,
|
||||
timeEstimate: "3-5 weeks",
|
||||
techStack: ["Next.js", "PostgreSQL", "Email API", "Calendar Integration"],
|
||||
popularity: 80,
|
||||
lastUpdated: "2024-01-11",
|
||||
},
|
||||
{
|
||||
id: "inventory-management",
|
||||
title: "Inventory Management",
|
||||
description: "Stock tracking and warehouse management system",
|
||||
category: "software",
|
||||
features: ["Stock Tracking", "Barcode Scanning", "Supplier Management", "Reports"],
|
||||
complexity: 4,
|
||||
timeEstimate: "3-4 weeks",
|
||||
techStack: ["Next.js", "PostgreSQL", "Barcode API", "PDF Generation"],
|
||||
popularity: 77,
|
||||
lastUpdated: "2024-01-10",
|
||||
},
|
||||
{
|
||||
id: "learning-platform",
|
||||
title: "Learning Management System",
|
||||
description: "Online education platform with courses and assessments",
|
||||
category: "software",
|
||||
features: ["Course Management", "Video Streaming", "Assessments", "Progress Tracking"],
|
||||
complexity: 5,
|
||||
timeEstimate: "4-6 weeks",
|
||||
techStack: ["Next.js", "Video API", "PostgreSQL", "Payment Processing"],
|
||||
popularity: 74,
|
||||
lastUpdated: "2024-01-09",
|
||||
},
|
||||
{
|
||||
id: "booking-system",
|
||||
title: "Booking System",
|
||||
description: "Appointment and reservation management platform",
|
||||
category: "software",
|
||||
features: ["Calendar Integration", "Payment Processing", "Notifications", "Customer Management"],
|
||||
complexity: 4,
|
||||
timeEstimate: "2-4 weeks",
|
||||
techStack: ["Next.js", "Calendar API", "Stripe", "Email Integration"],
|
||||
popularity: 71,
|
||||
lastUpdated: "2024-01-08",
|
||||
},
|
||||
{
|
||||
id: "chat-application",
|
||||
title: "Chat Application",
|
||||
description: "Real-time messaging platform with file sharing",
|
||||
category: "software",
|
||||
features: ["Real-time Messaging", "File Sharing", "Group Chats", "User Presence"],
|
||||
complexity: 4,
|
||||
timeEstimate: "2-3 weeks",
|
||||
techStack: ["Next.js", "WebSockets", "File Storage", "Real-time DB"],
|
||||
popularity: 68,
|
||||
lastUpdated: "2024-01-07",
|
||||
},
|
||||
{
|
||||
id: "api-platform",
|
||||
title: "API Platform",
|
||||
description: "RESTful API with documentation and rate limiting",
|
||||
category: "software",
|
||||
features: ["API Documentation", "Rate Limiting", "Authentication", "Monitoring"],
|
||||
complexity: 4,
|
||||
timeEstimate: "2-3 weeks",
|
||||
techStack: ["Next.js", "Swagger", "Redis", "Monitoring Tools"],
|
||||
popularity: 65,
|
||||
lastUpdated: "2024-01-06",
|
||||
},
|
||||
|
||||
// SEO Templates (10)
|
||||
{
|
||||
id: "seo-optimized-blog",
|
||||
title: "SEO-Optimized Blog",
|
||||
description: "Blog platform with advanced SEO features and schema markup",
|
||||
category: "seo",
|
||||
features: ["Schema Markup", "Meta Optimization", "Sitemap Generation", "Performance Optimization"],
|
||||
complexity: 3,
|
||||
timeEstimate: "2-3 weeks",
|
||||
techStack: ["Next.js", "SEO Tools", "Schema.org", "Google Search Console"],
|
||||
popularity: 90,
|
||||
lastUpdated: "2024-01-15",
|
||||
},
|
||||
{
|
||||
id: "local-business-site",
|
||||
title: "Local Business Website",
|
||||
description: "Local SEO optimized site with Google My Business integration",
|
||||
category: "seo",
|
||||
features: ["Local SEO", "Google My Business", "Review Management", "Location Pages"],
|
||||
complexity: 3,
|
||||
timeEstimate: "1-2 weeks",
|
||||
techStack: ["Next.js", "Google APIs", "Review APIs", "Local Schema"],
|
||||
popularity: 87,
|
||||
lastUpdated: "2024-01-14",
|
||||
},
|
||||
{
|
||||
id: "ecommerce-seo",
|
||||
title: "E-commerce SEO Site",
|
||||
description: "Product-focused e-commerce with advanced SEO optimization",
|
||||
category: "seo",
|
||||
features: ["Product Schema", "Category Optimization", "Review Rich Snippets", "Performance"],
|
||||
complexity: 4,
|
||||
timeEstimate: "3-4 weeks",
|
||||
techStack: ["Next.js", "Product APIs", "Review Systems", "CDN"],
|
||||
popularity: 84,
|
||||
lastUpdated: "2024-01-13",
|
||||
},
|
||||
{
|
||||
id: "news-website",
|
||||
title: "News Website",
|
||||
description: "News platform with article SEO and AMP support",
|
||||
category: "seo",
|
||||
features: ["Article Schema", "AMP Support", "Breaking News", "Social Sharing"],
|
||||
complexity: 4,
|
||||
timeEstimate: "2-4 weeks",
|
||||
techStack: ["Next.js", "AMP", "News APIs", "Social APIs"],
|
||||
popularity: 81,
|
||||
lastUpdated: "2024-01-12",
|
||||
},
|
||||
{
|
||||
id: "directory-website",
|
||||
title: "Business Directory",
|
||||
description: "Local business directory with search and listings",
|
||||
category: "seo",
|
||||
features: ["Business Listings", "Search Optimization", "Category Pages", "Review System"],
|
||||
complexity: 4,
|
||||
timeEstimate: "3-4 weeks",
|
||||
techStack: ["Next.js", "Search APIs", "Location Services", "Review APIs"],
|
||||
popularity: 78,
|
||||
lastUpdated: "2024-01-11",
|
||||
},
|
||||
{
|
||||
id: "recipe-website",
|
||||
title: "Recipe Website",
|
||||
description: "Recipe platform with rich snippets and cooking schema",
|
||||
category: "seo",
|
||||
features: ["Recipe Schema", "Nutrition Info", "Cooking Times", "User Ratings"],
|
||||
complexity: 3,
|
||||
timeEstimate: "2-3 weeks",
|
||||
techStack: ["Next.js", "Recipe APIs", "Nutrition APIs", "Rating System"],
|
||||
popularity: 75,
|
||||
lastUpdated: "2024-01-10",
|
||||
},
|
||||
{
|
||||
id: "job-board",
|
||||
title: "Job Board",
|
||||
description: "Job listing platform with structured data for search engines",
|
||||
category: "seo",
|
||||
features: ["Job Schema", "Search Filters", "Application Tracking", "Company Profiles"],
|
||||
complexity: 4,
|
||||
timeEstimate: "3-4 weeks",
|
||||
techStack: ["Next.js", "Job APIs", "Application System", "Company APIs"],
|
||||
popularity: 72,
|
||||
lastUpdated: "2024-01-09",
|
||||
},
|
||||
{
|
||||
id: "review-website",
|
||||
title: "Review Website",
|
||||
description: "Product/service review platform with rich snippets",
|
||||
category: "seo",
|
||||
features: ["Review Schema", "Rating System", "Comparison Tools", "User Profiles"],
|
||||
complexity: 4,
|
||||
timeEstimate: "2-4 weeks",
|
||||
techStack: ["Next.js", "Review APIs", "Rating System", "Comparison Tools"],
|
||||
popularity: 69,
|
||||
lastUpdated: "2024-01-08",
|
||||
},
|
||||
{
|
||||
id: "travel-website",
|
||||
title: "Travel Website",
|
||||
description: "Travel guide with location-based SEO and booking integration",
|
||||
category: "seo",
|
||||
features: ["Location Schema", "Travel Guides", "Booking Integration", "Photo Galleries"],
|
||||
complexity: 4,
|
||||
timeEstimate: "3-4 weeks",
|
||||
techStack: ["Next.js", "Travel APIs", "Booking APIs", "Map Integration"],
|
||||
popularity: 66,
|
||||
lastUpdated: "2024-01-07",
|
||||
},
|
||||
{
|
||||
id: "healthcare-website",
|
||||
title: "Healthcare Website",
|
||||
description: "Medical practice website with health-focused SEO",
|
||||
category: "seo",
|
||||
features: ["Medical Schema", "Appointment Booking", "Health Articles", "Doctor Profiles"],
|
||||
complexity: 3,
|
||||
timeEstimate: "2-3 weeks",
|
||||
techStack: ["Next.js", "Medical APIs", "Booking System", "Content Management"],
|
||||
popularity: 63,
|
||||
lastUpdated: "2024-01-06",
|
||||
},
|
||||
]
|
||||
|
||||
const categories = [
|
||||
{ id: "all", name: "All Templates", icon: Globe, count: templates.length },
|
||||
{
|
||||
id: "marketing",
|
||||
name: "Marketing & Branding",
|
||||
icon: Zap,
|
||||
count: templates.filter((t) => t.category === "marketing").length,
|
||||
},
|
||||
{
|
||||
id: "software",
|
||||
name: "Software & Tools",
|
||||
icon: Code,
|
||||
count: templates.filter((t) => t.category === "software").length,
|
||||
},
|
||||
{ id: "seo", name: "SEO & Content", icon: BarChart3, count: templates.filter((t) => t.category === "seo").length },
|
||||
]
|
||||
|
||||
const filteredTemplates = templates.filter((template) => {
|
||||
const matchesCategory = selectedCategory === "all" || template.category === selectedCategory
|
||||
const matchesSearch =
|
||||
template.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
template.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
template.features.some((feature) => feature.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
return matchesCategory && matchesSearch
|
||||
})
|
||||
|
||||
const getComplexityColor = (complexity: number) => {
|
||||
if (complexity <= 2) return "bg-green-100 text-green-800"
|
||||
if (complexity <= 3) return "bg-yellow-100 text-yellow-800"
|
||||
return "bg-red-100 text-red-800"
|
||||
}
|
||||
|
||||
const getComplexityLabel = (complexity: number) => {
|
||||
if (complexity <= 2) return "Simple"
|
||||
if (complexity <= 3) return "Moderate"
|
||||
return "Complex"
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-3">
|
||||
<h1 className="text-4xl font-bold text-gray-900">Choose Your Project Template</h1>
|
||||
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
Select from our comprehensive library of professionally designed templates
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search and Filter */}
|
||||
<div className="space-y-4">
|
||||
<div className="max-w-2xl mx-auto relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-5 w-5" />
|
||||
<Input
|
||||
placeholder="Search templates, features, or technologies..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 h-12 text-lg border-2 border-gray-200 hover:border-blue-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 rounded-xl shadow-sm transition-all duration-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category Filters */}
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
{categories.map((category) => {
|
||||
const Icon = category.icon
|
||||
return (
|
||||
<button
|
||||
key={category.id}
|
||||
onClick={() => setSelectedCategory(category.id)}
|
||||
className={`flex items-center space-x-3 px-6 py-4 rounded-xl border-2 transition-all duration-300 hover:scale-105 ${
|
||||
selectedCategory === category.id
|
||||
? "bg-gradient-to-r from-blue-600 to-blue-700 text-white border-blue-600 shadow-lg"
|
||||
: "bg-white text-gray-700 border-gray-200 hover:border-blue-300 hover:shadow-md hover:bg-blue-50"
|
||||
}`}
|
||||
>
|
||||
<div className={`p-2 rounded-lg ${
|
||||
selectedCategory === category.id
|
||||
? "bg-white/20"
|
||||
: "bg-blue-100 text-blue-600"
|
||||
}`}>
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-semibold">{category.name}</div>
|
||||
<div className={`text-sm ${selectedCategory === category.id ? "opacity-90" : "opacity-75"}`}>
|
||||
{category.count} templates
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Templates Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredTemplates.map((template) => (
|
||||
<Card
|
||||
key={template.id}
|
||||
className="group cursor-pointer transition-all duration-300 hover:scale-[1.02] bg-white border border-gray-300 hover:border-blue-400 rounded-xl shadow-md hover:shadow-xl overflow-hidden"
|
||||
>
|
||||
{/* Card Header with gradient background */}
|
||||
<div className="bg-white px-4 py-4 border-b border-gray-100">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="space-y-2 flex-1">
|
||||
<CardTitle className="text-xl font-bold text-gray-900 group-hover:text-blue-700 transition-colors line-clamp-2">
|
||||
{template.title}
|
||||
</CardTitle>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge className={`${getComplexityColor(template.complexity)} font-medium px-3 py-1 rounded-full`}>
|
||||
{getComplexityLabel(template.complexity)}
|
||||
</Badge>
|
||||
{template.popularity && (
|
||||
<div className="flex items-center space-x-1 bg-white/70 px-2 py-1 rounded-full">
|
||||
<Star className="h-4 w-4 text-yellow-500 fill-current" />
|
||||
<span className="text-sm font-semibold text-gray-700">{template.popularity}%</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-700 text-sm leading-relaxed font-medium">{template.description}</p>
|
||||
</div>
|
||||
|
||||
<CardContent className="p-4 flex flex-col h-full">
|
||||
<div className="flex-1 space-y-4">
|
||||
{/* Stats Row */}
|
||||
<div className="flex items-center justify-between text-sm text-gray-600 bg-gray-50 px-3 py-2 rounded-lg">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Clock className="h-4 w-4 text-blue-500" />
|
||||
<span className="font-medium">{template.timeEstimate}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Layers className="h-4 w-4 text-green-500" />
|
||||
<span className="font-medium">{template.features.length} features</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features Section */}
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm text-gray-800 mb-1 flex items-center">
|
||||
<span className="w-2 h-2 bg-blue-500 rounded-full mr-2"></span>
|
||||
Key Features
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{template.features.slice(0, 3).map((feature, index) => (
|
||||
<Badge key={index} variant="outline" className="text-xs bg-blue-50 border-blue-200 text-blue-700 px-3 py-2 rounded-full">
|
||||
{feature}
|
||||
</Badge>
|
||||
))}
|
||||
{template.features.length > 3 && (
|
||||
<Badge variant="outline" className="text-xs bg-gray-50 border-gray-200 text-gray-600 px-3 py-2 rounded-full">
|
||||
+{template.features.length - 3} more
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tech Stack Section */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm text-gray-800 mb-1 flex items-center">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
|
||||
Tech Stack
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{template.techStack.slice(0, 3).map((tech, index) => (
|
||||
<Badge key={index} variant="secondary" className="text-xs bg-green-50 text-green-700 px-3 py-1 rounded-full font-medium">
|
||||
{tech}
|
||||
</Badge>
|
||||
))}
|
||||
{template.techStack.length > 3 && (
|
||||
<Badge variant="secondary" className="text-xs bg-gray-50 text-gray-600 px-3 py-1 rounded-full">
|
||||
+{template.techStack.length - 3}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Button - Always at bottom */}
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
onClick={() => onNext(template)}
|
||||
className="w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-semibold py-2 rounded-lg shadow-lg hover:shadow-xl transition-all duration-300 group-hover:shadow-2xl"
|
||||
>
|
||||
<span className="flex items-center justify-center cursor-pointer">
|
||||
Select Template
|
||||
<ArrowRight className="ml-2 h-4 w-4 group-hover:translate-x-1 transition-transform" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Custom Template Option */}
|
||||
<Card className="group border-dashed border-2 border-gray-300 hover:border-blue-400 transition-all duration-300 hover:scale-[1.02] bg-gradient-to-br from-gray-50 to-blue-50 overflow-hidden">
|
||||
<CardContent className="text-center py-16 px-8">
|
||||
<div className="w-20 h-20 bg-gradient-to-br from-blue-100 to-indigo-100 rounded-full flex items-center justify-center mx-auto mb-6 group-hover:from-blue-200 group-hover:to-indigo-200 transition-all duration-300">
|
||||
<Plus className="h-10 w-10 text-blue-600 group-hover:text-blue-700 transition-colors" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-gray-800 mb-3 group-hover:text-blue-700 transition-colors">Create Custom Template</h3>
|
||||
<p className="text-gray-600 mb-8 max-w-md mx-auto text-lg leading-relaxed">
|
||||
Don't see what you need? Create a custom project type with your specific requirements and tech stack.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-2 border-blue-300 text-blue-700 hover:bg-blue-50 bg-white hover:border-blue-400 px-8 py-3 text-lg font-semibold rounded-lg shadow-md hover:shadow-lg transition-all duration-300 group-hover:shadow-xl"
|
||||
>
|
||||
<Plus className="mr-2 h-5 w-5" />
|
||||
Create Custom Template
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Results Summary */}
|
||||
{searchQuery && (
|
||||
<div className="text-center py-4">
|
||||
<p className="text-gray-600">
|
||||
Showing {filteredTemplates.length} template{filteredTemplates.length !== 1 ? "s" : ""}
|
||||
{searchQuery && ` matching "${searchQuery}"`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Feature Selection Step Component
|
||||
function FeatureSelectionStep({
|
||||
template,
|
||||
onNext,
|
||||
onBack,
|
||||
}: { template: Template; onNext: () => void; onBack: () => void }) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto space-y-8">
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-4">
|
||||
<h1 className="text-4xl font-bold text-gray-900">Select Features for {template.title}</h1>
|
||||
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||
Choose the features that best fit your project requirements.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Features List */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{template.features.map((feature, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className="hover:shadow-xl transition-all duration-300 cursor-pointer group border-2 hover:border-blue-200"
|
||||
>
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-xl group-hover:text-blue-600 transition-colors line-clamp-2">
|
||||
{feature}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-gray-600 text-sm leading-relaxed">
|
||||
This feature enhances your project with {feature} capabilities.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
<div className="text-center py-4">
|
||||
<div className="space-x-4">
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={onNext}>Continue</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Main Dashboard Component
|
||||
export function MainDashboard() {
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null)
|
||||
|
||||
const steps = [
|
||||
{ id: 1, name: "Project Type", description: "Choose template" },
|
||||
{ id: 2, name: "Features", description: "Select features" },
|
||||
{ id: 3, name: "Business Context", description: "Define requirements" },
|
||||
{ id: 4, name: "Generate", description: "Create project" },
|
||||
{ id: 5, name: "Architecture", description: "Review & deploy" },
|
||||
]
|
||||
|
||||
const renderStep = () => {
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return (
|
||||
<TemplateSelectionStep
|
||||
onNext={(template) => {
|
||||
setSelectedTemplate(template)
|
||||
setCurrentStep(2)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
case 2:
|
||||
return selectedTemplate ? (
|
||||
<FeatureSelectionStep
|
||||
template={selectedTemplate}
|
||||
onNext={() => setCurrentStep(3)}
|
||||
onBack={() => setCurrentStep(1)}
|
||||
/>
|
||||
) : null
|
||||
case 3:
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<h2 className="text-2xl font-bold mb-4">Business Context Step</h2>
|
||||
<p className="text-gray-600 mb-8">Coming soon - Define your business requirements and scaling needs</p>
|
||||
<div className="space-x-4">
|
||||
<Button variant="outline" onClick={() => setCurrentStep(2)}>
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={() => setCurrentStep(4)}>Continue</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 4:
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<h2 className="text-2xl font-bold mb-4">Generate Step</h2>
|
||||
<p className="text-gray-600 mb-8">Coming soon - Generate your project architecture and code</p>
|
||||
<div className="space-x-4">
|
||||
<Button variant="outline" onClick={() => setCurrentStep(3)}>
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={() => setCurrentStep(5)}>Continue</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 5:
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<h2 className="text-2xl font-bold mb-4">Architecture Step</h2>
|
||||
<p className="text-gray-600 mb-8">Coming soon - Review architecture and deploy your project</p>
|
||||
<div className="space-x-4">
|
||||
<Button variant="outline" onClick={() => setCurrentStep(4)}>
|
||||
Back
|
||||
</Button>
|
||||
<Button>Deploy Project</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
|
||||
{/* Progress Steps */}
|
||||
<div className="bg-white border-b border-gray-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="py-4">
|
||||
<nav className="flex justify-center">
|
||||
<ol className="flex items-center space-x-8">
|
||||
{steps.map((step, index) => (
|
||||
<li key={step.id} className="flex items-center">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`flex items-center justify-center w-10 h-10 rounded-full border-2 transition-all ${
|
||||
currentStep >= step.id
|
||||
? "bg-blue-600 border-blue-600 text-white shadow-lg"
|
||||
: currentStep === step.id - 1
|
||||
? "border-blue-300 text-blue-600"
|
||||
: "border-gray-300 text-gray-500"
|
||||
}`}
|
||||
>
|
||||
<span className="text-sm font-semibold">{step.id}</span>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<p
|
||||
className={`text-sm font-semibold ${
|
||||
currentStep >= step.id ? "text-blue-600" : "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{step.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">{step.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<ArrowRight
|
||||
className={`ml-8 h-5 w-5 ${currentStep > step.id ? "text-blue-600" : "text-gray-400"}`}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="py-8">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">{renderStep()}</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
152
src/components/navigation/header.tsx
Normal file
152
src/components/navigation/header.tsx
Normal file
@ -0,0 +1,152 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Bell, Settings, LogOut, User, Menu, X } from "lucide-react"
|
||||
import { useAuth } from "@/contexts/auth-context"
|
||||
|
||||
const navigation = [
|
||||
{ name: "Project Builder", href: "/", current: false },
|
||||
{ name: "Templates", href: "/templates", current: false },
|
||||
{ name: "Features", href: "/features", current: false },
|
||||
{ name: "Business Context", href: "/business-context", current: false },
|
||||
{ name: "Architecture", href: "/architecture", current: false },
|
||||
]
|
||||
|
||||
export default function Header() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
const pathname = usePathname()
|
||||
const { user, logout } = useAuth()
|
||||
|
||||
return (
|
||||
<header className="bg-white shadow-sm border-b">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex h-16 justify-between items-center">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center">
|
||||
<Link href="/" className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold text-sm">C</span>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-gray-900">Codenuk</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex space-x-8">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
pathname === item.href
|
||||
? "bg-blue-100 text-blue-700"
|
||||
: "text-gray-600 hover:text-gray-900 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Right side */}
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Auth Button or User Menu */}
|
||||
{!user ? (
|
||||
<Link href="/auth">
|
||||
<Button variant="outline" size="sm">
|
||||
Sign In
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
{/* Notifications */}
|
||||
<Button variant="ghost" size="sm" className="relative">
|
||||
<Bell className="h-5 w-5" />
|
||||
<Badge className="absolute -top-1 -right-1 h-5 w-5 rounded-full p-0 flex items-center justify-center text-xs">
|
||||
3
|
||||
</Badge>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* User Menu - Only show when user is authenticated */}
|
||||
{user && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-8 w-8 rounded-full cursor-pointer">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={user.avatar || "/avatars/01.png"} alt={user.name} />
|
||||
<AvatarFallback>{user.name.charAt(0).toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="end" forceMount>
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-medium leading-none">{user.name}</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">{user.email}</p>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
<span>Profile</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
<span>Settings</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={logout}>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
<span>Log out</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<Button variant="ghost" size="sm" className="md:hidden" onClick={() => setMobileMenuOpen(!mobileMenuOpen)}>
|
||||
{mobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="md:hidden">
|
||||
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3 border-t">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={`block px-3 py-2 rounded-md text-base font-medium transition-colors ${
|
||||
pathname === item.href
|
||||
? "bg-blue-100 text-blue-700"
|
||||
: "text-gray-600 hover:text-gray-900 hover:bg-gray-50"
|
||||
}`}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
414
src/components/templates/template-page.tsx
Normal file
414
src/components/templates/template-page.tsx
Normal file
@ -0,0 +1,414 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import {
|
||||
Search,
|
||||
Star,
|
||||
Clock,
|
||||
Plus,
|
||||
Copy,
|
||||
Globe,
|
||||
BarChart3,
|
||||
Zap,
|
||||
Code,
|
||||
ArrowRight,
|
||||
Download,
|
||||
Eye,
|
||||
Heart,
|
||||
Share2,
|
||||
} from "lucide-react"
|
||||
|
||||
interface Template {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
category: string
|
||||
features: string[]
|
||||
complexity: number
|
||||
timeEstimate: string
|
||||
techStack: string[]
|
||||
popularity?: number
|
||||
lastUpdated?: string
|
||||
downloads?: number
|
||||
likes?: number
|
||||
author?: string
|
||||
isPublic?: boolean
|
||||
preview?: string
|
||||
}
|
||||
|
||||
export function TemplatesPage() {
|
||||
const [activeTab, setActiveTab] = useState("browse")
|
||||
const [selectedCategory, setSelectedCategory] = useState("all")
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [sortBy, setSortBy] = useState("popularity")
|
||||
|
||||
const templates: Template[] = [
|
||||
// Marketing Templates
|
||||
{
|
||||
id: "marketing-website",
|
||||
title: "Marketing Website",
|
||||
description: "Professional marketing site with CMS and lead generation",
|
||||
category: "marketing",
|
||||
features: ["Content Management", "Contact Forms", "SEO Optimization", "Analytics Integration"],
|
||||
complexity: 2,
|
||||
timeEstimate: "1-2 weeks",
|
||||
techStack: ["Next.js", "Sanity CMS", "Tailwind CSS", "Vercel"],
|
||||
popularity: 95,
|
||||
lastUpdated: "2024-01-15",
|
||||
downloads: 1250,
|
||||
likes: 89,
|
||||
author: "Codenuk Team",
|
||||
isPublic: true,
|
||||
preview: "/marketing-website-preview.png",
|
||||
},
|
||||
{
|
||||
id: "landing-page",
|
||||
title: "Landing Page",
|
||||
description: "High-converting landing page with A/B testing capabilities",
|
||||
category: "marketing",
|
||||
features: ["A/B Testing", "Conversion Tracking", "Lead Capture", "Mobile Optimization"],
|
||||
complexity: 2,
|
||||
timeEstimate: "3-5 days",
|
||||
techStack: ["Next.js", "Tailwind CSS", "Google Analytics", "Mailchimp"],
|
||||
popularity: 88,
|
||||
lastUpdated: "2024-01-10",
|
||||
downloads: 980,
|
||||
likes: 76,
|
||||
author: "Codenuk Team",
|
||||
isPublic: true,
|
||||
preview: "/landing-page-preview.png",
|
||||
},
|
||||
{
|
||||
id: "blog-platform",
|
||||
title: "Blog Platform",
|
||||
description: "Content-rich blog with SEO optimization and social sharing",
|
||||
category: "marketing",
|
||||
features: ["Content Management", "SEO Tools", "Social Sharing", "Comment System"],
|
||||
complexity: 3,
|
||||
timeEstimate: "1-2 weeks",
|
||||
techStack: ["Next.js", "MDX", "Tailwind CSS", "Disqus"],
|
||||
popularity: 82,
|
||||
lastUpdated: "2024-01-12",
|
||||
downloads: 756,
|
||||
likes: 64,
|
||||
author: "Community",
|
||||
isPublic: true,
|
||||
preview: "/blog-platform-preview.png",
|
||||
},
|
||||
// Software Templates
|
||||
{
|
||||
id: "saas-platform",
|
||||
title: "SaaS Platform",
|
||||
description: "Complete SaaS application with user management, billing, and analytics",
|
||||
category: "software",
|
||||
features: ["User Authentication", "Payment Processing", "Analytics Integration", "API Management"],
|
||||
complexity: 5,
|
||||
timeEstimate: "4-6 weeks",
|
||||
techStack: ["Next.js", "PostgreSQL", "Stripe", "NextAuth.js"],
|
||||
popularity: 92,
|
||||
lastUpdated: "2024-01-15",
|
||||
downloads: 2100,
|
||||
likes: 156,
|
||||
author: "Codenuk Team",
|
||||
isPublic: true,
|
||||
preview: "/saas-platform-preview.png",
|
||||
},
|
||||
{
|
||||
id: "dashboard-app",
|
||||
title: "Analytics Dashboard",
|
||||
description: "Data visualization dashboard with real-time updates",
|
||||
category: "software",
|
||||
features: ["Data Visualization", "Real-time Updates", "User Authentication", "Export Features"],
|
||||
complexity: 4,
|
||||
timeEstimate: "3-4 weeks",
|
||||
techStack: ["Next.js", "Chart.js", "WebSockets", "PostgreSQL"],
|
||||
popularity: 89,
|
||||
lastUpdated: "2024-01-14",
|
||||
downloads: 1890,
|
||||
likes: 134,
|
||||
author: "Community",
|
||||
isPublic: true,
|
||||
preview: "/analytics-dashboard-preview.png",
|
||||
},
|
||||
// SEO Templates
|
||||
{
|
||||
id: "seo-optimized-blog",
|
||||
title: "SEO-Optimized Blog",
|
||||
description: "Blog platform with advanced SEO features and schema markup",
|
||||
category: "seo",
|
||||
features: ["Schema Markup", "Meta Optimization", "Sitemap Generation", "Performance Optimization"],
|
||||
complexity: 3,
|
||||
timeEstimate: "2-3 weeks",
|
||||
techStack: ["Next.js", "SEO Tools", "Schema.org", "Google Search Console"],
|
||||
popularity: 90,
|
||||
lastUpdated: "2024-01-15",
|
||||
downloads: 1456,
|
||||
likes: 98,
|
||||
author: "SEO Expert",
|
||||
isPublic: true,
|
||||
preview: "/seo-blog-preview.png",
|
||||
},
|
||||
]
|
||||
|
||||
const categories = [
|
||||
{ id: "all", name: "All Templates", icon: Globe, count: templates.length },
|
||||
{
|
||||
id: "marketing",
|
||||
name: "Marketing",
|
||||
icon: Zap,
|
||||
count: templates.filter((t) => t.category === "marketing").length,
|
||||
},
|
||||
{ id: "software", name: "Software", icon: Code, count: templates.filter((t) => t.category === "software").length },
|
||||
{ id: "seo", name: "SEO", icon: BarChart3, count: templates.filter((t) => t.category === "seo").length },
|
||||
]
|
||||
|
||||
const filteredTemplates = templates.filter((template) => {
|
||||
const matchesCategory = selectedCategory === "all" || template.category === selectedCategory
|
||||
const matchesSearch =
|
||||
template.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
template.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
template.features.some((feature) => feature.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
return matchesCategory && matchesSearch
|
||||
})
|
||||
|
||||
const sortedTemplates = [...filteredTemplates].sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case "popularity":
|
||||
return (b.popularity || 0) - (a.popularity || 0)
|
||||
case "downloads":
|
||||
return (b.downloads || 0) - (a.downloads || 0)
|
||||
case "recent":
|
||||
return new Date(b.lastUpdated || "").getTime() - new Date(a.lastUpdated || "").getTime()
|
||||
case "likes":
|
||||
return (b.likes || 0) - (a.likes || 0)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
const getComplexityColor = (complexity: number) => {
|
||||
if (complexity <= 2) return "bg-green-100 text-green-800"
|
||||
if (complexity <= 3) return "bg-yellow-100 text-yellow-800"
|
||||
return "bg-red-100 text-red-800"
|
||||
}
|
||||
|
||||
const getComplexityLabel = (complexity: number) => {
|
||||
if (complexity <= 2) return "Simple"
|
||||
if (complexity <= 3) return "Moderate"
|
||||
return "Complex"
|
||||
}
|
||||
|
||||
const TemplateCard = ({ template }: { template: Template }) => (
|
||||
<Card className="group hover:shadow-xl transition-all duration-300 border-2 hover:border-blue-200">
|
||||
<div className="relative">
|
||||
<Image
|
||||
src={template.preview || "/placeholder.svg"}
|
||||
alt={`${template.title} preview`}
|
||||
width={400}
|
||||
height={192}
|
||||
className="w-full h-48 object-cover rounded-t-lg"
|
||||
/>
|
||||
<div className="absolute top-3 right-3 flex space-x-2">
|
||||
<Button size="sm" variant="secondary" className="opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" className="opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Heart className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-2 flex-1">
|
||||
<CardTitle className="text-lg group-hover:text-blue-600 transition-colors">{template.title}</CardTitle>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge className={getComplexityColor(template.complexity)}>
|
||||
{getComplexityLabel(template.complexity)}
|
||||
</Badge>
|
||||
<div className="flex items-center space-x-1 text-sm text-gray-500">
|
||||
<Star className="h-4 w-4 text-yellow-500 fill-current" />
|
||||
<span>{template.popularity}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm">{template.description}</p>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between text-sm text-gray-500">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>{template.timeEstimate}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Download className="h-4 w-4" />
|
||||
<span>{template.downloads}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h4 className="font-medium text-sm text-gray-700 mb-2">Features:</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{template.features.slice(0, 2).map((feature, index) => (
|
||||
<Badge key={index} variant="outline" className="text-xs">
|
||||
{feature}
|
||||
</Badge>
|
||||
))}
|
||||
{template.features.length > 2 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{template.features.length - 2} more
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-sm text-gray-700 mb-2">Tech Stack:</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{template.techStack.slice(0, 3).map((tech, index) => (
|
||||
<Badge key={index} variant="secondary" className="text-xs">
|
||||
{tech}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2 pt-2">
|
||||
<Button className="flex-1 bg-blue-600 hover:bg-blue-700">
|
||||
Use Template
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Share2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 pt-2 border-t">
|
||||
<span>by {template.author}</span>
|
||||
<span>{template.lastUpdated}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<TabsList className="grid w-full max-w-md grid-cols-3">
|
||||
<TabsTrigger value="browse">Browse Templates</TabsTrigger>
|
||||
<TabsTrigger value="my-templates">My Templates</TabsTrigger>
|
||||
<TabsTrigger value="favorites">Favorites</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
placeholder="Search templates..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 w-80"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md text-sm"
|
||||
>
|
||||
<option value="popularity">Most Popular</option>
|
||||
<option value="downloads">Most Downloaded</option>
|
||||
<option value="recent">Recently Updated</option>
|
||||
<option value="likes">Most Liked</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabsContent value="browse" className="space-y-8">
|
||||
{/* Category Filters */}
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{categories.map((category) => {
|
||||
const Icon = category.icon
|
||||
return (
|
||||
<button
|
||||
key={category.id}
|
||||
onClick={() => setSelectedCategory(category.id)}
|
||||
className={`flex items-center space-x-2 px-4 py-2 rounded-lg border transition-all ${
|
||||
selectedCategory === category.id
|
||||
? "bg-blue-600 text-white border-blue-600"
|
||||
: "bg-white text-gray-700 border-gray-200 hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="font-medium">{category.name}</span>
|
||||
<Badge variant="secondary" className="ml-1">
|
||||
{category.count}
|
||||
</Badge>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Templates Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{sortedTemplates.map((template) => (
|
||||
<TemplateCard key={template.id} template={template} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Results Summary */}
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-600">
|
||||
Showing {sortedTemplates.length} template{sortedTemplates.length !== 1 ? "s" : ""}
|
||||
{searchQuery && ` matching "${searchQuery}"`}
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="my-templates" className="space-y-8">
|
||||
<div className="text-center py-20">
|
||||
<Plus className="h-16 w-16 text-gray-400 mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-gray-700 mb-3">No Templates Yet</h3>
|
||||
<p className="text-gray-500 mb-6 max-w-md mx-auto">
|
||||
Create your first template to get started. Templates you create will appear here.
|
||||
</p>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700">
|
||||
<Plus className="mr-2 h-5 w-5" />
|
||||
Create Your First Template
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="favorites" className="space-y-8">
|
||||
<div className="text-center py-20">
|
||||
<Heart className="h-16 w-16 text-gray-400 mx-auto mb-6" />
|
||||
<h3 className="text-2xl font-semibold text-gray-700 mb-3">No Favorites Yet</h3>
|
||||
<p className="text-gray-500 mb-6 max-w-md mx-auto">
|
||||
Heart templates you like to save them here for quick access later.
|
||||
</p>
|
||||
<Button variant="outline" onClick={() => setActiveTab("browse")}>
|
||||
Browse Templates
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
53
src/components/ui/avatar.tsx
Normal file
53
src/components/ui/avatar.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
46
src/components/ui/badge.tsx
Normal file
46
src/components/ui/badge.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
59
src/components/ui/button.tsx
Normal file
59
src/components/ui/button.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
92
src/components/ui/card.tsx
Normal file
92
src/components/ui/card.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
32
src/components/ui/checkbox.tsx
Normal file
32
src/components/ui/checkbox.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
257
src/components/ui/dropdown-menu.tsx
Normal file
257
src/components/ui/dropdown-menu.tsx
Normal file
@ -0,0 +1,257 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
21
src/components/ui/input.tsx
Normal file
21
src/components/ui/input.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
24
src/components/ui/label.tsx
Normal file
24
src/components/ui/label.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
31
src/components/ui/progress.tsx
Normal file
31
src/components/ui/progress.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
value,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className="bg-primary h-full w-full flex-1 transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Progress }
|
||||
45
src/components/ui/radio-group.tsx
Normal file
45
src/components/ui/radio-group.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function RadioGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
data-slot="radio-group"
|
||||
className={cn("grid gap-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function RadioGroupItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
data-slot="radio-group-item"
|
||||
className={cn(
|
||||
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator
|
||||
data-slot="radio-group-indicator"
|
||||
className="relative flex items-center justify-center"
|
||||
>
|
||||
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
||||
185
src/components/ui/select.tsx
Normal file
185
src/components/ui/select.tsx
Normal file
@ -0,0 +1,185 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
66
src/components/ui/tabs.tsx
Normal file
66
src/components/ui/tabs.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
138
src/contexts/auth-context.tsx
Normal file
138
src/contexts/auth-context.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
"use client"
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from "react"
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null
|
||||
isAuthenticated: boolean
|
||||
isLoading: boolean
|
||||
login: (email: string, password: string) => Promise<boolean>
|
||||
signup: (name: string, email: string, password: string) => Promise<boolean>
|
||||
logout: () => void
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext)
|
||||
if (context === undefined) {
|
||||
throw new Error("useAuth must be used within an AuthProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: AuthProviderProps) {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
// Check if user is logged in on mount
|
||||
useEffect(() => {
|
||||
const checkAuth = () => {
|
||||
// Check localStorage for user data
|
||||
const userData = localStorage.getItem("codenuk_user")
|
||||
if (userData) {
|
||||
try {
|
||||
const user = JSON.parse(userData)
|
||||
setUser(user)
|
||||
} catch (error) {
|
||||
console.error("Error parsing user data:", error)
|
||||
localStorage.removeItem("codenuk_user")
|
||||
}
|
||||
}
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
checkAuth()
|
||||
}, [])
|
||||
|
||||
const login = async (email: string, password: string): Promise<boolean> => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// For demo purposes, accept any email/password combination
|
||||
if (email && password) {
|
||||
const user: User = {
|
||||
id: "1",
|
||||
name: email.split("@")[0], // Use email prefix as name
|
||||
email: email,
|
||||
avatar: "/avatars/01.png"
|
||||
}
|
||||
|
||||
setUser(user)
|
||||
localStorage.setItem("codenuk_user", JSON.stringify(user))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error("Login error:", error)
|
||||
return false
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const signup = async (name: string, email: string, password: string): Promise<boolean> => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// For demo purposes, create user if all fields are provided
|
||||
if (name && email && password) {
|
||||
const user: User = {
|
||||
id: "1",
|
||||
name: name,
|
||||
email: email,
|
||||
avatar: "/avatars/01.png"
|
||||
}
|
||||
|
||||
setUser(user)
|
||||
localStorage.setItem("codenuk_user", JSON.stringify(user))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error("Signup error:", error)
|
||||
return false
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
setUser(null)
|
||||
localStorage.removeItem("codenuk_user")
|
||||
}
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
isAuthenticated: !!user,
|
||||
isLoading,
|
||||
login,
|
||||
signup,
|
||||
logout
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
}
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
30
src/types/feature.ts
Normal file
30
src/types/feature.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { LucideIcon } from "lucide-react"
|
||||
|
||||
export interface Feature {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
icon: LucideIcon
|
||||
complexity: number
|
||||
timeImpact: string
|
||||
dependencies: string[]
|
||||
conflicts: string[]
|
||||
techStack: string[]
|
||||
businessQuestions: string[]
|
||||
isCore?: boolean
|
||||
isPopular?: boolean
|
||||
}
|
||||
|
||||
export interface SelectedFeature extends Feature {
|
||||
order: number
|
||||
customConfig?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface FeatureCategory {
|
||||
id: string
|
||||
name: string
|
||||
icon: LucideIcon
|
||||
count: number
|
||||
}
|
||||
|
||||
27
src/types/template.ts
Normal file
27
src/types/template.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { LucideIcon } from "lucide-react"
|
||||
|
||||
export interface Template {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
category: string
|
||||
features: string[]
|
||||
complexity: number
|
||||
timeEstimate: string
|
||||
techStack: string[]
|
||||
popularity?: number
|
||||
lastUpdated?: string
|
||||
downloads?: number
|
||||
likes?: number
|
||||
author?: string
|
||||
isPublic?: boolean
|
||||
preview?: string
|
||||
}
|
||||
|
||||
export interface TemplateCategory {
|
||||
id: string
|
||||
name: string
|
||||
icon: LucideIcon
|
||||
count: number
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user