diff --git a/app/admin/dashboard/admin-dashboard-client.tsx b/app/admin/dashboard/admin-dashboard-client.tsx index 1246b1e..68589b6 100644 --- a/app/admin/dashboard/admin-dashboard-client.tsx +++ b/app/admin/dashboard/admin-dashboard-client.tsx @@ -2,15 +2,17 @@ import { useEffect, useState } from "react" import { Button } from "@/components/ui/button" -import { Plus, LogOut, Home, UserPlus, LayoutDashboard, Database, Activity } from "lucide-react" +import { Plus, LogOut, Home, UserPlus, LayoutDashboard, Database, Activity, Github } from "lucide-react" import { AdminProjectCard } from "@/components/admin-project-card" import { ProjectDialog } from "@/components/project-dialog" import { AdminDialog } from "@/components/admin-dialog" import { AdminCard } from "@/components/admin-card" import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@/components/ui/accordion" -import type { Project, Admin } from "@/lib/types" +import type { Project, Admin, GitHubProject } from "@/lib/types" import { logoutAdmin } from "@/lib/admin-auth" -import { getAdmins, getMaintenanceMode, getAvailability, getProjects } from "@/lib/actions" +import { getAdmins, getMaintenanceMode, getAvailability, getProjects, getGitHubProjects } from "@/lib/actions" +import { GitHubProjectDialog } from "@/components/github-project-dialog" +import { AdminGitHubProjectCard } from "@/components/admin-github-project-card" import { MaintenanceToggle } from "@/components/maintenance-toggle" import { AvailabilityToggle } from "@/components/availability-toggle" import Link from "next/link" @@ -18,6 +20,7 @@ import Link from "next/link" export default function AdminDashboardClient() { const [projects, setProjects] = useState([]) const [admins, setAdmins] = useState([]) + const [githubProjects, setGitHubProjects] = useState([]) const [isLoading, setIsLoading] = useState(true) const [addProjectOpen, setAddProjectOpen] = useState(false) const [addAdminOpen, setAddAdminOpen] = useState(false) @@ -25,6 +28,7 @@ export default function AdminDashboardClient() { const [maintenanceMessage, setMaintenanceMessage] = useState("") const [maintenanceProgress, setMaintenanceProgress] = useState(0) const [isAvailable, setIsAvailable] = useState(true) + const [addGitHubProjectOpen, setAddGitHubProjectOpen] = useState(false) const fetchProjects = async () => { const result = await getProjects() @@ -56,6 +60,13 @@ export default function AdminDashboardClient() { } } + const fetchGitHubProjects = async () => { + const result = await getGitHubProjects() + if (result.success) { + setGitHubProjects(result.data) + } + } + const fetchAvailability = async () => { const result = await getAvailability() if (result.success && result.isAvailable !== undefined) { @@ -70,12 +81,17 @@ export default function AdminDashboardClient() { fetchAdmins() fetchMaintenanceMode() fetchAvailability() + fetchGitHubProjects() }, []) const handleProjectDeleted = (projectId: string) => { setProjects((prev) => prev.filter((p) => p.id !== projectId)) } + const handleGitHubProjectDeleted = (projectId: string) => { + setGitHubProjects((prev) => prev.filter((p) => p.id !== projectId)) + } + const handleProjectUpdated = () => { fetchProjects() } @@ -182,6 +198,41 @@ export default function AdminDashboardClient() { )} + {/* GitHub Projects Section */} +
+
+
+

GITHUB PROJECTS

+

+ Add repos from GitHub — name & description are fetched automatically. +

