Skip to content

Commit 5911199

Browse files
committed
Polish multilingual UI and route motion
1 parent 25ae3ae commit 5911199

9 files changed

Lines changed: 218 additions & 36 deletions

File tree

apps/web/src/app/activities/[id]/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export default function ActivityPosterPage() {
182182
padding={62}
183183
strokeWidth={8}
184184
maxPoints={520}
185+
loop
185186
label={`${activity.name || 'Activity'} poster route`}
186187
/>
187188
</div>

apps/web/src/app/activities/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,8 @@ export default function ActivitiesPage() {
117117
) : null}
118118

119119
{!isLoading && !error && activities.length > 0 ? (
120-
<div className="overflow-x-auto">
121-
<table className="min-w-full text-sm">
120+
<div className="overscroll-x-contain overflow-x-auto">
121+
<table className="min-w-[44rem] whitespace-nowrap text-sm">
122122
<thead>
123123
<tr className="border-b border-slate-200 text-left text-[var(--text-muted)] dark:border-white/10">
124124
<th className="py-2 pr-4 font-medium">{t('common.date')}</th>

apps/web/src/app/dashboard/CyberDashboard.tsx

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useMemo, useState } from 'react'
3+
import { useEffect, useMemo, useState } from 'react'
44
import Image from 'next/image'
55
import Link from 'next/link'
66
import {
@@ -64,6 +64,16 @@ function formatActivityDate(value: string | undefined, dateLocale: string, unkno
6464
return date.toLocaleDateString(dateLocale, { month: 'short', day: 'numeric', year: 'numeric' })
6565
}
6666

67+
function formatLedgerDate(value: string | undefined, dateLocale: string, unknownLabel: string) {
68+
if (!value) return unknownLabel
69+
const date = new Date(value)
70+
if (Number.isNaN(date.getTime())) return unknownLabel
71+
const isCjkLocale = dateLocale.startsWith('zh') || dateLocale.startsWith('ja')
72+
return date.toLocaleDateString(dateLocale, isCjkLocale
73+
? { month: 'short', day: 'numeric' }
74+
: { month: 'short', day: 'numeric', year: 'numeric' })
75+
}
76+
6777
function formatWeekday(value: string | undefined, dateLocale: string) {
6878
if (!value) return '—'
6979
const date = new Date(value)
@@ -148,6 +158,33 @@ function TotalMetric({ label, value, icon: Icon }: { label: string; value: strin
148158
)
149159
}
150160

161+
function DesktopRunnerMuse() {
162+
const [shouldRender, setShouldRender] = useState(false)
163+
164+
useEffect(() => {
165+
const desktopQuery = window.matchMedia('(min-width: 1280px)')
166+
const updateVisibility = () => setShouldRender(desktopQuery.matches)
167+
updateVisibility()
168+
desktopQuery.addEventListener('change', updateVisibility)
169+
return () => desktopQuery.removeEventListener('change', updateVisibility)
170+
}, [])
171+
172+
if (!shouldRender) return null
173+
174+
return (
175+
<Image
176+
src={runnerMuseCameos.dashboardHero.src}
177+
alt=""
178+
width={410}
179+
height={615}
180+
sizes="205px"
181+
loading="eager"
182+
fetchPriority="high"
183+
className="hero-runner-float pointer-events-none absolute z-20 hidden object-contain drop-shadow-[0_22px_32px_rgba(0,0,0,0.34)] xl:-bottom-[230px] xl:left-0 xl:block xl:h-[290px] xl:w-[205px]"
184+
/>
185+
)
186+
}
187+
151188
export function CyberDashboard() {
152189
const { t, dateLocale } = useI18n()
153190
const currentYear = new Date().getFullYear()
@@ -214,7 +251,7 @@ export function CyberDashboard() {
214251
<div className="relative min-w-0 px-1 pb-10 pt-3 sm:px-2 lg:px-5 lg:pt-7 xl:px-8">
215252
<div className="grid items-start gap-4 lg:grid-cols-[minmax(450px,1.15fr)_minmax(330px,0.85fr)]">
216253
<div>
217-
<h1 className="max-w-[620px] text-[2.35rem] font-black leading-[1.02] tracking-[-0.055em] text-[var(--text-strong)] sm:text-[clamp(2.6rem,3vw,2.85rem)]">
254+
<h1 className="dashboard-display-title max-w-[620px] text-[var(--text-strong)]">
218255
{t('dashboard.headline')}
219256
</h1>
220257
<p className="mt-4 max-w-[590px] text-sm leading-6 text-[var(--text-muted)] sm:mt-5 sm:text-base sm:leading-7">
@@ -272,6 +309,7 @@ export function CyberDashboard() {
272309
padding={64}
273310
maxPoints={420}
274311
strokeWidth={6}
312+
loop
275313
label={`${selectedActivity.name || selectedLocation} ${t('dashboard.routeShape')}`}
276314
/>
277315
</div>
@@ -292,14 +330,7 @@ export function CyberDashboard() {
292330
</div>
293331
)}
294332

295-
<Image
296-
src={runnerMuseCameos.dashboardHero.src}
297-
alt=""
298-
width={410}
299-
height={615}
300-
sizes="205px"
301-
className="hero-runner-float pointer-events-none absolute z-20 hidden object-contain drop-shadow-[0_22px_32px_rgba(0,0,0,0.34)] xl:-bottom-[230px] xl:left-0 xl:block xl:h-[290px] xl:w-[205px]"
302-
/>
333+
<DesktopRunnerMuse />
303334
</div>
304335
</div>
305336

@@ -342,7 +373,7 @@ export function CyberDashboard() {
342373
</select>
343374
</div>
344375

345-
<div className="mt-8 hidden grid-cols-[88px_minmax(144px,1fr)_62px_62px_58px] gap-1.5 px-3 text-[10px] font-semibold uppercase tracking-[0.12em] text-[var(--text-muted)] md:grid 2xl:grid-cols-[88px_minmax(180px,1fr)_78px_76px_70px] 2xl:gap-3">
376+
<div className="mt-8 hidden grid-cols-[88px_minmax(144px,1fr)_62px_62px_68px] gap-1.5 px-3 text-[10px] font-semibold uppercase tracking-[0.12em] text-[var(--text-muted)] md:grid 2xl:grid-cols-[88px_minmax(180px,1fr)_78px_76px_70px] 2xl:gap-3">
346377
<span>{t('common.date')}</span>
347378
<span>{t('dashboard.routeColumn')}</span>
348379
<span className="text-right">{t('common.distance')}</span>
@@ -371,7 +402,7 @@ export function CyberDashboard() {
371402
aria-pressed={isSelected}
372403
>
373404
<span className="min-w-0">
374-
<span className="block truncate text-xs text-[var(--text-strong)]">{formatActivityDate(activityDate(activity), dateLocale, t('common.unknownDate'))}</span>
405+
<span className="block truncate text-xs text-[var(--text-strong)]">{formatLedgerDate(activityDate(activity), dateLocale, t('common.unknownDate'))}</span>
375406
<span className="mt-1 block text-[11px] text-[var(--text-muted)]">{formatWeekday(activityDate(activity), dateLocale)}</span>
376407
</span>
377408
<span className="grid min-w-0 grid-cols-[60px_1fr] items-center gap-3">

apps/web/src/app/globals.css

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
--route-purple: #7b5fd6;
2828
--route-red: #d84f4f;
2929
--route-canvas: #eef3f7;
30+
--route-grid: rgba(15, 23, 42, 0.08);
31+
--route-ghost: rgba(100, 116, 139, 0.44);
32+
--route-empty: rgba(71, 85, 105, 0.66);
3033
--radius-lg: 16px;
3134
--radius-md: 10px;
3235
}
@@ -56,6 +59,9 @@
5659
--route-purple: #aa7dff;
5760
--route-red: #ff5c5c;
5861
--route-canvas: #081113;
62+
--route-grid: rgba(139, 154, 147, 0.12);
63+
--route-ghost: rgba(139, 154, 147, 0.34);
64+
--route-empty: rgba(238, 244, 233, 0.62);
5965
}
6066

6167
@layer base {
@@ -205,7 +211,11 @@
205211

206212
.section-subtitle {
207213
color: var(--text-muted);
208-
@apply mt-1 text-sm;
214+
@apply mt-1 text-sm leading-6;
215+
}
216+
217+
.dashboard-display-title {
218+
@apply text-[2.35rem] font-black leading-[1.02] tracking-[-0.055em] sm:text-[clamp(2.6rem,3vw,2.85rem)];
209219
}
210220

211221
.route-atlas-label {
@@ -236,7 +246,7 @@
236246

237247
.route-ledger-row {
238248
display: grid;
239-
grid-template-columns: 5.5rem minmax(9rem, 1fr) 3.875rem 3.875rem 3.625rem;
249+
grid-template-columns: 5.5rem minmax(9rem, 1fr) 3.875rem 3.875rem 4.25rem;
240250
align-items: center;
241251
width: 100%;
242252
min-height: 82px;
@@ -269,7 +279,21 @@
269279
.route-glyph-path {
270280
stroke-dasharray: 1;
271281
stroke-dashoffset: 1;
272-
animation: draw-route 900ms cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
282+
animation: draw-route 1600ms cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
283+
}
284+
285+
.route-glyph-path-loop,
286+
.route-glyph-glow-loop {
287+
stroke-dasharray: 1;
288+
stroke-dashoffset: 1;
289+
}
290+
291+
.route-glyph-path-loop {
292+
animation: draw-route-loop 7.6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
293+
}
294+
295+
.route-glyph-glow-loop {
296+
animation: draw-route-glow-loop 7.6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
273297
}
274298

275299
.route-constellation-runner,
@@ -301,6 +325,30 @@
301325
}
302326
}
303327

328+
html:lang(zh) .dashboard-display-title,
329+
html:lang(zh-CN) .dashboard-display-title,
330+
html:lang(ja) .dashboard-display-title {
331+
font-weight: 800;
332+
letter-spacing: -0.035em;
333+
line-height: 1.08;
334+
}
335+
336+
@media (min-width: 1380px) and (max-width: 1599px) {
337+
html:lang(ja) .desktop-nav-group {
338+
gap: 0.125rem;
339+
padding-inline: 0.625rem;
340+
}
341+
342+
html:lang(ja) .desktop-nav-group-label {
343+
display: none;
344+
}
345+
346+
html:lang(ja) .desktop-nav-link {
347+
padding-inline: 0.5rem;
348+
font-size: 0.75rem;
349+
}
350+
}
351+
304352
@media (max-width: 767px) {
305353
.action-primary,
306354
.action-secondary,
@@ -327,12 +375,71 @@
327375
}
328376
}
329377

378+
@media (max-width: 639px) {
379+
html:lang(zh) .dashboard-display-title,
380+
html:lang(zh-CN) .dashboard-display-title,
381+
html:lang(ja) .dashboard-display-title {
382+
font-size: 2.15rem;
383+
line-height: 1.06;
384+
}
385+
}
386+
330387
@keyframes draw-route {
331388
to {
332389
stroke-dashoffset: 0;
333390
}
334391
}
335392

393+
@keyframes draw-route-loop {
394+
0%,
395+
6% {
396+
opacity: 0;
397+
stroke-dashoffset: 1;
398+
}
399+
400+
12% {
401+
opacity: 1;
402+
stroke-dashoffset: 1;
403+
}
404+
405+
64%,
406+
82% {
407+
opacity: 1;
408+
stroke-dashoffset: 0;
409+
}
410+
411+
94%,
412+
100% {
413+
opacity: 0;
414+
stroke-dashoffset: 0;
415+
}
416+
}
417+
418+
@keyframes draw-route-glow-loop {
419+
0%,
420+
6% {
421+
opacity: 0;
422+
stroke-dashoffset: 1;
423+
}
424+
425+
12% {
426+
opacity: 0.04;
427+
stroke-dashoffset: 1;
428+
}
429+
430+
64%,
431+
82% {
432+
opacity: 0.16;
433+
stroke-dashoffset: 0;
434+
}
435+
436+
94%,
437+
100% {
438+
opacity: 0;
439+
stroke-dashoffset: 0;
440+
}
441+
}
442+
336443
@keyframes route-constellation-pass {
337444
0% {
338445
opacity: 0;
@@ -388,4 +495,16 @@
388495
.hero-runner-float {
389496
animation: none !important;
390497
}
498+
499+
.route-glyph-path-loop {
500+
animation: none !important;
501+
opacity: 1 !important;
502+
stroke-dashoffset: 0 !important;
503+
}
504+
505+
.route-glyph-glow-loop {
506+
animation: none !important;
507+
opacity: 0.16 !important;
508+
stroke-dashoffset: 0 !important;
509+
}
391510
}

apps/web/src/app/map/page.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
'use client'
22

33
import { useMemo, useState } from 'react'
4+
import dynamic from 'next/dynamic'
45
import { useActivities, useActivitySummary } from '@/lib/hooks/useActivities'
5-
import RunningMap from '@/components/maps/RunningMap'
6-
import WaterfallMapView from '@/components/maps/WaterfallMapView'
76
import { formatDistance, formatDuration, ActivityType } from '@/lib/database/models/Activity'
87
import { getActivityConfig, shouldShowOnMap } from '@/lib/config/activities'
98
import { getDefaultActivityTypes } from '@/lib/config/activityTypes'
@@ -23,6 +22,16 @@ interface Activity {
2322

2423
type ViewMode = 'map' | 'waterfall'
2524

25+
const RunningMap = dynamic(() => import('@/components/maps/RunningMap'), {
26+
ssr: false,
27+
loading: () => <div className="h-[620px] animate-pulse rounded-lg bg-[var(--bg-2)]" aria-hidden="true" />,
28+
})
29+
30+
const WaterfallMapView = dynamic(() => import('@/components/maps/WaterfallMapView'), {
31+
ssr: false,
32+
loading: () => <div className="h-[480px] animate-pulse rounded-lg bg-[var(--bg-2)]" aria-hidden="true" />,
33+
})
34+
2635
function Metric({ label, value }: { label: string; value: string }) {
2736
return (
2837
<div className="min-w-0 bg-[var(--surface)] px-3 py-4 xl:px-5">

apps/web/src/app/stats/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ function RouteDiversity({ activities }: { activities: ActivityLike[] }) {
297297
padding={26}
298298
strokeWidth={4}
299299
maxPoints={140}
300+
animate={false}
300301
label={`${activity.name || 'Route'} diversity sample`}
301302
/>
302303
</div>

apps/web/src/components/AppLayout.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,10 @@ function DesktopNavigation({ pathname }: { pathname: string }) {
155155
const { t } = useI18n()
156156

157157
return (
158-
<nav className="hidden min-w-0 flex-1 items-stretch min-[1380px]:flex" aria-label={t('shell.primaryNavigation')}>
158+
<nav className="desktop-navigation hidden min-w-0 flex-1 items-stretch min-[1380px]:flex" aria-label={t('shell.primaryNavigation')}>
159159
{navGroups.map((group) => (
160-
<div key={group.labelKey} className="flex items-center gap-1 border-l border-[var(--line)] px-4 first:border-l-0">
161-
<span className="mr-2 text-[10px] font-semibold uppercase tracking-[0.16em] text-[var(--text-muted)]">
160+
<div key={group.labelKey} className="desktop-nav-group flex items-center gap-1 border-l border-[var(--line)] px-4 first:border-l-0">
161+
<span className="desktop-nav-group-label mr-2 shrink-0 whitespace-nowrap text-[10px] font-semibold uppercase tracking-[0.16em] text-[var(--text-muted)]">
162162
{t(group.labelKey)}
163163
</span>
164164
{group.items.map((item) => {
@@ -169,7 +169,7 @@ function DesktopNavigation({ pathname }: { pathname: string }) {
169169
href={item.href}
170170
aria-current={isActive ? 'page' : undefined}
171171
className={classNames(
172-
'relative inline-flex h-11 items-center whitespace-nowrap px-2.5 text-[13px] font-medium transition-colors after:absolute after:inset-x-2 after:bottom-0 after:h-px after:scale-x-0 after:bg-[var(--route-green)] after:transition-transform focus-visible:rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--route-green)]',
172+
'desktop-nav-link relative inline-flex h-11 items-center whitespace-nowrap px-2.5 text-[13px] font-medium transition-colors after:absolute after:inset-x-2 after:bottom-0 after:h-px after:scale-x-0 after:bg-[var(--route-green)] after:transition-transform focus-visible:rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--route-green)]',
173173
isActive
174174
? 'text-[var(--text-strong)] after:scale-x-100'
175175
: 'text-[var(--text-muted)] hover:text-[var(--text-strong)]'

0 commit comments

Comments
 (0)