feat: mobile profile enhancements (#290)

Co-authored-by: Maxwell <145994855+ping-maxwell@users.noreply.github.com>
This commit is contained in:
Cyberistic
2026-03-05 10:52:46 -08:00
committed by GitHub
co-authored by Maxwell
parent 37b19a6f86
commit 07aae4b8c9
4 changed files with 62 additions and 108 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ cp apps/web/.env.example apps/web/.env
bun install
# 6. Run database migrations
cd apps/web && npx prisma migrate dev && npx prisma generate && cd ../..
cd apps/web && bunx prisma migrate dev && bunx prisma generate && cd ../..
# 7. Start dev server
bun dev
@@ -0,0 +1,8 @@
/*
This file was added to keep backwards compatibility with older versions without dropping the whole database. No idea how it appeared in the first place.
Deleting the folder gives the following prisma error:
- The following migration(s) are applied to the database but missing from the local migrations directory: 20260228005751_init
- You may use prisma migrate reset to drop the development database.
- All data will be lost.
If you can figure out how to delete this folder, please do.
*/
@@ -210,7 +210,7 @@ export function ContributionChart({ data }: { data: ContributionData }) {
</div>
)}
</div>
<div className="overflow-x-auto" ref={scrollContainerRef}>
<div className="overflow-x-auto pr-2" ref={scrollContainerRef}>
<div
className="inline-grid pt-0"
style={{ gridTemplateColumns: `auto 1fr` }}
@@ -32,6 +32,8 @@ import Link from "next/link";
import { parseAsString, parseAsStringLiteral, useQueryState } from "nuqs";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
// !TODO: Last item in languages row should take up remaining space on mobile for a cleaner look
// !TODO: Better input handling of contribution graph on mobile
export interface UserProfile {
login: string;
name: string | null;
@@ -474,7 +476,7 @@ export function UserProfileContent({
return (
<div className="flex flex-col lg:flex-row gap-8 flex-1 min-h-0 pb-2">
{/* ── Left sidebar ── */}
<aside className="shrink-0 lg:w-70 lg:sticky lg:top-4 lg:self-start pl-4 pb-4 lg:pb-0">
<aside className="shrink-0 lg:w-[280px] lg:sticky lg:top-4 lg:self-start px-2 lg:pl-4">
{/* Avatar + identity */}
<div className="flex flex-col items-center lg:items-start">
<div className="relative group">
@@ -702,7 +704,7 @@ export function UserProfileContent({
</aside>
{/* ── Main content ── */}
<main className="flex-1 min-w-0 flex flex-col min-h-0 lg:overflow-y-auto pr-1">
<main className="flex-1 min-w-0 flex flex-col min-h-0 lg:overflow-y-auto px-2 lg:pr-4 pr-1">
{/* Overview stats header */}
<div className="shrink-0 mb-4">
<div className="flex items-center justify-between mb-3">
@@ -848,11 +850,11 @@ export function UserProfileContent({
{/* Tab switcher */}
<div className="shrink-0 mb-4">
<div className="flex items-center border border-border divide-x divide-border rounded-sm w-fit">
<div className="flex items-center border border-border divide-x divide-border rounded-sm lg:w-fit">
<button
onClick={() => setTab("repositories")}
className={cn(
"flex items-center gap-2 px-4 py-2 text-[11px] font-mono uppercase tracking-wider transition-colors cursor-pointer rounded-l-md",
"flex-1 flex items-center justify-center gap-2 px-4 py-2 text-[11px] font-mono uppercase tracking-wider transition-colors cursor-pointer lg:rounded-l-md",
tab === "repositories"
? "bg-muted/50 dark:bg-white/4 text-foreground"
: "text-muted-foreground hover:text-foreground/60 hover:bg-muted/60 dark:hover:bg-white/3",
@@ -867,7 +869,7 @@ export function UserProfileContent({
<button
onClick={() => setTab("activity")}
className={cn(
"flex items-center gap-2 px-4 py-2 text-[11px] font-mono uppercase tracking-wider transition-colors cursor-pointer rounded-r-md",
"flex-1 flex items-center justify-center gap-2 px-4 py-2 text-[11px] font-mono uppercase tracking-wider transition-colors cursor-pointer lg:rounded-r-md",
tab === "activity"
? "bg-muted/50 dark:bg-white/4 text-foreground"
: "text-muted-foreground hover:text-foreground/60 hover:bg-muted/60 dark:hover:bg-white/3",
@@ -883,8 +885,8 @@ export function UserProfileContent({
<>
{/* Search & filters */}
<div className="shrink-0">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 mb-3">
<div className="relative flex-1 w-full">
<div className="flex items-center gap-2 lg:mb-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground/50" />
<input
type="text"
@@ -905,7 +907,7 @@ export function UserProfileContent({
null,
);
}}
className="w-full bg-transparent border border-border pl-9 pr-4 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:border-foreground/20 focus:ring-[3px] focus:ring-ring/50 transition-colors rounded-md font-mono"
className="w-full bg-transparent border border-border pl-9 pr-4 py-2 text-base lg:text-sm placeholder:text-muted-foreground focus:outline-none focus:border-foreground/20 focus:ring-[3px] focus:ring-ring/50 transition-colors rounded-none lg:rounded-md font-mono"
/>
</div>
@@ -990,7 +992,7 @@ export function UserProfileContent({
<div className="flex items-start justify-between gap-4 mb-4">
{languages.length > 0 && (
<div className="flex items-center gap-1.5 flex-wrap flex-1 mt-0.5">
<div className="flex items-center gap-1.5 flex-wrap mt-0.5 after:flex-1 after:content-['']">
{topLanguages.map(
(lang) => (
<button
@@ -1122,7 +1124,7 @@ export function UserProfileContent({
)}
</div>
)}
<div className="flex items-center gap-3 shrink-0 ml-auto pt-1">
<div className="hidden lg:flex items-center gap-3 shrink-0 ml-auto pt-1">
{(search ||
languageFilter ||
filter !==
@@ -1144,6 +1146,28 @@ export function UserProfileContent({
</span>
</div>
</div>
{/* Mobile counter & clear row */}
<div className="lg:hidden flex items-center justify-between mb-4">
{(search ||
languageFilter ||
filter !== "all") && (
<button
onClick={
clearRepoFilters
}
aria-label="Clear repository filters"
className="flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground font-mono transition-colors"
>
<X className="w-3 h-3" />
Clear
</button>
)}
<span className="text-[11px] text-muted-foreground/30 font-mono tabular-nums ml-auto">
{filtered.length}/
{repos.length}
</span>
</div>
</div>
{/* Repo list */}
@@ -1152,7 +1176,7 @@ export function UserProfileContent({
<Link
key={repo.id}
href={`/${repo.full_name}`}
className="group flex items-center gap-4 px-4 py-3 hover:bg-muted/60 dark:hover:bg-white/3 transition-colors"
className="group flex items-start md:items-center gap-3 md:gap-4 px-4 py-3 hover:bg-muted/60 dark:hover:bg-white/3 transition-colors"
>
{/* Desktop: Inline layout */}
<div className="hidden sm:contents">
@@ -1164,6 +1188,7 @@ export function UserProfileContent({
repo.name
}
</span>
<div className="flex items-center gap-1.5 flex-wrap">
{repo.private ? (
<RepoBadge type="private" />
) : (
@@ -1176,6 +1201,7 @@ export function UserProfileContent({
<RepoBadge type="fork" />
)}
</div>
</div>
{repo.description && (
<p className="text-[11px] text-muted-foreground/60 mt-1 truncate max-w-lg">
@@ -1184,90 +1210,10 @@ export function UserProfileContent({
}
</p>
)}
</div>
<div className="flex items-center gap-4 shrink-0">
{repo.language && (
<span className="flex items-center gap-1.5 text-[11px] text-muted-foreground/60 font-mono">
<span
className="w-2 h-2 rounded-full"
style={{
backgroundColor:
getLanguageColor(
repo.language,
),
}}
/>
{
repo.language
}
</span>
)}
{repo.stargazers_count >
0 && (
<span className="flex items-center gap-1 text-[11px] text-muted-foreground/60">
<Star className="w-3 h-3" />
{formatNumber(
repo.stargazers_count,
)}
</span>
)}
{repo.forks_count >
0 && (
<span className="flex items-center gap-1 text-[11px] text-muted-foreground/60">
<GitFork className="w-3 h-3" />
{formatNumber(
repo.forks_count,
)}
</span>
)}
{repo.updated_at && (
<span className="text-[11px] text-muted-foreground font-mono w-14 text-right">
<TimeAgo
date={
repo.updated_at
}
/>
</span>
)}
<ChevronRight className="w-3 h-3 text-foreground/10 opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</div>
{/* Mobile: Stacked layout */}
<div className="sm:hidden flex items-start gap-3 w-full">
<FolderGit2 className="w-3.5 h-3.5 text-muted-foreground shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<div className="flex flex-col gap-1">
<span className="text-sm text-foreground group-hover:text-foreground transition-colors font-mono truncate">
{
repo.name
}
</span>
<div className="flex items-center gap-1.5">
{repo.private ? (
<RepoBadge type="private" />
) : (
<RepoBadge type="public" />
)}
{repo.archived && (
<RepoBadge type="archived" />
)}
{repo.fork && (
<RepoBadge type="fork" />
)}
</div>
</div>
{repo.description && (
<p className="text-[11px] text-muted-foreground/60 mt-1 line-clamp-2">
{
repo.description
}
</p>
)}
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 mt-2">
<div className="flex items-center flex-wrap md:flex-nowrap gap-x-3 gap-y-1 md:gap-4 shrink-0">
{repo.language && (
<span className="flex items-center gap-1.5 text-[11px] text-muted-foreground/60 font-mono">
<span
@@ -1303,7 +1249,7 @@ export function UserProfileContent({
</span>
)}
{repo.updated_at && (
<span className="text-[11px] text-muted-foreground font-mono">
<span className="text-[11px] text-muted-foreground font-mono md:w-14 md:text-right md:ml-auto">
<TimeAgo
date={
repo.updated_at
@@ -1311,7 +1257,7 @@ export function UserProfileContent({
/>
</span>
)}
</div>
<ChevronRight className="hidden md:block w-3 h-3 text-foreground/10 opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</div>
</Link>