feat: add ProjectsPage and ProjectsContent components with project data integration
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
import { ProjectsContent } from "@/components/projects-content";
|
||||||
|
|
||||||
|
export default function ProjectsPage() {
|
||||||
|
return <ProjectsContent />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import { Badge, Card, Typography } from "poyraz-ui/atoms";
|
||||||
|
import { ImageCard } from "poyraz-ui/molecules";
|
||||||
|
import { FIGMA_TEMPLATES, MOBILE_APPS, WEB_APPS, type ProjectItem } from "@/data/projects";
|
||||||
|
import { getGithubRepos, getNpmPackages } from "@/lib/project-feeds";
|
||||||
|
|
||||||
|
function getLanguageMeta(language: string) {
|
||||||
|
const key = language.toLowerCase();
|
||||||
|
|
||||||
|
if (key.includes("typescript")) {
|
||||||
|
return { icon: "vscode-icons:file-type-typescript-official", color: "text-blue-600" };
|
||||||
|
}
|
||||||
|
if (key.includes("javascript")) {
|
||||||
|
return { icon: "vscode-icons:file-type-js-official", color: "text-amber-500" };
|
||||||
|
}
|
||||||
|
if (key.includes("python")) {
|
||||||
|
return { icon: "vscode-icons:file-type-python", color: "text-yellow-600" };
|
||||||
|
}
|
||||||
|
if (key.includes("go")) {
|
||||||
|
return { icon: "vscode-icons:file-type-go", color: "text-cyan-600" };
|
||||||
|
}
|
||||||
|
if (key.includes("rust")) {
|
||||||
|
return { icon: "vscode-icons:file-type-rust", color: "text-orange-600" };
|
||||||
|
}
|
||||||
|
if (key.includes("java")) {
|
||||||
|
return { icon: "vscode-icons:file-type-java", color: "text-red-600" };
|
||||||
|
}
|
||||||
|
if (key.includes("html")) {
|
||||||
|
return { icon: "vscode-icons:file-type-html", color: "text-orange-500" };
|
||||||
|
}
|
||||||
|
if (key.includes("css")) {
|
||||||
|
return { icon: "vscode-icons:file-type-css", color: "text-blue-500" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { icon: "mdi:code-tags", color: "text-zinc-500" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectSection({ title, items }: { title: string; items: ProjectItem[] }) {
|
||||||
|
return (
|
||||||
|
<section className="space-y-2">
|
||||||
|
<Typography variant="large" className="text-base">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<div className="grid grid-cols-2 gap-2 lg:grid-cols-4">
|
||||||
|
{items.map((item) => (
|
||||||
|
<ImageCard
|
||||||
|
key={item.id}
|
||||||
|
image={item.image}
|
||||||
|
title={item.title}
|
||||||
|
description={item.description}
|
||||||
|
badge={item.badge}
|
||||||
|
href={item.href}
|
||||||
|
className="aspect-square rounded-sm border-border"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ProjectsContent() {
|
||||||
|
const [repos, npmPackages] = await Promise.all([getGithubRepos(), getNpmPackages()]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="flex h-full flex-col gap-3 overflow-y-auto">
|
||||||
|
<Card className="rounded-sm border-border bg-background p-2">
|
||||||
|
<div className="overflow-x-auto rounded-sm">
|
||||||
|
<Image
|
||||||
|
src="https://ghchart.rshah.org/dc2626/poyrazavsever"
|
||||||
|
alt="GitHub contribution activity for poyrazavsever"
|
||||||
|
width={820}
|
||||||
|
height={120}
|
||||||
|
className="h-auto w-full min-w-[740px]"
|
||||||
|
unoptimized
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<ProjectSection title="Mobile Apps" items={MOBILE_APPS} />
|
||||||
|
<ProjectSection title="Web Apps" items={WEB_APPS} />
|
||||||
|
<ProjectSection title="Figma Templates" items={FIGMA_TEMPLATES} />
|
||||||
|
|
||||||
|
<section className="space-y-2">
|
||||||
|
<Typography variant="large" className="text-base">
|
||||||
|
npm paketleri (@poyrazavsever)
|
||||||
|
</Typography>
|
||||||
|
<div className="grid gap-2 md:grid-cols-2">
|
||||||
|
{npmPackages.length === 0 ? (
|
||||||
|
<Card className="rounded-sm border-border p-3 md:col-span-2">
|
||||||
|
<Typography variant="small" className="text-muted-foreground">
|
||||||
|
npm API response is empty right now.
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
npmPackages.map((pkg) => (
|
||||||
|
<Link
|
||||||
|
key={pkg.name}
|
||||||
|
href={pkg.npmUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="block h-full no-underline text-inherit"
|
||||||
|
>
|
||||||
|
<Card className="flex h-full flex-col rounded-sm border-border p-3 transition-colors hover:border-zinc-700">
|
||||||
|
<Typography variant="large" className="text-base leading-tight">
|
||||||
|
{pkg.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="small" className="mt-1 flex-1 line-clamp-3 text-muted-foreground">
|
||||||
|
{pkg.description}
|
||||||
|
</Typography>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Badge className="rounded-sm">v{pkg.version}</Badge>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="space-y-2">
|
||||||
|
<Typography variant="large" className="text-base">
|
||||||
|
GitHub Repos (@poyrazavsever)
|
||||||
|
</Typography>
|
||||||
|
<div className="grid gap-2 md:grid-cols-2">
|
||||||
|
{repos.length === 0 ? (
|
||||||
|
<Card className="rounded-sm border-border p-3 md:col-span-2">
|
||||||
|
<Typography variant="small" className="text-muted-foreground">
|
||||||
|
GitHub API response is empty right now.
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
repos.map((repo) => {
|
||||||
|
const languageMeta = getLanguageMeta(repo.language);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={repo.id}
|
||||||
|
href={repo.htmlUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="block h-full no-underline text-inherit"
|
||||||
|
>
|
||||||
|
<Card className="flex h-full flex-col rounded-sm border-border p-3 transition-colors hover:border-zinc-700">
|
||||||
|
<Typography variant="large" className="text-base leading-tight">
|
||||||
|
{repo.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="small" className="mt-1 flex-1 line-clamp-4 text-muted-foreground">
|
||||||
|
{repo.description}
|
||||||
|
</Typography>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||||
|
<Badge variant="outline" className="rounded-sm">
|
||||||
|
<span className="inline-flex items-center gap-1">
|
||||||
|
<Icon
|
||||||
|
icon={languageMeta.icon}
|
||||||
|
width={14}
|
||||||
|
height={14}
|
||||||
|
className={languageMeta.color}
|
||||||
|
/>
|
||||||
|
<span>{repo.language}</span>
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="rounded-sm">
|
||||||
|
<span className="inline-flex items-center gap-1">
|
||||||
|
<Icon icon="mdi:star" width={14} height={14} className="text-amber-500" />
|
||||||
|
<span>{repo.stars}</span>
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
export type ProjectItem = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
image: string;
|
||||||
|
badge?: string;
|
||||||
|
href?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MOBILE_APPS: ProjectItem[] = [
|
||||||
|
{
|
||||||
|
id: "mobile-habit-flow",
|
||||||
|
title: "Habit Flow",
|
||||||
|
description:
|
||||||
|
"A mobile-first productivity app focused on clean habit tracking, reminders, and weekly review loops.",
|
||||||
|
image: "/images/hero1.png",
|
||||||
|
badge: "React Native",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mobile-campus-connect",
|
||||||
|
title: "Campus Connect",
|
||||||
|
description:
|
||||||
|
"Event and club discovery app prototype for university communities with role-based content publishing.",
|
||||||
|
image: "/images/hero2.png",
|
||||||
|
badge: "Expo",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const WEB_APPS: ProjectItem[] = [
|
||||||
|
{
|
||||||
|
id: "web-portfolio",
|
||||||
|
title: "Personal Portfolio",
|
||||||
|
description:
|
||||||
|
"A minimal portfolio system with blog details, command palette navigation, and reusable design system primitives.",
|
||||||
|
image: "/news/design.svg",
|
||||||
|
badge: "Next.js",
|
||||||
|
href: "https://poyrazavsever.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "web-poyraz-ui-docs",
|
||||||
|
title: "Poyraz UI Docs",
|
||||||
|
description:
|
||||||
|
"Documentation and showcase site for the UI kit, including component examples and implementation patterns.",
|
||||||
|
image: "/news/performance.svg",
|
||||||
|
badge: "poyraz-ui",
|
||||||
|
href: "https://ui.poyrazavsever.com",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FIGMA_TEMPLATES: ProjectItem[] = [
|
||||||
|
{
|
||||||
|
id: "figma-minimal-saas",
|
||||||
|
title: "Minimal SaaS Landing Kit",
|
||||||
|
description:
|
||||||
|
"Landing page template set with token-based spacing and typography scales for fast startup launches.",
|
||||||
|
image: "/news/design.svg",
|
||||||
|
badge: "Figma",
|
||||||
|
href: "https://www.behance.net/poyrazavsever",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "figma-dashboard-clean",
|
||||||
|
title: "Clean Dashboard Blocks",
|
||||||
|
description:
|
||||||
|
"Reusable dashboard cards and layout blocks designed for rapid MVP iteration and design consistency.",
|
||||||
|
image: "/news/performance.svg",
|
||||||
|
badge: "Design System",
|
||||||
|
href: "https://www.behance.net/poyrazavsever",
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
const USERNAME = "poyrazavsever";
|
||||||
|
|
||||||
|
export type GithubRepo = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
htmlUrl: string;
|
||||||
|
description: string;
|
||||||
|
language: string;
|
||||||
|
stars: number;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NpmPackage = {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
description: string;
|
||||||
|
npmUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GithubActivity = {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
repo: string;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GithubRepoApi = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
html_url: string;
|
||||||
|
description: string | null;
|
||||||
|
language: string | null;
|
||||||
|
stargazers_count: number;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NpmSearchResponse = {
|
||||||
|
objects?: Array<{
|
||||||
|
package?: {
|
||||||
|
name?: string;
|
||||||
|
version?: string;
|
||||||
|
description?: string;
|
||||||
|
links?: {
|
||||||
|
npm?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GithubEventApi = {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
repo?: {
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getGithubRepos(): Promise<GithubRepo[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.github.com/users/${USERNAME}/repos?sort=updated&per_page=12&type=owner`,
|
||||||
|
{
|
||||||
|
next: { revalidate: 1800 },
|
||||||
|
headers: {
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
"User-Agent": "portfolio-new",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) return [];
|
||||||
|
const data = (await response.json()) as GithubRepoApi[];
|
||||||
|
|
||||||
|
return data.map((repo) => ({
|
||||||
|
id: repo.id,
|
||||||
|
name: repo.name,
|
||||||
|
htmlUrl: repo.html_url,
|
||||||
|
description: repo.description ?? "No description",
|
||||||
|
language: repo.language ?? "Unknown",
|
||||||
|
stars: repo.stargazers_count,
|
||||||
|
updatedAt: repo.updated_at,
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNpmPackages(): Promise<NpmPackage[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://registry.npmjs.org/-/v1/search?text=maintainer:${USERNAME}&size=12`,
|
||||||
|
{
|
||||||
|
next: { revalidate: 1800 },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) return [];
|
||||||
|
const data = (await response.json()) as NpmSearchResponse;
|
||||||
|
const items = data.objects ?? [];
|
||||||
|
|
||||||
|
return items
|
||||||
|
.map((item) => item.package)
|
||||||
|
.filter((pkg): pkg is NonNullable<typeof pkg> => Boolean(pkg?.name))
|
||||||
|
.map((pkg) => ({
|
||||||
|
name: pkg.name ?? "unknown-package",
|
||||||
|
version: pkg.version ?? "-",
|
||||||
|
description: pkg.description ?? "No description",
|
||||||
|
npmUrl: pkg.links?.npm ?? `https://www.npmjs.com/package/${pkg.name}`,
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGithubActivity(): Promise<GithubActivity[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://api.github.com/users/${USERNAME}/events/public?per_page=6`, {
|
||||||
|
next: { revalidate: 600 },
|
||||||
|
headers: {
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
"User-Agent": "portfolio-new",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) return [];
|
||||||
|
const data = (await response.json()) as GithubEventApi[];
|
||||||
|
|
||||||
|
return data.map((event) => ({
|
||||||
|
id: event.id,
|
||||||
|
type: event.type,
|
||||||
|
repo: event.repo?.name ?? "unknown-repo",
|
||||||
|
createdAt: event.created_at,
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-1
@@ -1,7 +1,14 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "ghchart.rshah.org",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
Reference in New Issue
Block a user