+
+ +
+ + {githubProjects.length === 0 ? ( +
+
+ +
+

No GitHub projects added yet.

+
+ ) : ( +
+ {githubProjects.map((gp) => ( + + ))} +
+ )} +
+ @@ -275,6 +326,7 @@ export default function AdminDashboardClient() { + ) } diff --git a/app/page.tsx b/app/page.tsx index 1fe82e5..3b46f81 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,8 +1,9 @@ import { db } from "@/db" -import { projets as projetsTable } from "@/db/schema" -import { desc } from "drizzle-orm" -import type { Project } from "@/lib/types" +import { projets as projetsTable, githubProjects as githubProjectsTable } from "@/db/schema" +import { desc, eq, or, isNull } from "drizzle-orm" +import type { Project, GitHubProject } from "@/lib/types" import { PortfolioContent } from "@/components/portfolio-content" +import { GitHubProjectSlider } from "@/components/github-project-slider" import { TechStack } from "@/components/tech-stack" import { Button } from "@/components/ui/button" import { ScrollButton } from "@/components/scroll-button" @@ -46,17 +47,46 @@ export default async function HomePage() { } } - let projects: Project[] = [] + let featuredProject: Project | null = null + let otherProjects: Project[] = [] let fetchError = false + let githubProjects: GitHubProject[] = [] try { - const rawProjects = await db.select().from(projetsTable).orderBy(desc(projetsTable.created_at)) - projects = rawProjects.map((p: any) => ({ + const [rawFeatured] = await db.select().from(projetsTable) + .where(eq(projetsTable.featured, true)) + .limit(1) + + const rawOther = await db.select().from(projetsTable) + .where( + or( + eq(projetsTable.featured, false), + isNull(projetsTable.featured) + ) + ) + .orderBy(desc(projetsTable.created_at)) + + featuredProject = rawFeatured ? ({ + ...rawFeatured, + id: rawFeatured.id.toString(), + created_at: rawFeatured.created_at?.toISOString() || new Date().toISOString(), + updated_at: new Date().toISOString(), + }) as unknown as Project : null + + otherProjects = rawOther.map((p: any) => ({ ...p, id: p.id.toString(), created_at: p.created_at?.toISOString() || new Date().toISOString(), updated_at: new Date().toISOString(), })) as unknown as Project[] + const rawGithub = await db.select().from(githubProjectsTable) + .where(eq(githubProjectsTable.type, "used")) + .orderBy(desc(githubProjectsTable.created_at)) + githubProjects = rawGithub.map((p: any) => ({ + ...p, + id: p.id.toString(), + created_at: p.created_at?.toISOString() || new Date().toISOString(), + })) as unknown as GitHubProject[] } catch (e) { console.error("Database connection error:", e) fetchError = true @@ -95,8 +125,8 @@ export default async function HomePage() {
-
-
+
+

@@ -113,7 +143,7 @@ export default async function HomePage() {

-
+
Discover Projects @@ -152,10 +182,12 @@ export default async function HomePage() { )}
- +
+ +
diff --git a/components/admin-github-project-card.tsx b/components/admin-github-project-card.tsx new file mode 100644 index 0000000..71b4cc8 --- /dev/null +++ b/components/admin-github-project-card.tsx @@ -0,0 +1,97 @@ +"use client" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Trash2, RefreshCw, Github, Star, Loader2, ExternalLink } from "lucide-react" +import { deleteGitHubProject, refreshGitHubProject } from "@/lib/actions" +import { DeleteGitHubProjectDialog } from "./delete-github-project-dialog" +import type { GitHubProject } from "@/lib/types" + +interface AdminGitHubProjectCardProps { + project: GitHubProject + onDeleted: (id: string) => void +} + +export function AdminGitHubProjectCard({ project, onDeleted }: AdminGitHubProjectCardProps) { + const [refreshing, setRefreshing] = useState(false) + const [deleteOpen, setDeleteOpen] = useState(false) + + const handleRefresh = async () => { + setRefreshing(true) + await refreshGitHubProject(project.id) + setRefreshing(false) + } + + return ( + <> +
+
+
+ + {project.name} +
+
+ + +
+
+ {project.description && ( +

{project.description}

+ )} +
+ {project.language && ( +
+
+ {project.language} +
+ )} +
+ + {project.stars} +
+ + + +
+
+ + {project.type === "created" ? "Created" : "Used"} + +
+
+ onDeleted(project.id)} + /> + + ) +} diff --git a/components/admin-project-card.tsx b/components/admin-project-card.tsx index 4eee74f..4be96af 100644 --- a/components/admin-project-card.tsx +++ b/components/admin-project-card.tsx @@ -65,6 +65,11 @@ export function AdminProjectCard({ project, onDeleted, onUpdated }: AdminProject Archived )} + {project.featured && ( + + Featured + + )}

{project.description || "No description."} diff --git a/components/delete-github-project-dialog.tsx b/components/delete-github-project-dialog.tsx new file mode 100644 index 0000000..7450dfc --- /dev/null +++ b/components/delete-github-project-dialog.tsx @@ -0,0 +1,61 @@ +"use client" + +import { useState } from "react" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { deleteGitHubProject } from "@/lib/actions" + +interface DeleteGitHubProjectDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + projectId: string + projectName: string + onDeleted: () => void +} + +export function DeleteGitHubProjectDialog({ + open, + onOpenChange, + projectId, + projectName, + onDeleted, +}: DeleteGitHubProjectDialogProps) { + const [loading, setLoading] = useState(false) + + const handleDelete = async () => { + setLoading(true) + const result = await deleteGitHubProject(projectId) + if (result.success) { + onDeleted() + onOpenChange(false) + } + setLoading(false) + } + + return ( + + + + Delete GitHub Project + + Are you sure you want to remove {projectName} from the list? + + + + Cancel + + {loading ? "Deleting..." : "Delete"} + + + + + ) +} diff --git a/components/github-project-dialog.tsx b/components/github-project-dialog.tsx new file mode 100644 index 0000000..2822dc1 --- /dev/null +++ b/components/github-project-dialog.tsx @@ -0,0 +1,100 @@ +"use client" + +import { useState } from "react" +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { createGitHubProject } from "@/lib/actions" +import { Loader2, Github } from "lucide-react" + +interface GitHubProjectDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onSuccess: () => void +} + +export function GitHubProjectDialog({ open, onOpenChange, onSuccess }: GitHubProjectDialogProps) { + const [url, setUrl] = useState("") + const [type, setType] = useState<"created" | "used">("created") + const [loading, setLoading] = useState(false) + const [error, setError] = useState("") + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!url.trim()) { + setError("Please enter a GitHub repository URL") + return + } + setLoading(true) + setError("") + + const result = await createGitHubProject(url.trim(), type) + if (result.success) { + setUrl("") + setType("created") + setError("") + onSuccess() + onOpenChange(false) + } else { + setError(result.error || "Failed to add project") + } + setLoading(false) + } + + return ( +

+ + + + + Add GitHub Project + + + Paste a GitHub repository URL. The name and description will be fetched automatically. + + +
+
+ + setUrl(e.target.value)} + disabled={loading} + /> +
+
+ + +
+ {error && ( +

+ {error} +

+ )} + +
+
+
+ ) +} diff --git a/components/github-project-slider.tsx b/components/github-project-slider.tsx new file mode 100644 index 0000000..ca3b18f --- /dev/null +++ b/components/github-project-slider.tsx @@ -0,0 +1,111 @@ +"use client" + +import { useRef } from "react" +import { ChevronLeft, ChevronRight, Star, GitFork, ExternalLink } from "lucide-react" +import type { GitHubProject } from "@/lib/types" +import Link from "next/link" + +interface GitHubProjectSliderProps { + projects: GitHubProject[] +} + +function ProjectCard({ project }: { project: GitHubProject }) { + return ( + +
+
+

{project.name}

+ +
+ {project.description && ( +

+ {project.description} +

+ )} +
+ {project.language && ( +
+
+ {project.language} +
+ )} +
+ + {project.stars} +
+
+
+ + ) +} + +function ProjectRow({ title, projects }: { title: string; projects: GitHubProject[] }) { + const scrollRef = useRef(null) + + const scroll = (direction: "left" | "right") => { + if (!scrollRef.current) return + const amount = 300 + scrollRef.current.scrollBy({ + left: direction === "left" ? -amount : amount, + behavior: "smooth", + }) + } + + if (projects.length === 0) return null + + return ( +
+
+

{title}

+
+ + +
+
+
+ {projects.map((project) => ( + + ))} +
+
+ ) +} + +export function GitHubProjectSlider({ projects }: GitHubProjectSliderProps) { + if (projects.length === 0) return null + + return ( +
+
+
+
Open Source
+

GITHUB PROJECTS

+
+
+ +
+
+
+ ) +} diff --git a/components/portfolio-content.tsx b/components/portfolio-content.tsx index 5ee9cda..c2a3e0c 100644 --- a/components/portfolio-content.tsx +++ b/components/portfolio-content.tsx @@ -2,25 +2,39 @@ import type { Project } from "@/lib/types" import { ProjectCard } from "@/components/project-card" +import { ProjectCardSmall } from "@/components/project-card-small" interface PortfolioContentProps { - projects: Project[] + featuredProject: Project | null + otherProjects: Project[] } -export function PortfolioContent({ projects }: PortfolioContentProps) { +export function PortfolioContent({ featuredProject, otherProjects }: PortfolioContentProps) { return ( -
-
- {projects.map((project, index) => ( -
- +
+ {featuredProject && ( +
+
+
- ))} -
+ + )} + + {otherProjects.length > 0 && ( +
+
+ {otherProjects.map((project, index) => ( +
+ +
+ ))} +
+
+ )}
) } diff --git a/components/project-card-small.tsx b/components/project-card-small.tsx new file mode 100644 index 0000000..739bf6b --- /dev/null +++ b/components/project-card-small.tsx @@ -0,0 +1,88 @@ +import type { Project } from "@/lib/types" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { ExternalLink, Github, ImageOff } from "lucide-react" +import Image from "next/image" +import Link from "next/link" + +interface ProjectCardSmallProps { + project: Project +} + +export function ProjectCardSmall({ project }: ProjectCardSmallProps) { + return ( +
+ {/* Image */} +
+ {project.image_url ? ( + {`${project.title} + ) : ( +
+
+ +
+ + No preview + +
+ )} +
+
+ + {/* Content */} +
+

+ {project.title} +

+ + {project.description && ( +

+ {project.description} +

+ )} + +
+ {project.tags.slice(0, 4).map((tag) => ( + + {tag} + + ))} + {project.tags.length > 4 && ( + +{project.tags.length - 4} + )} +
+ +
+ {project.project_url && ( + + )} + {project.github_url && ( + + )} +
+
+
+ ) +} diff --git a/components/project-card.tsx b/components/project-card.tsx index 19daa8c..1c04d16 100644 --- a/components/project-card.tsx +++ b/components/project-card.tsx @@ -1,10 +1,9 @@ import type { Project } from "@/lib/types" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" -import { ExternalLink, Github, ArrowUpRight, Wrench, Construction, CheckCircle2, Archive, PackageCheck, ImageOff, Trophy, Play, Pause } from "lucide-react" +import { ExternalLink, Github, Wrench, Construction, CheckCircle2, Archive, PackageCheck, ImageOff, Trophy, Play, Pause } from "lucide-react" import Image from "next/image" import Link from "next/link" - interface ProjectCardProps { project: Project } @@ -14,238 +13,250 @@ export function ProjectCard({ project }: ProjectCardProps) { const isArchived = project.is_archived; const isInDev = project.in_development; const isPaused = project.development_status === 'paused'; + const isDarkBg = project.text_color === 'black' ? false : true + + const statusBadges = ( + <> + {isInDev && ( + <> +
+
+
+ {isPaused ? ( + <> + + Development paused + + ) : ( + <> + + Development in progress + + )} +
+
+ + )} + + {isFinished && ( +
+
+ +
+
+ )} + + {isArchived && !isFinished && ( +
+
+ +
+
+ )} + + ); return ( -
-
-
- {/* Glow Effect Overlay */} -
+ {/* Glow Effect Overlay */} +
- {project.image_url ? ( -
- {`${project.title} + {/* Image - fills full card on desktop, stacked on mobile */} +
+ {`${project.title} -
+ /> + + {/* Desktop blur gradient */} +
+ + {/* Mobile gradient */} +
+ {statusBadges} +
+ + ) : ( +
+
+
+ +
+ + No preview available + +
+
+ )} + + {/* Content Section */} +
+
+
+
+

+ {project.title} +

{isInDev && ( - <> -
-
-
- {isPaused ? ( - <> - - Development paused - - ) : ( - <> - - Development in progress - - )} -
+
+ {isPaused ? ( + + ) : ( + + )}
- )} - {isFinished && ( -
-
- -
-
+
+ +
)} - - {isArchived && !isFinished && ( -
-
- -
-
+ {isArchived && ( +
+ +
)}
- ) : ( -
-
-
- -
- - No preview available - -
-
- )} -
-
-
-
-

- {project.title} -

- {isInDev && ( -
- {isPaused ? ( - - ) : ( - - )} -
- )} - {isFinished && ( -
- -
- )} - {isArchived && ( -
- -
- )} -
-
- -
-
-

- {project.description} -

-
+
+

+ {project.description} +

+
-
-
- {project.tags.map((tag) => ( - - {tag} - - ))} - {isInDev && ( - - {isPaused ? ( - <> - - On pause - - ) : ( - <> - WIP - - )} - - )} - {isFinished && ( - - Finished - - )} - {isArchived && ( - - Archived - - )} -
+
+
+ {project.tags.map((tag) => ( + + {tag} + + ))} + {isInDev && ( + + {isPaused ? ( + <> + + On pause + + ) : ( + <> + WIP + + )} + + )} + {isFinished && ( + + Finished + + )} + {isArchived && ( + + Archived + + )} +
- {isInDev && ( -
-
-
- )} + {isInDev && ( +
+
+ )} +
-
-
- {isInDev ? ( - - ) : ( - project.project_url && ( - - ) - )} +
+ {isInDev ? ( + + ) : ( + project.project_url && ( + + ) + )} - {project.github_url && ( - - )} -
-
-
+ {project.github_url && ( + + )}
diff --git a/components/project-form.tsx b/components/project-form.tsx index b050a46..94e9e3c 100644 --- a/components/project-form.tsx +++ b/components/project-form.tsx @@ -11,7 +11,7 @@ import { createProject, updateProject } from "@/lib/actions" import { useRouter } from "next/navigation" import { Switch } from "@/components/ui/switch" import { Slider } from "@/components/ui/slider" -import { Settings2, Sparkles, Save, Play, Pause } from "lucide-react" +import { Settings2, Sparkles, Save, Play, Pause, Sun, Moon } from "lucide-react" interface ProjectFormProps { project?: Project @@ -28,6 +28,8 @@ export function ProjectForm({ project, onSuccess }: ProjectFormProps) { const [isCompleted, setIsCompleted] = useState(project?.is_completed || false) const [isArchived, setIsArchived] = useState(project?.is_archived || false) const [progress, setProgress] = useState(project?.development_progress || 0) + const [textColor, setTextColor] = useState<'black' | 'white'>(project?.text_color || 'white') + const [featured, setFeatured] = useState(project?.featured || false) const formRef = useRef(null) const router = useRouter() @@ -50,6 +52,8 @@ export function ProjectForm({ project, onSuccess }: ProjectFormProps) { is_completed: isCompleted, is_archived: isArchived, development_progress: progress, + text_color: textColor, + featured, } try { @@ -172,6 +176,14 @@ export function ProjectForm({ project, onSuccess }: ProjectFormProps) {
)} +
+
+ +

Show as featured project

+
+ +
+
@@ -182,6 +194,39 @@ export function ProjectForm({ project, onSuccess }: ProjectFormProps) {
+ +
+ +
+ + +
+
diff --git a/db/schema-pg.ts b/db/schema-pg.ts index 9d5e6a6..07f3118 100644 --- a/db/schema-pg.ts +++ b/db/schema-pg.ts @@ -15,6 +15,8 @@ export const projets = pgTable("projets", { is_completed: boolean("is_completed").default(false), is_archived: boolean("is_archived").default(false), development_progress: integer("development_progress").default(0), + featured: boolean("featured").default(false), + text_color: text("text_color"), changelog: jsonb("changelog").$type().default([]), created_at: timestamp("created_at").defaultNow(), }) @@ -31,3 +33,15 @@ export const settings = pgTable("settings", { value: jsonb("value").notNull(), updated_at: timestamp("updated_at").defaultNow(), }) + +export const githubProjects = pgTable("github_projects", { + id: serial("id").primaryKey(), + type: text("type").notNull().$type<"created" | "used">(), + github_url: text("github_url").notNull(), + name: text("name").notNull(), + description: text("description"), + language: text("language"), + stars: integer("stars").default(0), + order: integer("order").default(0), + created_at: timestamp("created_at").defaultNow(), +}) diff --git a/db/schema-sqlite.ts b/db/schema-sqlite.ts index 95a0021..b7a65b8 100644 --- a/db/schema-sqlite.ts +++ b/db/schema-sqlite.ts @@ -26,6 +26,8 @@ export const projets = sqliteTable("projets", { is_completed: integer("is_completed").default(0), is_archived: integer("is_archived").default(0), development_progress: integer("development_progress").default(0), + text_color: text("text_color"), + featured: integer("featured").default(0), changelog: jsonText("changelog").$type(), created_at: timestampText("created_at"), }, (table) => ({ @@ -46,3 +48,15 @@ export const settings = sqliteTable("settings", { value: jsonText("value").$type(), updated_at: timestampText("updated_at"), }) + +export const githubProjects = sqliteTable("github_projects", { + id: integer("id").primaryKey({ autoIncrement: true }), + type: text("type").notNull(), + github_url: text("github_url").notNull(), + name: text("name").notNull(), + description: text("description"), + language: text("language"), + stars: integer("stars").default(0), + order: integer("order").default(0), + created_at: timestampText("created_at"), +}) diff --git a/db/schema.ts b/db/schema.ts index 1fd47a9..cf28e90 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -8,4 +8,5 @@ export const { projets, admin, settings, + githubProjects, } = selectedSchema diff --git a/drizzle/0001_soft_ghost_riders.sql b/drizzle/0001_soft_ghost_riders.sql new file mode 100644 index 0000000..1f2405e --- /dev/null +++ b/drizzle/0001_soft_ghost_riders.sql @@ -0,0 +1,11 @@ +CREATE TABLE "github_projects" ( + "id" serial PRIMARY KEY NOT NULL, + "type" text NOT NULL, + "github_url" text NOT NULL, + "name" text NOT NULL, + "description" text, + "language" text, + "stars" integer DEFAULT 0, + "order" integer DEFAULT 0, + "created_at" timestamp DEFAULT now() +); diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 897df90..cc3e2f0 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1784293091515, "tag": "0000_stiff_nova", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1784393091515, + "tag": "0001_soft_ghost_riders", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/actions.ts b/lib/actions.ts index 8961a0b..761a9f4 100644 --- a/lib/actions.ts +++ b/lib/actions.ts @@ -1,15 +1,19 @@ "use server" import { db } from "@/db" -import { projets, admin, settings } from "@/db/schema" +import { projets, admin, settings, githubProjects } from "@/db/schema" import { eq, desc } from "drizzle-orm" import { revalidatePath } from "next/cache" -import type { Project, ChangelogEntry } from "./types" +import type { Project, ChangelogEntry, GitHubProject } from "./types" +import { dbType } from "@/db/config" import bcrypt from "bcryptjs" function toSql(value: unknown): unknown { if (value == null) return null - if (typeof value === "boolean") return value ? 1 : 0 + if (typeof value === "boolean") { + if (dbType === "sqlite") return value ? 1 : 0 + return value + } if (typeof value === "string" || typeof value === "number") return value if (value instanceof Date) return value.toISOString() return value @@ -224,6 +228,121 @@ export async function updateAvailability(isAvailable: boolean) { } } +function parseGithubUrl(url: string): { owner: string; repo: string } | null { + try { + const clean = url.replace(/\.git$/, "").replace(/\/$/, "") + const match = clean.match(/github\.com[:\/]([^\/]+)\/([^\/\s?#]+)/) + if (match) return { owner: match[1], repo: match[2] } + return null + } catch { + return null + } +} + +export async function createGitHubProject(githubUrl: string, type: "created" | "used") { + try { + const parsed = parseGithubUrl(githubUrl) + if (!parsed) return { success: false, error: "Invalid GitHub URL" } + + const res = await fetch(`https://api.github.com/repos/${parsed.owner}/${parsed.repo}`, { + headers: { "User-Agent": "v6-portfolio" }, + }) + + if (!res.ok) { + if (res.status === 403) return { success: false, error: "GitHub API rate limit reached. Try again later." } + if (res.status === 404) return { success: false, error: "Repository not found." } + return { success: false, error: `GitHub API error: ${res.status}` } + } + + const data = await res.json() + const values: Record = { + type, + github_url: data.html_url, + name: data.name, + description: data.description || "", + language: data.language, + stars: data.stargazers_count || 0, + created_at: new Date(), + } + + const [project] = await db.insert(githubProjects).values(values).returning() + + revalidatePath("/") + revalidatePath("/admin/dashboard") + return { success: true, project } + } catch (error: unknown) { + console.error("Error creating GitHub project:", error) + return { success: false, error: (error as Error).message } + } +} + +export async function getGitHubProjects() { + try { + const data = await db.select().from(githubProjects).orderBy(desc(githubProjects.created_at)) + return { + success: true, + data: data.map((p: any) => ({ + ...p, + id: p.id.toString(), + created_at: p.created_at?.toISOString() || new Date().toISOString(), + })) as unknown as GitHubProject[], + } + } catch (error: unknown) { + console.error("Error fetching GitHub projects:", error) + return { success: false, error: (error as Error).message, data: [] } + } +} + +export async function deleteGitHubProject(id: string) { + try { + const numericId = validateId(id) + if (numericId === null) return { success: false, error: "Invalid ID" } + await db.delete(githubProjects).where(eq(githubProjects.id, numericId)) + revalidatePath("/") + revalidatePath("/admin/dashboard") + return { success: true } + } catch (error: unknown) { + console.error("Error deleting GitHub project:", error) + return { success: false, error: "An unexpected error occurred" } + } +} + +export async function refreshGitHubProject(id: string) { + try { + const numericId = validateId(id) + if (numericId === null) return { success: false, error: "Invalid ID" } + + const [existing] = await db.select().from(githubProjects).where(eq(githubProjects.id, numericId)).limit(1) + if (!existing) return { success: false, error: "Project not found" } + + const parsed = parseGithubUrl(existing.github_url) + if (!parsed) return { success: false, error: "Invalid GitHub URL in database" } + + const res = await fetch(`https://api.github.com/repos/${parsed.owner}/${parsed.repo}`, { + headers: { "User-Agent": "v6-portfolio" }, + }) + + if (!res.ok) return { success: false, error: `GitHub API error: ${res.status}` } + + const data = await res.json() + await db.update(githubProjects) + .set({ + name: data.name, + description: data.description || "", + language: data.language, + stars: data.stargazers_count || 0, + }) + .where(eq(githubProjects.id, numericId)) + + revalidatePath("/") + revalidatePath("/admin/dashboard") + return { success: true } + } catch (error: unknown) { + console.error("Error refreshing GitHub project:", error) + return { success: false, error: (error as Error).message } + } +} + export async function getProjects() { try { const data = await db.select().from(projets).orderBy(desc(projets.created_at)) diff --git a/lib/types.ts b/lib/types.ts index d72181a..d1ec503 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -14,6 +14,8 @@ export interface Project { development_progress?: number is_completed?: boolean is_archived?: boolean + text_color?: 'black' | 'white' | null + featured?: boolean changelog?: ChangelogEntry[] } @@ -29,3 +31,15 @@ export interface ChangelogEntry { date: string changes: string[] } + +export interface GitHubProject { + id: string + type: "created" | "used" + github_url: string + name: string + description: string | null + language: string | null + stars: number + order: number + created_at: string +}