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
11 changes: 6 additions & 5 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Metadata, Viewport } from "next";
import "./globals.css";
import { GlobalNav } from "@/components/layout/AppChrome";
import { GlobalNav, SiteFooter } from "@/components/layout/AppChrome";
import { auth } from "@/lib/auth";

// NEXT_PUBLIC_APP_URL 이 비어 있으면 Next 기본(요청 host)을 쓴다.
Expand Down Expand Up @@ -42,14 +42,15 @@ export default async function RootLayout({
}) {
const session = await auth();

const approved = Boolean(session?.user.approved);
const isAdmin = session?.user.role === "ADMIN";

return (
<html lang="ko">
<body>
<GlobalNav
approved={Boolean(session?.user.approved)}
isAdmin={session?.user.role === "ADMIN"}
/>
<GlobalNav approved={approved} isAdmin={isAdmin} />
{children}
<SiteFooter approved={approved} isAdmin={isAdmin} />
</body>
</html>
);
Expand Down
1 change: 1 addition & 0 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
return (
<>
<HeroBand
align="center"
eyebrow="SNU Google only"
title="뮤런"
description={
Expand Down
150 changes: 94 additions & 56 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import Link from "next/link";
import { redirect } from "next/navigation";

import { auth, signOut } from "@/lib/auth";
import { db } from "@/lib/db";
import { Button } from "@/components/ui/button";
import {
HeroBand,
PageShell,
StatStrip,
UtilityCard,
} from "@/components/layout/AppChrome";

Expand All @@ -16,6 +18,13 @@ export default async function HomePage() {
if (!session?.user) redirect("/login");
if (!session.user.approved) redirect("/pending");

const [sessionCount, memberCount, distanceSum] = await Promise.all([
db.session.count(),
db.user.count({ where: { approved: true } }),
db.participation.aggregate({ _sum: { distanceKm: true } }),
]);
const totalKm = distanceSum._sum.distanceKm ?? 0;

async function logout() {
"use server";
await signOut({ redirectTo: "/login" });
Expand All @@ -24,85 +33,114 @@ export default async function HomePage() {
return (
<>
<HeroBand
align="center"
eyebrow="Animu Running Archive"
title="뮤런"
description={<>안녕하세요, {session.user.name}. 오늘의 러닝 기록을 남겨요.</>}
description={<>안녕하세요, {session.user.name}. 달린 만큼 남습니다.</>}
actions={
<>
<Button asChild size="lg">
<Link href="/sessions/new">새 세션 만들기</Link>
</Button>
<Button asChild variant="outline" size="lg" className="border-white bg-transparent text-white">
<Button
asChild
variant="outline"
size="lg"
className="border-white bg-transparent text-white"
>
<Link href="/sessions">아카이브 보기</Link>
</Button>
</>
}
>
<div className="grid gap-px overflow-hidden rounded-[18px] border border-white/10 bg-white/10 sm:grid-cols-3">
<HeroStat label="Surface" value="Parchment" />
<HeroStat label="Accent" value="Action Blue" />
<HeroStat label="Rhythm" value="Run log" />
</div>
<StatStrip
items={[
{ label: "Sessions", value: `${sessionCount}회` },
{ label: "Distance", value: `${formatKm(totalKm)} km` },
{ label: "Runners", value: `${memberCount}명` },
]}
/>
</HeroBand>

<PageShell width="content" surface="parchment">
<div className="grid gap-5 md:grid-cols-3">
<HomeCard
eyebrow="Create"
title="새 세션"
body="오늘의 세션을 만들고 사진과 기록을 이어 붙입니다."
href="/sessions/new"
cta="세션 만들기"
/>
<HomeCard
eyebrow="Archive"
title="아카이브"
body="날짜, 장소, 멤버로 지난 러닝의 맥락을 다시 찾습니다."
href="/sessions"
cta="전체 보기"
/>
<HomeCard
eyebrow="Me"
title="내 기록"
body="누적 거리와 페이스 흐름을 한 화면에서 확인합니다."
href="/me"
cta="기록 보기"
/>
</div>

<div className="grid gap-4 md:grid-cols-3">
<UtilityCard className="flex flex-col gap-4">
<span className="font-display text-[28px] font-semibold leading-[1.14] tracking-[0.196px]">Create</span>
<p className="font-text text-sm leading-[1.43] tracking-[-0.224px] text-apple-muted-48">
오늘의 세션을 만들고 사진과 기록을 이어 붙입니다.
</p>
<Button asChild className="mt-auto w-full">
<Link href="/sessions/new">새 세션</Link>
</Button>
</UtilityCard>
<UtilityCard className="flex flex-col gap-4">
<span className="font-display text-[28px] font-semibold leading-[1.14] tracking-[0.196px]">Archive</span>
<p className="font-text text-sm leading-[1.43] tracking-[-0.224px] text-apple-muted-48">
날짜, 장소, 멤버로 지난 러닝의 맥락을 다시 찾습니다.
</p>
<Button asChild variant="outline" className="mt-auto w-full">
<Link href="/sessions">전체 아카이브</Link>
</Button>
</UtilityCard>
<UtilityCard className="flex flex-col gap-4">
<span className="font-display text-[28px] font-semibold leading-[1.14] tracking-[0.196px]">Me</span>
<p className="font-text text-sm leading-[1.43] tracking-[-0.224px] text-apple-muted-48">
누적 거리와 페이스 흐름을 한 화면에서 확인합니다.
</p>
<Button asChild variant="secondary" className="mt-auto w-full">
<Link href="/me">내 기록</Link>
</Button>
</UtilityCard>
</div>

<div className="mt-8 flex items-center justify-center gap-5 font-text text-sm leading-[1.29] tracking-[-0.224px]">
{session.user.role === "ADMIN" && (
<Link href="/admin/members" className="text-apple-primary underline-offset-4 hover:underline">
회원 관리
</Link>
)}
<form action={logout}>
<Button type="submit" variant="ghost" size="sm">
로그아웃
</Button>
</form>
</div>
<div className="mt-10 flex items-center justify-center gap-5 font-text text-sm leading-[1.29] tracking-[-0.224px]">
{session.user.role === "ADMIN" && (
<Link
href="/admin/members"
className="text-apple-primary underline-offset-4 hover:underline"
>
회원 관리
</Link>
)}
<form action={logout}>
<Button type="submit" variant="ghost" size="sm">
로그아웃
</Button>
</form>
</div>
</PageShell>
</>
);
}

function HeroStat({ label, value }: { label: string; value: string }) {
function HomeCard({
eyebrow,
title,
body,
href,
cta,
}: {
eyebrow: string;
title: string;
body: string;
href: string;
cta: string;
}) {
return (
<div className="bg-white/[0.03] p-5">
<span className="block font-text text-xs uppercase leading-none tracking-[-0.12px] text-apple-body-muted">
{label}
<UtilityCard className="flex flex-col gap-3 p-7">
<span className="font-text text-xs font-semibold uppercase leading-none tracking-[0.8px] text-apple-primary">
{eyebrow}
</span>
<span className="mt-2 block font-display text-[21px] font-semibold leading-[1.19] tracking-[0.231px] text-white">
{value}
<span className="font-display text-[28px] font-semibold leading-[1.14] tracking-[-0.28px]">
{title}
</span>
</div>
<p className="font-text text-sm leading-[1.43] tracking-[-0.224px] text-apple-muted-48">
{body}
</p>
<Link
href={href}
className="mt-auto pt-2 font-text text-[15px] leading-[1.29] tracking-[-0.224px] text-apple-primary underline-offset-4 hover:underline"
>
{cta} &rsaquo;
</Link>
</UtilityCard>
);
}

function formatKm(km: number): string {
return km >= 100 ? String(Math.round(km)) : km.toFixed(1);
}
2 changes: 2 additions & 0 deletions app/pending/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export default async function PendingPage() {
return (
<>
<HeroBand
align="center"
size="compact"
eyebrow="Approval required"
title="승인 대기 중"
description={
Expand Down
1 change: 1 addition & 0 deletions app/runners/_components/MemberView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function MemberView({ stats, isSelf }: Props) {
<BackLink href="/sessions">아카이브</BackLink>
</SubNav>
<HeroBand
size="compact"
eyebrow={
<>
{isSelf ? "My running" : "Runner"} · 가입 {formatDate(stats.joinedAt)}
Expand Down
1 change: 1 addition & 0 deletions app/sessions/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export default async function SessionDetailPage({ params }: PageProps) {
</SubNav>

<HeroBand
size="compact"
eyebrow={formatDateHeader(sessionRow.date)}
title={sessionRow.location}
description={metaLine}
Expand Down
22 changes: 10 additions & 12 deletions app/sessions/_components/SessionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ export function SessionCard(props: Props) {
return (
<Link
href={`/sessions/${props.id}`}
className="group block overflow-hidden rounded-[18px] border border-apple-hairline bg-apple-canvas p-3 text-apple-ink transition-transform active:scale-[0.99]"
className="group block overflow-hidden rounded-[18px] border border-apple-hairline bg-apple-canvas text-apple-ink transition-transform active:scale-[0.99]"
>
<div className="relative aspect-video w-full overflow-hidden rounded-lg bg-apple-parchment">
<div className="relative aspect-video w-full overflow-hidden bg-apple-parchment">
{photoSrc ? (
<Image
src={photoSrc}
Expand All @@ -39,17 +39,15 @@ export function SessionCard(props: Props) {
사진 없음
</div>
)}
<span className="absolute right-3 top-3 rounded-full bg-apple-chip/70 px-3 py-1.5 font-text text-xs font-semibold leading-none tracking-[-0.12px] text-apple-ink backdrop-blur-md">
참여 {props.participantCount}명
</span>
</div>
<div className="flex flex-col gap-2 px-1 pt-4">
<div className="flex items-start justify-between gap-3">
<span className="font-text text-[17px] font-semibold leading-[1.24] tracking-[-0.374px]">
{formatDate(props.date)}
</span>
<span className="rounded-full bg-apple-parchment px-3 py-1 font-text text-xs leading-none tracking-[-0.12px] text-apple-muted-48">
참여 {props.participantCount}명
</span>
</div>
<p className="font-display text-[21px] font-semibold leading-[1.19] tracking-[0.231px]">
<div className="flex flex-col gap-1.5 p-5">
<span className="font-text text-sm font-semibold leading-[1.29] tracking-[-0.224px] text-apple-muted-48">
{formatDate(props.date)}
</span>
<p className="font-display text-[24px] font-semibold leading-[1.19] tracking-[-0.28px]">
{props.location}
</p>
<p className="font-text text-sm leading-[1.43] tracking-[-0.224px] text-apple-muted-48">
Expand Down
43 changes: 10 additions & 33 deletions app/sessions/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Link from "next/link";

import { Button } from "@/components/ui/button";
import { db } from "@/lib/db";
import { requireApproved } from "@/lib/guard";
import { listApprovedMembers, listSessions } from "@/lib/sessions";
import {
Expand Down Expand Up @@ -33,9 +34,10 @@ export default async function SessionsArchivePage({ searchParams }: PageProps) {
const sp = await searchParams;
const { cursorId, filters, hasActiveFilters } = parseSessionArchiveParams(sp);

const [page, members] = await Promise.all([
const [page, members, totalCount] = await Promise.all([
listSessions({ cursorId, filters }),
listApprovedMembers(),
db.session.count(),
]);

const nextHref =
Expand All @@ -52,20 +54,18 @@ export default async function SessionsArchivePage({ searchParams }: PageProps) {
</SubNav>

<HeroBand
size="compact"
eyebrow="Sessions"
title="러닝 아카이브"
description="날짜, 장소, 참여 멤버로 세션을 찾아보세요."
actions={
<Button asChild size="lg">
<Link href="/sessions/new">새 세션 만들기</Link>
</Button>
description={
<>
지금까지 {totalCount}번 달렸습니다. 날짜, 장소, 참여 멤버로 세션을
찾아보세요.
</>
}
>
<ArchiveSummary />
</HeroBand>
/>

<PageShell width="content" surface="parchment">

<FilterBar
q={filters.q ?? ""}
memberIds={filters.memberIds ?? []}
Expand Down Expand Up @@ -121,26 +121,3 @@ function EmptyFilterResult() {
</UtilityCard>
);
}

function ArchiveSummary() {
return (
<div className="grid gap-px overflow-hidden rounded-[18px] border border-white/10 bg-white/10 sm:grid-cols-3">
<SummaryCell label="Surface" value="Dark tile" />
<SummaryCell label="Flow" value="Filter first" />
<SummaryCell label="Focus" value="Action Blue" />
</div>
);
}

function SummaryCell({ label, value }: { label: string; value: string }) {
return (
<div className="bg-white/[0.03] p-5">
<span className="block font-text text-xs uppercase leading-none tracking-[-0.12px] text-apple-body-muted">
{label}
</span>
<span className="mt-2 block font-display text-[21px] font-semibold leading-[1.19] tracking-[0.231px] text-white">
{value}
</span>
</div>
);
}
Loading
Loading