mirror of
https://github.com/better-auth/better-hub
synced 2026-08-09 12:26:04 +00:00
feat: mobile profile enhancements (#290)
Co-authored-by: Maxwell <145994855+ping-maxwell@users.noreply.github.com>
This commit is contained in:
+1
-1
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user