Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,676 changes: 1,577 additions & 99 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
"gsap": "^3.15.0",
"lucide-react": "^0.511.0",
"meshline": "^3.3.1",
"next": "15.5.20",
"next": "15.5.22",
"next-themes": "^0.4.4",
"nodemailer": "^9.0.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.6.0",
"react-markdown": "10.1.0",
"remark-gfm": "4.0.1",
"sweetalert2": "^11.26.24",
"three": "^0.184.0"
},
Expand All @@ -38,7 +40,7 @@
"@types/react-dom": "^18.2.0",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.0",
"eslint-config-next": "^15.5.18",
"eslint-config-next": "15.5.22",
"postcss": "^8.5.15",
"tailwindcss": "^3.4.19",
"typescript": "^5.0.0"
Expand Down
73 changes: 73 additions & 0 deletions src/app/api/github-readme/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { NextResponse } from 'next/server'
import { parseGitHubRepository } from '@/lib/portfolioMedia'

type GitHubReadmeResponse = {
content?: string
download_url?: string | null
encoding?: string
html_url?: string
name?: string
path?: string
}

function directoryUrl(value?: string | null) {
if (!value) return ''
return value.slice(0, value.lastIndexOf('/') + 1)
}

export async function GET(request: Request) {
const repositoryUrl = new URL(request.url).searchParams.get('repository')
const repository = parseGitHubRepository(repositoryUrl)

if (!repository) {
return NextResponse.json({ error: 'A valid GitHub repository URL is required.' }, { status: 400 })
}

const headers: Record<string, string> = {
Accept: 'application/vnd.github+json',
'User-Agent': 'Dev-Sahad-Portfolio/1.0',
'X-GitHub-Api-Version': '2022-11-28',
}
if (process.env.GITHUB_TOKEN) {
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`
}

const response = await fetch(
`https://api.github.com/repos/${encodeURIComponent(repository.owner)}/${encodeURIComponent(repository.repo)}/readme`,
{
headers,
next: { revalidate: 3600 },
},
)

if (response.status === 404) {
return NextResponse.json({ error: 'This repository does not have a README yet.' }, { status: 404 })
}

if (!response.ok) {
return NextResponse.json(
{ error: 'The repository README is temporarily unavailable.' },
{ status: response.status === 403 ? 503 : 502 },
)
}

const readme = (await response.json()) as GitHubReadmeResponse
if (!readme.content || readme.encoding !== 'base64') {
return NextResponse.json({ error: 'The README response was not readable.' }, { status: 502 })
}

const markdown = Buffer.from(readme.content.replace(/\s/g, ''), 'base64').toString('utf8')

return NextResponse.json(
{
markdown,
rawBaseUrl: directoryUrl(readme.download_url),
sourceBaseUrl: directoryUrl(readme.html_url),
},
{
headers: {
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
},
},
)
}
3 changes: 2 additions & 1 deletion src/app/api/import-github-projects/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createClient } from '@supabase/supabase-js'
import { getAuthenticatedAdminUser } from '@/lib/supabaseAdmin'
import { getRepositoryThumbnail } from '@/lib/portfolioMedia'

export const dynamic = 'force-dynamic'

Expand Down Expand Up @@ -187,7 +188,7 @@ CREATE POLICY "admin_write" ON public.projects
live_url: r.homepage?.startsWith('http') ? r.homepage : null,
technologies: [r.language, ...(r.topics ?? [])].filter(Boolean).join(', ') || 'JavaScript',
key_features: r.topics?.join(', ') || null,
image_url: null,
image_url: getRepositoryThumbnail(r.html_url),
image_urls: [],
}))

Expand Down
145 changes: 145 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,148 @@ html[data-motion='reduced'] .intro-3d-object,
html[data-motion='reduced'] .intro-3d-copy {
transform: none !important;
}

.readme-content {
color: rgba(255, 255, 255, 0.66);
font-family: 'Syne', sans-serif;
font-size: 0.875rem;
line-height: 1.8;
overflow-wrap: anywhere;
}

.readme-content > :first-child {
margin-top: 0;
}

.readme-content > :last-child {
margin-bottom: 0;
}

.readme-content h1,
.readme-content h2,
.readme-content h3,
.readme-content h4 {
color: rgba(255, 255, 255, 0.94);
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.3;
margin: 2rem 0 0.8rem;
}

.readme-content h1 {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
font-size: clamp(1.7rem, 4vw, 2.4rem);
padding-bottom: 0.8rem;
}

