diff --git a/README.md b/README.md index 8f25179..58fe5f9 100644 --- a/README.md +++ b/README.md @@ -2,64 +2,113 @@ GDSC VIT -

< Insert Project Title Here >

-

< Insert Project Description Here >

+

DevBoard

+

A modern, feature-rich GitHub widget builder and portfolio dashboard. Create beautiful, interactive widgets for your GitHub profile, visualize stats, and showcase your work with premium design tools inspired by Figma, Notion, and Linear.

--- [![Join Us](https://img.shields.io/badge/Join%20Us-Developer%20Student%20Clubs-red)](https://dsc.community.dev/vellore-institute-of-technology/) -[![Discord Chat](https://img.shields.io/discord/760928671698649098.svg)](https://discord.gg/498KVdSKWR) - + [![DOCS](https://img.shields.io/badge/Documentation-see%20docs-green?style=flat-square&logo=appveyor)](INSERT_LINK_FOR_DOCS_HERE) [![UI ](https://img.shields.io/badge/User%20Interface-Link%20to%20UI-orange?style=flat-square&logo=appveyor)](INSERT_UI_LINK_HERE) - ## Features -- [ ] < feature > -- [ ] < feature > -- [ ] < feature > -- [ ] < feature > +- [x] Premium Canvas Sizes: Presets and custom dimensions for widgets +- [x] Drag & Drop Elements: Text, images, containers, shapes, charts, progress bars, badges, buttons, QR codes +- [x] GitHub Data Integration: Live stats, profile info, avatar, commits, and more +- [x] Inline Editing: Double-click to edit text inline +- [x] Layer Management: Organize, lock/unlock, toggle visibility +- [x] Grid & Snap: Toggle grid, adjust size, snap-to-grid +- [x] Theme Switching: Dark/light canvas themes +- [x] SVG Export: Auto-generated, production-ready SVG code +- [x] Save & Privacy: Save widgets, set privacy, add tags +- [x] Undo/Redo: Robust history management -
+
## Dependencies - - < dependency > - - < dependency > - +- Node.js (v18+ recommended) +- npm or yarn +- Docker (optional) +## Dependencies ## Running - -< directions to install > +Clone the repository: ```bash -< insert code > +git clone https://github.com/NitinTheGreat/devboard-frontend.git +cd devboard-frontend/devboard ``` -< directions to execute > +Install dependencies: +```bash +npm install +# or +yarn install +``` +Run the development server: ```bash -< insert code > +npm run dev +# or +yarn dev +``` + +Open [http://localhost:3000](http://localhost:3000) in your browser. + +Create a `.env.local` file in the `devboard` directory and set: +``` +NEXT_PUBLIC_API_BASE_URL= +``` +## Running + +A sample `Dockerfile` is provided in `devboard/` for easy containerization: +```Dockerfile +FROM node:18-alpine AS builder +WORKDIR /app +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +RUN yarn install --frozen-lockfile || npm install +COPY . . +RUN yarn build || npm run build + +FROM node:18-alpine AS runner +WORKDIR /app +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/public ./public +COPY --from=builder /app/package.json ./package.json +RUN yarn install --production --frozen-lockfile || npm install --production +ENV NODE_ENV=production +ENV PORT=3000 +EXPOSE 3000 +CMD ["yarn", "start"] ``` + ## Contributors + - - + + +
- John Doe +
+ Nitin Kumar Pandey +

- Your Name Here (Insert Your Image Link In Src + + GitHub + + + LinkedIn + +

+

-

- - GitHub - - - LinkedIn - -

- - + + +

diff --git a/devboard/.env.example b/devboard/.env.example new file mode 100644 index 0000000..4a70930 --- /dev/null +++ b/devboard/.env.example @@ -0,0 +1,2 @@ +GEMINI_API_KEY=your api key +NEXT_PUBLIC_API_BASE_URL=http://localhost:3000 \ No newline at end of file diff --git a/devboard/.gitignore b/devboard/.gitignore new file mode 100644 index 0000000..e72b4d6 --- /dev/null +++ b/devboard/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/devboard/Dockerfile b/devboard/Dockerfile new file mode 100644 index 0000000..113b920 --- /dev/null +++ b/devboard/Dockerfile @@ -0,0 +1,36 @@ +# DevBoard Frontend Dockerfile +# Build a production-ready container for Next.js frontend + +FROM node:18-alpine AS builder +WORKDIR /app + +# Install dependencies +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +RUN yarn install --frozen-lockfile || npm install + +# Copy source code +COPY . . + +# Build Next.js app +RUN yarn build || npm run build + +# Production image +FROM node:18-alpine AS runner +WORKDIR /app + +# Copy built assets from builder +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/public ./public +COPY --from=builder /app/package.json ./package.json + +# Install only production dependencies +RUN yarn install --production --frozen-lockfile || npm install --production + +# Set environment variables (override in deployment) +ENV NODE_ENV=production +ENV PORT=3000 + +EXPOSE 3000 + +# Start Next.js server +CMD ["yarn", "start"] diff --git a/devboard/README.md b/devboard/README.md new file mode 100644 index 0000000..684a44e --- /dev/null +++ b/devboard/README.md @@ -0,0 +1,130 @@ + # DevBoard Frontend + +A modern, feature-rich GitHub widget builder and portfolio dashboard. DevBoard empowers developers to create beautiful, interactive widgets for their GitHub profiles, visualize stats, and showcase their work with premium design tools inspired by Figma, Notion, and Linear. + +--- + +## 🚀 Features + +### Widget Builder +- **Premium Canvas Sizes**: Choose from preset sizes (Badge, Stats Card, Banner, Square, Large Display) or set custom dimensions. +- **Drag & Drop Elements**: Add text, images, containers, shapes (rectangle, circle, triangle, star), charts, progress bars, badges, buttons, and QR codes. +- **GitHub Data Integration**: Fetch live GitHub stats (username, name, bio, followers, following, public repos, avatar, creation date, commits) and display them dynamically in widgets. +- **Inline Editing**: Double-click elements to edit text inline with a beautiful, responsive editor. +- **Layer Management**: Organize, lock/unlock, and toggle visibility for each element. View all layers in a sidebar. +- **Grid & Snap**: Toggle grid visibility, adjust grid size, and enable snap-to-grid for precise placement. +- **Theme Switching**: Instantly switch between dark and light canvas themes. +- **SVG Export**: Auto-generates production-ready SVG code for your widget. Copy to clipboard with one click. +- **Save & Privacy**: Save widgets to your dashboard, set them as private, and tag them for easy organization. +- **Undo/Redo**: Robust history management for all canvas actions. + +### Portfolio Dashboard +- **Profile Integration**: Connect your GitHub account and display widgets on your portfolio page. +- **Widget Marketplace**: Discover, use, and remix widgets created by the community (coming soon). + +### UI/UX +- **Modern Design**: Inspired by top design tools, with floating toolbars, draggable panels, and smooth transitions. +- **Accessibility**: Keyboard shortcuts for quick actions, responsive layout for all devices. + +--- + +## 🛠️ Getting Started + +### Prerequisites +- Node.js (v18+ recommended) +- npm or yarn +- Docker (optional, for containerized deployment) + +### Installation + +1. **Clone the repository:** + ```sh + git clone https://github.com/NitinTheGreat/devboard-frontend.git + cd devboard-frontend/devboard + ``` +2. **Install dependencies:** + ```sh + npm install + # or + yarn install + ``` +3. **Run the development server:** + ```sh + npm run dev + # or + yarn dev + ``` +4. **Open in browser:** + Visit [http://localhost:3000](http://localhost:3000) + +### Environment Variables +Create a `.env.local` file in the `devboard` directory and set: +``` +NEXT_PUBLIC_API_BASE_URL= +``` + +--- + +## 🐳 Docker Deployment + +A sample `Dockerfile` is provided for easy containerization. See below for details. + +--- + +## 🧩 Project Structure + +- `devboard/app/` — Next.js app routes and pages +- `devboard/components/` — UI components, widget builder, authentication, marketplace, etc. +- `devboard/lib/` — Utility functions, context, types +- `devboard/public/` — Static assets (SVGs, icons) +- `devboard/types/` — TypeScript types + +--- + +## ✨ Contributing + +We welcome contributions to DevBoard! To get started: + +1. **Fork the repository** and create your branch: + ```sh + git checkout -b feature/your-feature-name + ``` +2. **Make your changes** (see `devboard/components/widget-builder/index.tsx` for main logic). +3. **Test locally** and ensure all features work as expected. +4. **Commit and push**: + ```sh + git commit -m "Add: your feature description" + git push origin feature/your-feature-name + ``` +5. **Open a Pull Request** on GitHub. Please describe your changes clearly and reference any related issues. + +### Guidelines +- Write clean, readable code and follow the existing style. +- Document new features in the README if relevant. +- Add tests if possible. +- Be respectful and collaborative. + +--- + +## 👤 Contributor + +- **Nitin Kumar Pandey** + [LinkedIn](https://www.linkedin.com/in/nitinkrpandey) + +--- + +## 📄 License + +This project is licensed under the MIT License. See [LICENSE](../LICENSE) for details. + +--- + +## 💬 Questions & Support + +For questions, suggestions, or support, please open an issue or reach out via [LinkedIn](https://www.linkedin.com/in/nitinkrpandey). + +--- + +## 🐳 Dockerfile Example + +See below for a ready-to-use Dockerfile to run DevBoard Frontend in a container. diff --git a/devboard/app/api/llm/generate/route.ts b/devboard/app/api/llm/generate/route.ts new file mode 100644 index 0000000..d567f63 --- /dev/null +++ b/devboard/app/api/llm/generate/route.ts @@ -0,0 +1,5 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(req: NextRequest) { + return NextResponse.json({}); +} \ No newline at end of file diff --git a/devboard/app/api/readme/generate/route.ts b/devboard/app/api/readme/generate/route.ts new file mode 100644 index 0000000..a20a032 --- /dev/null +++ b/devboard/app/api/readme/generate/route.ts @@ -0,0 +1,103 @@ +import { type NextRequest, NextResponse } from "next/server" +import { ReadmeInputSchema, type ReadmeState } from "@/lib/types" +import { createReadmeGraph } from "@/lib/readme-graph" + +export async function POST(request: NextRequest) { + try { + // Validate input + const body = await request.json() + const validatedInput = ReadmeInputSchema.parse(body) + + const { username, currentContent, isNew, personalInfo } = validatedInput + + // Create streaming response + const encoder = new TextEncoder() + + const stream = new ReadableStream({ + async start(controller) { + try { + // Initialize the graph + const graph = createReadmeGraph() + + // Prepare initial state + const initialState: ReadmeState = { + username, + currentContent, + isNew, + personalInfo, + } + + // Execute the graph + const result = await graph.invoke(initialState) + + // Check for errors + if (result.error) { + const errorData = encoder.encode( + `data: ${JSON.stringify({ + type: "error", + error: result.error, + })}\n\n`, + ) + controller.enqueue(errorData) + controller.close() + return + } + + // Stream the final content + const finalContent = result.finalContent || "" + const chunkSize = 10 + + for (let i = 0; i < finalContent.length; i += chunkSize) { + const chunk = finalContent.slice(i, i + chunkSize) + const data = encoder.encode( + `data: ${JSON.stringify({ + type: "content", + content: chunk, + })}\n\n`, + ) + controller.enqueue(data) + // Small delay for streaming effect + await new Promise((resolve) => setTimeout(resolve, 50)) + } + + // Send completion signal + const completeData = encoder.encode( + `data: ${JSON.stringify({ + type: "complete", + finalContent: finalContent, + })}\n\n`, + ) + controller.enqueue(completeData) + controller.close() + } catch (error) { + console.error("Graph execution error:", error) + const errorData = encoder.encode( + `data: ${JSON.stringify({ + type: "error", + error: error instanceof Error ? error.message : "Unknown error", + })}\n\n`, + ) + controller.enqueue(errorData) + controller.close() + } + }, + }) + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }) + } catch (error) { + console.error("API route error:", error) + + // Handle validation errors + if (error instanceof Error && error.name === "ZodError") { + return NextResponse.json({ error: "Invalid input data", details: error.message }, { status: 400 }) + } + + return NextResponse.json({ error: "Failed to generate README" }, { status: 500 }) + } +} diff --git a/devboard/app/api/widget/[id]/route.ts b/devboard/app/api/widget/[id]/route.ts new file mode 100644 index 0000000..6ba5f8a --- /dev/null +++ b/devboard/app/api/widget/[id]/route.ts @@ -0,0 +1,134 @@ +import { type NextRequest, NextResponse } from "next/server" + +// GitHub API functions +async function fetchGitHubUser(username: string) { + const response = await fetch(`https://api.github.com/users/${username}`) + if (!response.ok) throw new Error("User not found") + return response.json() +} + +async function fetchGitHubStats(username: string) { + // Mock implementation - replace with actual GitHub GraphQL API + return { + totalCommitContributions: Math.floor(Math.random() * 2000) + 500, + totalPullRequestContributions: Math.floor(Math.random() * 200) + 50, + totalIssueContributions: Math.floor(Math.random() * 100) + 20, + contributionYears: new Date().getFullYear() - 2018, + totalStars: Math.floor(Math.random() * 1000) + 100, + } +} + +// Theme definitions +const THEMES = { + dark: { + backgroundColor: "#0D1117", + primaryColor: "#58A6FF", + secondaryColor: "#21262D", + textColor: "#F0F6FC", + accentColor: "#238636", + }, + light: { + backgroundColor: "#FFFFFF", + primaryColor: "#0969DA", + secondaryColor: "#F6F8FA", + textColor: "#24292F", + accentColor: "#1A7F37", + }, + github: { + backgroundColor: "#0D1117", + primaryColor: "#F78166", + secondaryColor: "#161B22", + textColor: "#E6EDF3", + accentColor: "#7C3AED", + }, + ocean: { + backgroundColor: "#0F172A", + primaryColor: "#0EA5E9", + secondaryColor: "#1E293B", + textColor: "#F1F5F9", + accentColor: "#06B6D4", + }, +} + +function replaceVariables(content: string, userData: any, stats: any, theme: any) { + let processedContent = content + + // Replace GitHub variables + processedContent = processedContent.replace(/\{\{username\}\}/g, userData.login) + processedContent = processedContent.replace(/\{\{name\}\}/g, userData.name || userData.login) + processedContent = processedContent.replace(/\{\{bio\}\}/g, userData.bio || "No bio available") + processedContent = processedContent.replace(/\{\{followers\}\}/g, userData.followers.toString()) + processedContent = processedContent.replace(/\{\{following\}\}/g, userData.following.toString()) + processedContent = processedContent.replace(/\{\{public_repos\}\}/g, userData.public_repos.toString()) + processedContent = processedContent.replace(/\{\{public_gists\}\}/g, userData.public_gists.toString()) + processedContent = processedContent.replace(/\{\{avatar_url\}\}/g, userData.avatar_url) + processedContent = processedContent.replace( + /\{\{created_at\}\}/g, + new Date(userData.created_at).getFullYear().toString(), + ) + + // Replace stats variables + processedContent = processedContent.replace( + /\{\{totalCommitContributions\}\}/g, + stats.totalCommitContributions.toString(), + ) + processedContent = processedContent.replace( + /\{\{totalPullRequestContributions\}\}/g, + stats.totalPullRequestContributions.toString(), + ) + processedContent = processedContent.replace( + /\{\{totalIssueContributions\}\}/g, + stats.totalIssueContributions.toString(), + ) + processedContent = processedContent.replace(/\{\{contributionYears\}\}/g, stats.contributionYears.toString()) + processedContent = processedContent.replace(/\{\{totalStars\}\}/g, stats.totalStars.toString()) + + return processedContent +} + +export async function GET(request: NextRequest, { params }: { params: { id: string } }) { + try { + const { searchParams } = new URL(request.url) + const username = searchParams.get("username") + const themeName = searchParams.get("theme") || "dark" + + if (!username) { + return new NextResponse("Username parameter is required", { status: 400 }) + } + + // Fetch widget from your backend + const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL + const widgetResponse = await fetch(`${apiUrl}/api/widget/${params.id}`) + + if (!widgetResponse.ok) { + return new NextResponse("Widget not found", { status: 404 }) + } + + const widget = await widgetResponse.json() + + // Fetch GitHub data + const [userData, stats] = await Promise.all([fetchGitHubUser(username), fetchGitHubStats(username)]) + + // Get theme + const theme = THEMES[themeName as keyof typeof THEMES] || THEMES.dark + + // Process widget content + const processedContent = replaceVariables(widget.content, userData, stats, theme) + + // Generate final SVG + const svg = ` + + ${processedContent} +` + + return new NextResponse(svg, { + headers: { + "Content-Type": "image/svg+xml", + "Cache-Control": "public, max-age=3600", // Cache for 1 hour + }, + }) + } catch (error) { + console.error("Widget rendering error:", error) + return new NextResponse("Internal server error", { status: 500 }) + } +} diff --git a/devboard/app/api/widget/route.ts b/devboard/app/api/widget/route.ts new file mode 100644 index 0000000..5b4bd02 --- /dev/null +++ b/devboard/app/api/widget/route.ts @@ -0,0 +1,61 @@ +import { type NextRequest, NextResponse } from "next/server" + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const authHeader = request.headers.get("authorization") + + if (!authHeader) { + return NextResponse.json({ error: "Authorization required" }, { status: 401 }) + } + + // Forward to your existing backend + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/widget`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: authHeader, + }, + body: JSON.stringify(body), + }) + + if (!response.ok) { + const errorText = await response.text() + console.error("Backend error:", errorText) + return NextResponse.json({ error: "Failed to save widget" }, { status: response.status }) + } + + const result = await response.json() + return NextResponse.json(result) + } catch (error) { + console.error("API error:", error) + return NextResponse.json({ error: "Internal server error" }, { status: 500 }) + } +} + +export async function GET(request: NextRequest) { + try { + const authHeader = request.headers.get("authorization") + + if (!authHeader) { + return NextResponse.json({ error: "Authorization required" }, { status: 401 }) + } + + // Forward to your existing backend + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/widget`, { + headers: { + Authorization: authHeader, + }, + }) + + if (!response.ok) { + return NextResponse.json({ error: "Failed to fetch widgets" }, { status: response.status }) + } + + const result = await response.json() + return NextResponse.json(result) + } catch (error) { + console.error("API error:", error) + return NextResponse.json({ error: "Internal server error" }, { status: 500 }) + } +} diff --git a/devboard/app/favicon.ico b/devboard/app/favicon.ico new file mode 100644 index 0000000..00b9556 Binary files /dev/null and b/devboard/app/favicon.ico differ diff --git a/devboard/app/globals.css b/devboard/app/globals.css new file mode 100644 index 0000000..dc98be7 --- /dev/null +++ b/devboard/app/globals.css @@ -0,0 +1,122 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@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); +} + +:root { + --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); +} + +.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; + } +} diff --git a/devboard/app/layout.tsx b/devboard/app/layout.tsx new file mode 100644 index 0000000..eb96d56 --- /dev/null +++ b/devboard/app/layout.tsx @@ -0,0 +1,44 @@ +import type React from "react" +import type { Metadata } from "next" +import { Inter } from "next/font/google" +import "./globals.css" +import { AuthProvider } from "@/lib/auth-context" +import { Toaster } from "sonner" +import Navbar from "@/components/Navbar" + +const inter = Inter({ subsets: ["latin"] }) + +export const metadata: Metadata = { + title: "DevBoard ", + description: "Transform your GitHub README into a stunning portfolio website and many more", +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + +

+ + {children} +
+ + + + + ) +} diff --git a/devboard/app/login/page.tsx b/devboard/app/login/page.tsx new file mode 100644 index 0000000..b7899d0 --- /dev/null +++ b/devboard/app/login/page.tsx @@ -0,0 +1,27 @@ +import { Suspense } from "react" +import { Loader2 } from "lucide-react" +import LoginContent from "@/components/login-content" +import LoginAuthHandler from "@/components/login-auth-handler" + +// Loading component for Suspense fallback +function LoginLoading() { + return ( +
+
+ +

Loading...

+
+
+ ) +} + +export default function LoginPage() { + return ( + <> + }> + + + + + ) +} diff --git a/devboard/app/page.tsx b/devboard/app/page.tsx new file mode 100644 index 0000000..d410e39 --- /dev/null +++ b/devboard/app/page.tsx @@ -0,0 +1,28 @@ +import { Suspense } from "react" +import { Loader2 } from "lucide-react" +import HeroSection from "@/components/Hero" +import AuthHandler from "@/components/auth-handler" +import Marketplace from "@/components/marketplace" +// Loading component for Suspense fallback +function PageLoading() { + return ( +
+
+ +

