V 2.0.0 ferige Version
This commit is contained in:
23
app/api/data/route.ts
Normal file
23
app/api/data/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { doMySQL } from '@/lib/mysqlinterface';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const options = {
|
||||
curdate: searchParams.get('curdate') ?? undefined,
|
||||
testing: searchParams.get('test') ?? undefined,
|
||||
};
|
||||
const erg = await doMySQL('getlastdata', options);
|
||||
return NextResponse.json(erg);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const body = await request.json();
|
||||
const options = {
|
||||
data: body,
|
||||
testing: searchParams.get('test') ?? undefined,
|
||||
};
|
||||
const erg = await doMySQL('putdata', options);
|
||||
return NextResponse.json(erg);
|
||||
}
|
||||
235
app/components/SpritzClient.tsx
Normal file
235
app/components/SpritzClient.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
interface DataItem {
|
||||
day: string;
|
||||
status: boolean;
|
||||
einheit: number;
|
||||
}
|
||||
|
||||
interface Schema {
|
||||
curdate: string;
|
||||
months: string[];
|
||||
years: string[];
|
||||
data: DataItem[];
|
||||
einheit: number;
|
||||
}
|
||||
|
||||
export interface SysParams {
|
||||
testing: boolean;
|
||||
doinit: boolean;
|
||||
einheit: number;
|
||||
version: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
sysParams: SysParams;
|
||||
}
|
||||
|
||||
function CellContent({ day, einheit }: { day: DateTime; einheit: number }) {
|
||||
return (
|
||||
<div className="inner">
|
||||
{day.toFormat('d')}
|
||||
<div className="lowline small">
|
||||
<div className="wtg">{day.setLocale('de').toFormat('ccc')}</div>
|
||||
<div className="eh">{einheit !== 0 ? einheit : ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function buildMonthsLabel(s: Schema): string {
|
||||
let months = s.months.join(' - ');
|
||||
months += ' ';
|
||||
months += s.years.join('/');
|
||||
return months;
|
||||
}
|
||||
|
||||
export default function SpritzClient({ sysParams }: Props) {
|
||||
const [schema, setSchema] = useState<Schema | null>(null);
|
||||
const [curEinheit, setCurEinheit] = useState<number>(sysParams.einheit);
|
||||
const [monthsLabel, setMonthsLabel] = useState<string>('');
|
||||
const [einheitInput, setEinheitInput] = useState<number>(sysParams.einheit);
|
||||
|
||||
const schemaRef = useRef<Schema | null>(null);
|
||||
const curEinheitRef = useRef<number>(sysParams.einheit);
|
||||
|
||||
schemaRef.current = schema;
|
||||
curEinheitRef.current = curEinheit;
|
||||
|
||||
const apiUrl = sysParams.testing ? '/api/data?test=true' : '/api/data';
|
||||
|
||||
async function fetchData(): Promise<{ data: Schema | null; err: string | null }> {
|
||||
const res = await fetch(apiUrl);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function storeData(data: Schema): Promise<unknown> {
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function initSchema(startdate: string): Promise<Schema> {
|
||||
const setArray: DataItem[] = [];
|
||||
const monthArray: string[] = [];
|
||||
const yearsArray: string[] = [];
|
||||
const ld0 = DateTime.fromISO(startdate);
|
||||
let k = 0;
|
||||
for (let i = 0; i < 35; i++) {
|
||||
const elem: DataItem = { status: false, einheit: 0, day: '' };
|
||||
if (i === 17) {
|
||||
elem.day = '';
|
||||
} else {
|
||||
const ld = ld0.plus({ day: k });
|
||||
elem.day = ld.toFormat('y-LL-dd');
|
||||
const month = ld.setLocale('de').toFormat('LLLL');
|
||||
const year = ld.toFormat('y');
|
||||
if (!monthArray.includes(month)) monthArray.push(month);
|
||||
if (!yearsArray.includes(year)) yearsArray.push(year);
|
||||
k++;
|
||||
}
|
||||
setArray.push(elem);
|
||||
}
|
||||
const newSchema: Schema = {
|
||||
curdate: startdate,
|
||||
months: monthArray,
|
||||
years: yearsArray,
|
||||
data: setArray,
|
||||
einheit: curEinheitRef.current,
|
||||
};
|
||||
await storeData(newSchema);
|
||||
return newSchema;
|
||||
}
|
||||
|
||||
function applySchema(s: Schema) {
|
||||
// Update einheit input to last non-zero einheit found in data
|
||||
let lastEinheit = s.einheit;
|
||||
for (const item of s.data) {
|
||||
if (item.einheit !== 0) lastEinheit = item.einheit;
|
||||
}
|
||||
if (lastEinheit !== 0 && curEinheitRef.current === 0) {
|
||||
setCurEinheit(lastEinheit);
|
||||
curEinheitRef.current = lastEinheit;
|
||||
setEinheitInput(lastEinheit);
|
||||
}
|
||||
setMonthsLabel(buildMonthsLabel(s));
|
||||
setSchema(s);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
let s: Schema | null = null;
|
||||
if (sysParams.doinit) {
|
||||
s = await initSchema('2023-05-01');
|
||||
}
|
||||
if (!s) {
|
||||
const ret = await fetchData();
|
||||
s = ret.data;
|
||||
}
|
||||
if (!s) {
|
||||
// DB leer → neues Schema mit aktuellem Datum anlegen
|
||||
const today = DateTime.now().toFormat('y-LL-dd');
|
||||
s = await initSchema(today);
|
||||
}
|
||||
if (s) {
|
||||
applySchema(s);
|
||||
}
|
||||
}
|
||||
init();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleClick = useCallback(async (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const button = (e.target as HTMLElement).closest('button') as HTMLButtonElement | null;
|
||||
if (!button || button.disabled) return;
|
||||
const currentSchema = schemaRef.current;
|
||||
if (!currentSchema) return;
|
||||
|
||||
const idNum = parseInt(button.id.slice(2)) - 1; // 0-basierter Index
|
||||
const lastDay = currentSchema.data[34].day;
|
||||
|
||||
const newSchema: Schema = {
|
||||
...currentSchema,
|
||||
data: currentSchema.data.map((item, idx) => {
|
||||
if (idx === idNum) {
|
||||
return { ...item, status: !item.status, einheit: curEinheitRef.current };
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
};
|
||||
|
||||
await storeData(newSchema);
|
||||
applySchema(newSchema);
|
||||
|
||||
// Letzten Button (bt35) geklickt → nächste Periode anlegen
|
||||
if (button.id === 'bt35') {
|
||||
const nextDate = DateTime.fromISO(lastDay).plus({ day: 1 }).toFormat('y-LL-dd');
|
||||
const nextSchema = await initSchema(nextDate);
|
||||
applySchema(nextSchema);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleEinheitChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = parseInt(e.target.value) || 0;
|
||||
setCurEinheit(val);
|
||||
curEinheitRef.current = val;
|
||||
setEinheitInput(val);
|
||||
if (schemaRef.current) {
|
||||
schemaRef.current = { ...schemaRef.current, einheit: val };
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!schema) {
|
||||
return <div style={{ padding: '30px' }}>Lade…</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="spritztab">
|
||||
<h1>Spritz-Tabelle</h1>
|
||||
<h2 id="curmon">{monthsLabel}</h2>
|
||||
<div id="sptab" onClick={handleClick}>
|
||||
{schema.data.map((item, idx) => {
|
||||
const btId = `bt${idx + 1}`;
|
||||
const day = item.day ? DateTime.fromISO(item.day) : null;
|
||||
const isDisabled = item.day === '';
|
||||
const ariaLabel = isDisabled ? 'o' : item.status ? 'x' : '';
|
||||
const displayEinheit = item.einheit !== 0 ? item.einheit : (item.status ? schema.einheit : 0);
|
||||
return (
|
||||
<button
|
||||
key={btId}
|
||||
id={btId}
|
||||
disabled={isDisabled}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{day && <CellContent day={day} einheit={displayEinheit} />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div id="infeld">
|
||||
<label htmlFor="einheiten">Einheiten:</label>
|
||||
<input
|
||||
id="einheiten"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
value={einheitInput}
|
||||
onChange={handleEinheitChange}
|
||||
/>
|
||||
</div>
|
||||
<footer>
|
||||
<div id="v">
|
||||
Version {sysParams.version} vom {sysParams.date}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
129
app/globals.css
129
app/globals.css
@@ -1,42 +1,103 @@
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
color: #00B7FF;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
.spritztab #sptab {
|
||||
width: 91vmin;
|
||||
height: 65vmin;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0;
|
||||
margin: 3vmin auto;
|
||||
}
|
||||
|
||||
.spritztab h1 {
|
||||
text-align: center;
|
||||
margin-top: 3vmin;
|
||||
font-size: 8vmin;
|
||||
}
|
||||
|
||||
.spritztab h2 {
|
||||
text-align: center;
|
||||
font-size: 5vmin;
|
||||
}
|
||||
|
||||
.spritztab button {
|
||||
width: 13vmin;
|
||||
height: 13vmin;
|
||||
background: white;
|
||||
border: 1px solid black;
|
||||
margin: 0;
|
||||
font-size: 3.5vmin;
|
||||
color: black;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 0.5vmin 0.5vmin 0;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spritztab [aria-label="o"] {
|
||||
background-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201%201%22%3E%3Ccircle%20cx%3D%220.5%22%20cy%3D%220.5%22%20r%3D%220.4%22%20fill%3D%22none%22%20stroke-width%3D%220.1%22%20stroke%3D%22blue%22%2F%3E%3C%2Fsvg%3E');
|
||||
}
|
||||
|
||||
.spritztab [aria-label="x"] {
|
||||
background-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201%201%22%3E%3Cline%20x1%3D%220.1%22%20y1%3D%220.1%22%20x2%3D%220.9%22%20y2%3D%220.9%22%20stroke-width%3D%220.1%22%20stroke%3D%22red%22%2F%3E%3Cline%20x1%3D%220.1%22%20y1%3D%220.9%22%20x2%3D%220.9%22%20y2%3D%220.1%22%20stroke-width%3D%220.1%22%20stroke%3D%22red%22%2F%3E%3C%2Fsvg%3E');
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 60%;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
#infeld {
|
||||
width: 91vmin;
|
||||
margin: auto;
|
||||
font-size: 3.5vmin;
|
||||
}
|
||||
|
||||
#einheiten {
|
||||
font-size: 3.5vmin;
|
||||
margin-left: 2vmin;
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 91vmin;
|
||||
margin: auto;
|
||||
font-size: 2vmin;
|
||||
}
|
||||
#v {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.inner {
|
||||
width: 100%;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.lowline {
|
||||
width: 100%;
|
||||
clear: both;
|
||||
margin-top: 0.5vmin;
|
||||
}
|
||||
|
||||
.eh {
|
||||
color: black;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.wtg {
|
||||
float: left;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
export const viewport: Viewport = {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Spritzschema",
|
||||
manifest: "/manifest.json",
|
||||
icons: {
|
||||
apple: [
|
||||
{ url: "/apple-icon-57x57.png", sizes: "57x57" },
|
||||
{ url: "/apple-icon-60x60.png", sizes: "60x60" },
|
||||
{ url: "/apple-icon-72x72.png", sizes: "72x72" },
|
||||
{ url: "/apple-icon-76x76.png", sizes: "76x76" },
|
||||
{ url: "/apple-icon-114x114.png", sizes: "114x114" },
|
||||
{ url: "/apple-icon-120x120.png", sizes: "120x120" },
|
||||
{ url: "/apple-icon-144x144.png", sizes: "144x144" },
|
||||
{ url: "/apple-icon-152x152.png", sizes: "152x152" },
|
||||
{ url: "/apple-icon-180x180.png", sizes: "180x180" },
|
||||
],
|
||||
icon: [
|
||||
{ url: "/android-icon-192x192.png", sizes: "192x192", type: "image/png" },
|
||||
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
|
||||
{ url: "/favicon-96x96.png", sizes: "96x96", type: "image/png" },
|
||||
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -23,10 +36,8 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${geistSans.variable} ${geistMono.variable}`}>
|
||||
{children}
|
||||
</body>
|
||||
<html lang="de">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
88
app/page.tsx
88
app/page.tsx
@@ -1,66 +1,30 @@
|
||||
import Image from "next/image";
|
||||
import styles from "./page.module.css";
|
||||
import { Suspense } from 'react';
|
||||
import SpritzClient from './components/SpritzClient';
|
||||
import type { SysParams } from './components/SpritzClient';
|
||||
import pkg from '../package.json';
|
||||
|
||||
export default function Home() {
|
||||
interface SearchParams {
|
||||
test?: string;
|
||||
doinit?: string;
|
||||
einheit?: string;
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<SearchParams>;
|
||||
}) {
|
||||
const params = await searchParams;
|
||||
const sysParams: SysParams = {
|
||||
testing: params.test === 'true',
|
||||
doinit: params.doinit === 'true',
|
||||
einheit: parseInt(params.einheit ?? '0') || 0,
|
||||
version: pkg.version,
|
||||
date: (pkg as typeof pkg & { date?: string }).date ?? '',
|
||||
};
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<main className={styles.main}>
|
||||
<Image
|
||||
className={styles.logo}
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className={styles.intro}>
|
||||
<h1>To get started, edit the page.tsx file.</h1>
|
||||
<p>
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.ctas}>
|
||||
<a
|
||||
className={styles.primary}
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className={styles.logo}
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className={styles.secondary}
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<Suspense fallback={<div style={{ padding: '30px' }}>Lade…</div>}>
|
||||
<SpritzClient sysParams={sysParams} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user