.readme-content h2 {
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
font-size: 1.35rem;
padding-bottom: 0.55rem;
}

.readme-content h3 {
font-size: 1.08rem;
}

.readme-content p,
.readme-content ul,
.readme-content ol,
.readme-content pre,
.readme-content blockquote,
.readme-content table {
margin: 0.9rem 0;
}

.readme-content ul,
.readme-content ol {
padding-left: 1.5rem;
}

.readme-content ul {
list-style: disc;
}

.readme-content ol {
list-style: decimal;
}

.readme-content li {
margin: 0.35rem 0;
padding-left: 0.25rem;
}

.readme-content a {
color: #67e8f9;
text-decoration: underline;
text-decoration-color: rgba(103, 232, 249, 0.3);
text-underline-offset: 0.22em;
}

.readme-content a:hover {
color: #cffafe;
text-decoration-color: currentColor;
}

.readme-content img {
background: rgba(255, 255, 255, 0.025);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 1rem;
display: inline-block;
height: auto;
margin: 0.65rem 0;
max-width: 100%;
}

.readme-content pre {
background: #08080a;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 1rem;
overflow-x: auto;
padding: 1rem;
}

.readme-content code {
font-family: 'DM Mono', monospace;
font-size: 0.8em;
}

.readme-content :not(pre) > code {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.4rem;
color: rgba(255, 255, 255, 0.85);
padding: 0.15rem 0.35rem;
}

.readme-content blockquote {
border-left: 3px solid rgba(103, 232, 249, 0.5);
color: rgba(255, 255, 255, 0.5);
padding-left: 1rem;
}

.readme-content table {
border-collapse: collapse;
display: block;
max-width: 100%;
overflow-x: auto;
width: max-content;
}

.readme-content th,
.readme-content td {
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 0.65rem 0.8rem;
text-align: left;
}

.readme-content th {
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.85);
}

.readme-content hr {
border: 0;
border-top: 1px solid rgba(255, 255, 255, 0.1);
margin: 2rem 0;
}
6 changes: 4 additions & 2 deletions src/app/portfolio/[id]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata } from 'next'
import { createClient } from '@/utils/supabase/server'
import { getProjectThumbnail } from '@/lib/portfolioMedia'

type Props = {
children: React.ReactNode
Expand All @@ -9,8 +10,9 @@ type Props = {
export async function generateMetadata({ params }: Pick<Props, 'params'>): Promise<Metadata> {
const { id } = await params
const database = await createClient()
const { data: project } = await database.from('projects').select('title,description,image_url').eq('id', id).maybeSingle()
const { data: project } = await database.from('projects').select('title,description,image_url,github_url').eq('id', id).maybeSingle()
if (!project) return { title: 'Project case study' }
const thumbnail = getProjectThumbnail(project)
return {
title: project.title,
description: project.description,
Expand All @@ -19,7 +21,7 @@ export async function generateMetadata({ params }: Pick<Props, 'params'>): Promi
type: 'article',
title: project.title,
description: project.description,
images: project.image_url ? [{ url: project.image_url }] : undefined,
images: thumbnail ? [{ url: thumbnail }] : undefined,
},
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/app/portfolio/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { useParams, useRouter } from "next/navigation";
import { motion } from "framer-motion";
import { supabase } from "@/lib/supabase";
import { trackEvent } from "@/lib/analytics";
import { getProjectImages } from "@/lib/portfolioMedia";
import ProjectReadme from "@/components/portfolio/ProjectReadme";

import {
ArrowLeft,
Expand Down Expand Up @@ -70,12 +72,7 @@ export default function PortfolioDetailPage() {
.split(",")
.filter((f) => f.trim() !== "");

const galleryImages =
project?.image_urls && Array.isArray(project.image_urls)
? project.image_urls
: project?.image_url
? [project.image_url]
: [];
const galleryImages = project ? getProjectImages(project) : [];

const nextImage = () => {
if (currentImage < galleryImages.length - 1) {
Expand Down Expand Up @@ -291,6 +288,7 @@ export default function PortfolioDetailPage() {
</p>
</motion.div>
)}

</motion.div>

{/* RIGHT */}
Expand Down Expand Up @@ -371,6 +369,8 @@ export default function PortfolioDetailPage() {
</motion.div>
</motion.div>
</div>

<ProjectReadme githubUrl={project.github_url} />
</div>
);
}
Loading
Loading