Loading...

+
+
+ ) +} + +export default function Home() { + return ( + <> + }> + + + + + + ) +} diff --git a/devboard/app/portfolio/page.tsx b/devboard/app/portfolio/page.tsx new file mode 100644 index 0000000..e5b97b6 --- /dev/null +++ b/devboard/app/portfolio/page.tsx @@ -0,0 +1,398 @@ +"use client" + +import type React from "react" + +import { useState, useRef, useEffect } from "react" +import { useRouter } from "next/navigation" +import { Button } from "@/components/ui/button" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Textarea } from "@/components/ui/textarea" +import { Label } from "@/components/ui/label" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Progress } from "@/components/ui/progress" +import { Loader2, Upload, Copy, Download, ArrowLeft } from "lucide-react" +import { toast } from "sonner" +import { useAuth } from "@/lib/auth-context" + +interface GenerationState { + currentStep: string + progress: number + parsedData: string + portfolioCode: string + error: string | null +} + +export default function PortfolioGenerator() { + const [isUploading, setIsUploading] = useState(false) + const [isGenerating, setIsGenerating] = useState(false) + const [content, setContent] = useState("") + const [customMessage, setCustomMessage] = useState("") + const [style, setStyle] = useState("minimal") + const [generationState, setGenerationState] = useState({ + currentStep: "", + progress: 0, + parsedData: "", + portfolioCode: "", + error: null, + }) + const [streamingCode, setStreamingCode] = useState("") + const fileInputRef = useRef(null) + const codeRef = useRef(null) + const { isAuthenticated } = useAuth() + const router = useRouter() + + // Auto-scroll to bottom of code as it streams + useEffect(() => { + if (codeRef.current) { + codeRef.current.scrollTop = codeRef.current.scrollHeight + } + }, [streamingCode]) + + const handleFileUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + + setIsUploading(true) + try { + const text = await file.text() + setContent(text) + toast.success(`${file.name} uploaded successfully`) + } catch (error) { + toast.error("Error uploading file") + } finally { + setIsUploading(false) + } + } + + const handleGenerate = async () => { + if (!isAuthenticated) { + toast.error("Please log in to generate your portfolio") + router.push("/login") + return + } + + if (!content) { + toast.error("Please upload a README or resume first") + return + } + + setIsGenerating(true) + setStreamingCode("") + setGenerationState({ + currentStep: "starting", + progress: 0, + parsedData: "", + portfolioCode: "", + error: null, + }) + + try { + const response = await fetch("/api/portfolio/generate", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content, customMessage, style }), + }) + + if (!response.ok) { + throw new Error("Failed to start generation") + } + + const reader = response.body?.getReader() + const decoder = new TextDecoder() + + if (reader) { + while (true) { + const { done, value } = await reader.read() + if (done) break + + const chunk = decoder.decode(value) + const lines = chunk.split("\n") + + for (const line of lines) { + if (line.startsWith("data: ")) { + try { + const data = JSON.parse(line.slice(6)) + + if (data.type === "complete") { + setIsGenerating(false) + toast.success("Portfolio generated successfully!") + break + } + + if (data.type === "error") { + throw new Error(data.error) + } + + // Update generation state + if (data.currentStep || data.progress !== undefined) { + setGenerationState((prev) => ({ + ...prev, + currentStep: data.currentStep || prev.currentStep, + progress: data.progress !== undefined ? data.progress : prev.progress, + parsedData: data.parsedData || prev.parsedData, + portfolioCode: data.portfolioCode || prev.portfolioCode, + })) + } + + // Handle streaming code generation + if (data.currentStep === "generating" && data.portfolioCode) { + setStreamingCode(data.portfolioCode) + } + } catch (e) { + // Ignore parsing errors for incomplete chunks + } + } + } + } + } + } catch (error) { + console.error("Generation error:", error) + toast.error("Failed to generate portfolio") + setGenerationState((prev) => ({ + ...prev, + error: error instanceof Error ? error.message : "Unknown error", + currentStep: "error", + })) + } finally { + setIsGenerating(false) + } + } + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(generationState.portfolioCode || streamingCode) + toast.success("Code copied to clipboard!") + } catch (error) { + toast.error("Failed to copy code") + } + } + + const downloadCode = () => { + const code = generationState.portfolioCode || streamingCode + const blob = new Blob([code], { type: "text/javascript" }) + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = url + a.download = "Portfolio.jsx" + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + toast.success("Code downloaded!") + } + + const getStepDescription = (step: string) => { + switch (step) { + case "starting": + return "Initializing portfolio generation..." + case "parsing": + return "Analyzing your content and extracting key information..." + case "generating": + return "Generating your React portfolio code..." + case "complete": + return "Portfolio generation completed!" + case "error": + return "An error occurred during generation" + default: + return "Processing..." + } + } + + return ( +
+
+ {/* Header */} +
+ +

Portfolio Generator

+
+ +
+ {/* Configuration Panel */} + + + Configuration + + + {/* File Upload */} +
+ + + +
+ + {/* Content Textarea */} +
+ +