Layout
This commit is contained in:
@@ -2,13 +2,16 @@ package com.peakup.activity.model;
|
||||
|
||||
import com.peakup.common.model.ReadonlyEntity;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.PostLoad;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Transient;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.hibernate.annotations.Immutable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Entity
|
||||
@@ -18,9 +21,28 @@ import java.util.Map;
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ActivitySummary extends AbstractActivity implements ReadonlyEntity {
|
||||
|
||||
@Column(name = "training_load")
|
||||
public @Nullable Double trainingLoad;
|
||||
|
||||
@Column(name = "ctl")
|
||||
public @Nullable Double ctl;
|
||||
|
||||
@Column(name = "atl")
|
||||
public @Nullable Double atl;
|
||||
|
||||
@Transient
|
||||
private @Nullable Map<String, String> valuesForSummary;
|
||||
|
||||
@PostLoad
|
||||
void populateValuesForSummary() {
|
||||
final Map<String, String> map = new HashMap<>();
|
||||
if (distanceM != null) map.put("distanceM", String.valueOf(distanceM));
|
||||
if (trainingLoad != null) map.put("trainingLoad", String.valueOf(trainingLoad));
|
||||
if (ctl != null) map.put("ctl", String.valueOf(ctl));
|
||||
if (atl != null) map.put("atl", String.valueOf(atl));
|
||||
valuesForSummary = map;
|
||||
}
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
public ActivitySummary() {
|
||||
// hibernate
|
||||
|
||||
@@ -49,7 +49,7 @@ public class Scheduler {
|
||||
@Inject
|
||||
ScheduledTaskService scheduledTaskService;
|
||||
|
||||
@Scheduled(every = "20m")
|
||||
@Scheduled(every = "10m")
|
||||
@RunOnVirtualThread
|
||||
void importActivities() {
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'
|
||||
import { Sidebar } from '@/components/Sidebar'
|
||||
import { BottomNav } from '@/components/BottomNav'
|
||||
import { LoginDialog } from '@/components/LoginDialog'
|
||||
import { Dashboard } from '@/pages/Dashboard'
|
||||
import { Activities } from '@/pages/Activities'
|
||||
@@ -53,9 +54,12 @@ function AppContent() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full min-h-screen">
|
||||
<div className="flex flex-col sm:flex-row w-full min-h-screen">
|
||||
<Sidebar currentPage={page} onNavigate={setPage} athlete={athlete} onLogout={handleLogout} />
|
||||
<main className="flex-1 flex overflow-hidden">{renderPage()}</main>
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<main className="flex-1 flex overflow-hidden min-h-0">{renderPage()}</main>
|
||||
<BottomNav currentPage={page} onNavigate={setPage} onLogout={handleLogout} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
45
peakup-frontend/src/components/BottomNav.tsx
Normal file
45
peakup-frontend/src/components/BottomNav.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Activity, Calendar, Home, LogOut, Settings } from 'lucide-react'
|
||||
|
||||
const bottomNavItems = [
|
||||
{ icon: <Home size={20} />, label: 'Home', page: 'dashboard' },
|
||||
{ icon: <Activity size={20} />, label: 'Activities', page: 'activities' },
|
||||
{ icon: <Calendar size={20} />, label: 'Plan', page: 'calendar' },
|
||||
{ icon: <Settings size={20} />, label: 'Settings', page: 'settings' },
|
||||
]
|
||||
|
||||
interface BottomNavProps {
|
||||
currentPage: string
|
||||
onNavigate: (page: string) => void
|
||||
onLogout: () => void
|
||||
}
|
||||
|
||||
export function BottomNav({ currentPage, onNavigate, onLogout }: BottomNavProps) {
|
||||
return (
|
||||
<nav className="flex sm:hidden bg-white border-t border-zinc-200 shrink-0">
|
||||
{bottomNavItems.map((item) => {
|
||||
const active = currentPage === item.page
|
||||
return (
|
||||
<button
|
||||
key={item.page}
|
||||
onClick={() => onNavigate(item.page)}
|
||||
className={[
|
||||
'flex-1 flex flex-col items-center justify-center gap-1 py-3',
|
||||
'text-[10px] font-semibold tracking-wider transition-colors',
|
||||
active ? 'text-red-500' : 'text-zinc-400 hover:text-zinc-700',
|
||||
].join(' ')}
|
||||
>
|
||||
{item.icon}
|
||||
<span className={active ? 'gradient-text' : ''}>{item.label}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="flex-1 flex flex-col items-center justify-center gap-1 py-3 text-[10px] font-semibold tracking-wider text-zinc-400 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<LogOut size={20} />
|
||||
<span>Sign out</span>
|
||||
</button>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -27,7 +27,7 @@ interface SidebarProps {
|
||||
|
||||
export function Sidebar({currentPage, onNavigate, athlete, onLogout}: SidebarProps) {
|
||||
return (
|
||||
<aside className="w-56 min-h-screen border-r border-zinc-200 bg-white flex flex-col shrink-0">
|
||||
<aside className="hidden sm:flex flex-col w-56 min-h-screen border-r border-zinc-200 bg-white shrink-0">
|
||||
{/* Logo */}
|
||||
<div className="px-6 py-6 border-b border-zinc-200 flex justify-center">
|
||||
<PeakupLogo className="h-24 w-auto"/>
|
||||
|
||||
@@ -3,5 +3,8 @@
|
||||
import type { AbstractActivityDo } from './AbstractActivityDo';
|
||||
|
||||
export interface ActivitySummaryDo extends AbstractActivityDo {
|
||||
trainingLoad: number | null;
|
||||
ctl: number | null;
|
||||
atl: number | null;
|
||||
valuesForSummary: Record<string, string> | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user