feat: implement Sleepy sleep diary
This commit is contained in:
28
app/api/entries/[date]/route.ts
Normal file
28
app/api/entries/[date]/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getEntry, saveEntry } from '@/lib/db';
|
||||
import type { SleepEntry } from '@/lib/sleep';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
function isDate(value: string): boolean {
|
||||
return /^\d{4}-\d{2}-\d{2}$/.test(value);
|
||||
}
|
||||
|
||||
export async function GET(_: Request, { params }: { params: Promise<{ date: string }> }) {
|
||||
const { date } = await params;
|
||||
if (!isDate(date)) return NextResponse.json({ error: 'Некорректная дата' }, { status: 400 });
|
||||
|
||||
return NextResponse.json({ entry: getEntry(date) });
|
||||
}
|
||||
|
||||
export async function PUT(request: Request, { params }: { params: Promise<{ date: string }> }) {
|
||||
const { date } = await params;
|
||||
if (!isDate(date)) return NextResponse.json({ error: 'Некорректная дата' }, { status: 400 });
|
||||
|
||||
const entry = await request.json() as SleepEntry;
|
||||
if (entry.date !== date || entry.rating < 1 || entry.rating > 5 || entry.awakenings < 0) {
|
||||
return NextResponse.json({ error: 'Проверьте заполненные значения' }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ entry: saveEntry(entry) });
|
||||
}
|
||||
9
app/globals.css
Normal file
9
app/globals.css
Normal file
@@ -0,0 +1,9 @@
|
||||
:root { --bg:#f5f4ed; --surface:#faf9f5; --warm:#e8e6dc; --ink:#141413; --text:#3d3d3a; --muted:#78776f; --line:#e3e1d7; --accent:#c96442; --focus:#3898ec; }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; background:var(--bg); color:var(--ink); font-family:Arial, system-ui, sans-serif; line-height:1.55; }
|
||||
button,input,select,textarea { font:inherit; } button { cursor:pointer; } button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible { outline:3px solid color-mix(in srgb, var(--focus) 35%, transparent); outline-offset:2px; }
|
||||
.container { width:min(1200px, calc(100% - 32px)); margin:auto; }.hero { display:grid; grid-template-columns:1fr 380px; gap:48px; align-items:start; padding:34px 0 28px; }.brand { display:flex; align-items:center; gap:12px; padding-top:10px; }.brand-mark { display:grid; place-items:center; width:42px; height:42px; border-radius:50%; background:var(--ink); color:var(--bg); font:24px Georgia,serif; }.brand h1,h2 { margin:0; font-family:Georgia,serif; font-weight:500; letter-spacing:-.03em; }.brand h1 { font-size:28px; }.brand p { margin:0; color:var(--muted); font-size:12px; text-transform:uppercase; letter-spacing:.1em; }
|
||||
.date-card,.form-card { background:var(--surface); border:1px solid var(--line); box-shadow:0 5px 24px #0000000c; border-radius:16px; }.date-card { padding:20px; display:grid; grid-template-columns:1fr auto; gap:8px 18px; }.date-card h2 { font-size:20px; }.eyebrow,.badge { color:var(--muted); font-size:11px; letter-spacing:.09em; text-transform:uppercase; }.date-card p { grid-column:1/-1; margin:6px 0 0; color:var(--muted); font-size:13px; }.date-card strong { color:var(--text); }.date-controls { display:flex; gap:8px; }.date-controls button,.stepper button { width:42px; height:42px; border:1px solid var(--line); border-radius:8px; color:var(--ink); background:var(--surface); font-size:20px; }.date-controls button:hover,.stepper button:hover { background:var(--warm); }.progress { grid-column:1/-1; height:7px; overflow:hidden; border-radius:99px; background:var(--warm); }.progress span { display:block; height:100%; border-radius:inherit; background:var(--text); transition:width .2s ease; }
|
||||
.layout { display:grid; grid-template-columns:minmax(0,1fr) 380px; align-items:start; gap:28px; padding-bottom:48px; }.form-card { padding:0 28px; }.step { padding:28px 0; border-bottom:1px solid var(--line); }.step:last-child { border:0; }.step-heading { display:flex; gap:16px; margin-bottom:18px; }.step-heading>span { display:grid; flex:0 0 auto; place-items:center; width:36px; height:36px; border:1px solid var(--line); border-radius:50%; background:var(--warm); color:var(--text); font-family:Georgia,serif; }.step h2 { font-size:24px; }.step-heading p { margin:5px 0 0; color:var(--muted); font-size:14px; }.two-columns { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-top:16px; }.field { display:block; margin-top:16px; color:var(--text); font-size:14px; font-weight:600; }.field span { display:block; margin-bottom:7px; }.field input,.field select,.field textarea { width:100%; min-height:48px; padding:11px 13px; color:var(--ink); background:var(--surface); border:1px solid var(--line); border-radius:12px; }.field textarea { resize:vertical; }.segmented { display:inline-flex; gap:4px; padding:4px; border:1px solid var(--line); border-radius:999px; background:var(--warm); }.segmented button { min-width:78px; min-height:39px; padding:0 16px; color:var(--muted); border:0; border-radius:999px; background:transparent; }.segmented button.active { color:var(--bg); background:var(--ink); }.chips { display:flex; flex-wrap:wrap; gap:8px; margin-top:14px; }.chips button { min-height:38px; padding:0 13px; border:1px solid var(--line); border-radius:99px; color:var(--text); background:var(--surface); }.chips button.selected,.rating button.selected { color:var(--bg); background:var(--ink); border-color:var(--ink); }.stepper { display:flex; align-items:center; gap:10px; }.stepper output { min-width:48px; text-align:center; font:28px Georgia,serif; }.stepper span { margin:0; color:var(--muted); font-weight:400; font-size:13px; }.rating { display:grid; grid-template-columns:repeat(5,1fr); gap:8px; }.rating button { min-height:74px; display:flex; flex-direction:column; justify-content:center; align-items:center; gap:3px; padding:8px; color:var(--text); border:1px solid var(--line); border-radius:12px; background:var(--surface); }.rating b { font:23px Georgia,serif; }.rating span { font-size:11px; color:var(--muted); }.rating button.selected span { color:var(--bg); }
|
||||
.summary { position:sticky; top:24px; padding:28px; color:var(--bg); border-radius:16px; background:var(--ink); box-shadow:0 5px 24px #00000018; }.summary .badge { display:inline-block; padding:5px 10px; color:var(--bg); border-radius:99px; background:#ffffff1c; }.summary h2 { margin-top:12px; font-size:27px; }.summary>p { margin:4px 0; color:#dad8ce; font-size:14px; }.efficiency { display:flex; align-items:center; gap:18px; margin:21px 0; padding:16px; border:1px solid #ffffff18; border-radius:12px; background:#ffffff0b; }.ring { --progress:0deg; display:grid; place-items:center; width:100px; aspect-ratio:1; border-radius:50%; background:conic-gradient(var(--bg) var(--progress), #ffffff2b 0); }.ring::before { content:''; position:absolute; width:78px; aspect-ratio:1; border-radius:50%; background:var(--ink); }.ring span { z-index:1; font:22px Georgia,serif; }.efficiency div:last-child span { color:#c5c2b8; font-size:12px; }.efficiency strong { display:block; font:28px Georgia,serif; }.summary dl { margin:0; }.summary dl div { display:grid; grid-template-columns:1fr auto; gap:12px; padding:9px 0; font-size:14px; }.summary dt { color:#c5c2b8; }.summary dd { margin:0; font-family:ui-monospace,monospace; }.summary .total { margin-top:4px; padding-top:14px; border-top:1px solid #ffffff21; }.summary .total dd { font-weight:700; }.interpretation { margin:18px 0!important; padding:12px; color:#edeae1!important; border-radius:8px; background:#ffffff12; }.save { width:100%; min-height:48px; color:var(--surface); border:0; border-radius:8px; background:var(--accent); font-weight:700; }.save:disabled { cursor:wait; opacity:.7; }.save-status { min-height:20px; margin-top:8px!important; text-align:center; }.save-status.error { color:#ffb3a1; }.save-status.saved { color:#9ee0bd; } footer { padding:0 0 38px; color:var(--muted); font-size:13px; }
|
||||
@media (max-width:980px) { .hero,.layout { grid-template-columns:1fr; gap:20px; }.summary { position:static; }.date-card { max-width:560px; } } @media (max-width:600px) { .container { width:min(100% - 24px, 1200px); }.hero { padding-top:18px; }.form-card { padding:0 18px; }.step { padding:23px 0; }.two-columns { grid-template-columns:1fr; gap:0; }.rating { gap:5px; }.rating button { min-height:56px; }.rating span { display:none; }.summary { padding:21px; }.date-card { padding:16px; } } @media (prefers-reduced-motion:reduce) { *,*::before,*::after { scroll-behavior:auto!important; transition:none!important; } }
|
||||
17
app/layout.tsx
Normal file
17
app/layout.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import { ServiceWorker } from '@/components/ServiceWorker';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Sleepy - дневник сна',
|
||||
description: 'Простой дневник сна для наблюдения за динамикой.',
|
||||
manifest: '/manifest.webmanifest',
|
||||
icons: { icon: '/favicon.svg', apple: '/favicon.svg' },
|
||||
appleWebApp: { capable: true, title: 'Sleepy', statusBarStyle: 'default' },
|
||||
};
|
||||
|
||||
export const viewport: Viewport = { themeColor: '#f5f4ed' };
|
||||
|
||||
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
||||
return <html lang="ru"><body><ServiceWorker />{children}</body></html>;
|
||||
}
|
||||
5
app/page.tsx
Normal file
5
app/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SleepDiary } from '@/components/SleepDiary';
|
||||
|
||||
export default function Home() {
|
||||
return <SleepDiary />;
|
||||
}
|
||||
Reference in New Issue
Block a user