Compare commits

...

3 Commits

Author SHA1 Message Date
Claude 0db81aa715 Remove duplicate CSS rules and simplify z-index hierarchy in globals.css
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 11:04:11 +02:00
Claude ec00024615 Merge toolbar-fixes.css and user-theme.css into globals.css
Consolidate two override CSS files into globals.css to reduce the number
of separate stylesheets. Both files were primarily overriding rules
already in globals.css (81% and 55% overlap respectively).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 13:44:46 +02:00
Claude 2f3e1bff1b Extract shared types to src/types/dashboard.ts
Consolidate duplicated type definitions (User, Settings, Widget,
CalendarEvent, CalendarSource, NoteBoardItem, etc.) into a single
shared types file. Removes ~100 lines of duplicated type code across
page.tsx, settings/page.tsx, NoteWidget.tsx, and CalendarWidget.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 13:42:30 +02:00
9 changed files with 343 additions and 516 deletions
+248 -121
View File
@@ -53,7 +53,6 @@ button:disabled {
.app { .app {
min-height: 100vh; min-height: 100vh;
background: var(--background);
color: var(--text); color: var(--text);
} }
@@ -304,14 +303,9 @@ button:disabled {
.react-grid-item.react-draggable-dragging, .react-grid-item.react-draggable-dragging,
.react-grid-item.resizing { .react-grid-item.resizing {
z-index: 200;
transition: none; transition: none;
} }
.gridItemMenuOpen {
z-index: 250 !important;
}
.react-grid-item > .react-resizable-handle { .react-grid-item > .react-resizable-handle {
z-index: 40; z-index: 40;
width: 30px; width: 30px;
@@ -403,20 +397,11 @@ button:disabled {
} }
.widgetMenu { .widgetMenu {
position: relative;
flex: 0 0 auto; flex: 0 0 auto;
} }
.widgetMenuButton { .widgetMenuButton {
height: 30px;
padding: 0 9px;
color: var(--text);
background: var(--surface-strong);
border: 1px solid var(--border);
border-radius: 9px;
cursor: pointer;
white-space: nowrap; white-space: nowrap;
font-size: 12px;
} }
.widgetMenuButton:hover, .widgetMenuButton:hover,
@@ -425,18 +410,10 @@ button:disabled {
} }
.widgetDropdown { .widgetDropdown {
position: absolute;
top: calc(100% + 8px);
right: 0;
z-index: 350;
width: 190px; width: 190px;
display: grid; display: grid;
gap: 7px;
padding: 10px;
background: var(--surface); background: var(--surface);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 14px;
box-shadow: var(--shadow);
} }
.widgetDropdownButton { .widgetDropdownButton {
@@ -1408,31 +1385,6 @@ button:disabled {
border-radius: 16px; border-radius: 16px;
} }
.app {
position: relative;
isolation: isolate;
--dashboard-background-image: none;
--dashboard-background-opacity: 0;
}
.app::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background-image: var(--dashboard-background-image);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
opacity: var(--dashboard-background-opacity);
}
.app > * {
position: relative;
z-index: 1;
}
.settingsSubPanel { .settingsSubPanel {
display: grid; display: grid;
gap: 14px; gap: 14px;
@@ -1522,56 +1474,21 @@ button:disabled {
z-index: 1200; z-index: 1200;
} }
.dashboardWorkspace,
.widgetGridShell,
.emptyDashboard {
z-index: 1;
}
.app::before {
z-index: 0;
}
.app > * {
position: relative;
z-index: 1;
}
.app > .topBar, .app > .topBar,
.app > .adminTopBar { .app > .adminTopBar {
position: relative; position: relative;
z-index: 5000; z-index: 5000;
} }
.topBar .profileMenu {
position: relative;
z-index: 5100;
}
.topBar .profileDropdown { .topBar .profileDropdown {
position: absolute; position: absolute;
z-index: 5200;
pointer-events: auto; pointer-events: auto;
} }
.dashboardWorkspace, .widgetGrid {
.widgetGridShell,
.widgetGrid,
.emptyDashboard,
.react-grid-layout,
.react-grid-item {
z-index: auto; z-index: auto;
} }
.react-grid-item.react-draggable-dragging,
.react-grid-item.resizing {
z-index: 200;
}
.gridItemMenuOpen {
z-index: 250 !important;
}
.widgetCard-search .searchWidgetForm { .widgetCard-search .searchWidgetForm {
grid-template-columns: minmax(82px, 130px) minmax(0, 1fr) auto !important; grid-template-columns: minmax(82px, 130px) minmax(0, 1fr) auto !important;
align-items: center; align-items: center;
@@ -1805,6 +1722,8 @@ button:disabled {
.app { .app {
position: relative; position: relative;
isolation: isolate; isolation: isolate;
--dashboard-background-image: none;
--dashboard-background-opacity: 0;
background-color: var(--background); background-color: var(--background);
} }
@@ -2472,33 +2391,6 @@ button:disabled {
padding-bottom: 0 !important; padding-bottom: 0 !important;
} }
/* Widget-Menüs immer über anderen Widgets anzeigen */
.react-grid-item {
overflow: visible !important;
}
.react-grid-item.gridItemMenuOpen {
z-index: 99999 !important;
}
.gridItemMenuOpen,
.gridItemMenuOpen .widgetCard,
.gridItemMenuOpen .widgetHeader,
.gridItemMenuOpen .widgetMenu {
overflow: visible !important;
z-index: 99999 !important;
}
.widgetMenu {
position: relative !important;
z-index: 100000 !important;
}
.widgetDropdown {
position: absolute !important;
z-index: 100001 !important;
}
/* Suche-Widget: im normalen Modus ohne Rahmen/Header, im Bearbeitungsmodus aber mit Ziehen/Menü */ /* Suche-Widget: im normalen Modus ohne Rahmen/Header, im Bearbeitungsmodus aber mit Ziehen/Menü */
.widgetCard-search { .widgetCard-search {
overflow: visible !important; overflow: visible !important;
@@ -2839,16 +2731,6 @@ button:disabled {
z-index: 99999 !important; z-index: 99999 !important;
} }
.widgetMenu {
position: relative !important;
z-index: 100000 !important;
}
.widgetDropdown {
position: absolute !important;
z-index: 100001 !important;
}
/* Fix: Widget-Edit-Header sauber vertikal zentrieren */ /* Fix: Widget-Edit-Header sauber vertikal zentrieren */
.widgetCard.widgetEditing .widgetHeader { .widgetCard.widgetEditing .widgetHeader {
min-height: 44px !important; min-height: 44px !important;
@@ -3388,3 +3270,248 @@ button:disabled {
padding: 6px 7px !important; padding: 6px 7px !important;
font-size: 12px !important; font-size: 12px !important;
} }
/* --- Toolbar Fixes (merged from toolbar-fixes.css) --- */
.editToolbar .button:first-child,
.editToolbar button:first-child {
display: inline-flex !important;
}
.editToolbar button,
.editToolbar .button {
visibility: visible !important;
opacity: 1 !important;
pointer-events: auto !important;
}
.editToolbar .topEditModeButton,
.editToolbar button[aria-label="Bearbeitungsmodus aktivieren"],
.editToolbar button[aria-label="Bearbeitungsmodus deaktivieren"] {
display: none !important;
}
.topBar {
position: relative;
z-index: 5000;
}
.topBar .profileMenu {
position: relative;
z-index: 5400;
}
.topBar .profileDropdown {
z-index: 5500;
}
.topBar .topEditModeButton {
position: absolute;
top: 50%;
right: 78px;
z-index: 5350;
transform: translateY(-50%);
}
.topEditModeButton {
width: 42px;
height: 42px;
display: inline-grid;
place-items: center;
padding: 0;
color: var(--text);
background: var(--surface-strong);
border: 1px solid var(--border);
border-radius: 999px;
cursor: pointer;
box-shadow: var(--shadow);
font-size: 0;
}
.topEditModeButton:hover {
border-color: var(--accent);
}
.topEditModeButtonActive {
color: var(--accent-text);
background: var(--accent);
border-color: var(--accent);
}
.lockIcon {
position: relative;
width: 22px;
height: 24px;
display: block;
color: currentColor;
}
.lockIconBody {
position: absolute;
left: 3px;
bottom: 2px;
width: 16px;
height: 13px;
background: transparent;
border: 2px solid currentColor;
border-radius: 4px;
}
.lockIconBody::before {
content: "";
position: absolute;
left: 50%;
top: 4px;
width: 3px;
height: 3px;
background: currentColor;
border-radius: 999px;
transform: translateX(-50%);
}
.lockIconBody::after {
content: "";
position: absolute;
left: 50%;
top: 7px;
width: 2px;
height: 4px;
background: currentColor;
border-radius: 999px;
transform: translateX(-50%);
}
.lockIconShackle {
position: absolute;
width: 12px;
height: 11px;
background: transparent;
border: 2px solid currentColor;
border-bottom: 0;
border-radius: 10px 10px 0 0;
}
.lockIconClosed .lockIconShackle {
left: 5px;
top: 1px;
transform: none;
}
.lockIconOpen .lockIconShackle {
left: 9px;
top: 1px;
transform: rotate(35deg);
transform-origin: left bottom;
}
.dashboardWorkspace,
.widgetGridShell,
.emptyDashboard,
.react-grid-layout,
.react-grid-item {
z-index: auto;
}
.react-grid-item.react-draggable-dragging,
.react-grid-item.resizing {
z-index: 300;
}
.gridItemMenuOpen {
z-index: 400 !important;
}
@media (max-width: 760px) {
.topBar .topEditModeButton {
right: 72px;
}
.topEditModeButton {
width: 38px;
height: 38px;
}
.lockIcon {
transform: scale(0.9);
}
}
/* --- User Theme (merged from user-theme.css) --- */
:root {
--accent-text-fallback: #ffffff;
}
.app {
--accent-text: var(--accent-text, var(--accent-text-fallback));
}
.button,
.adminInlineButton,
.favoriteAddButton,
.searchWidgetForm .button {
border-color: color-mix(in srgb, var(--accent) 70%, var(--border) 30%);
}
.button:not(.buttonSecondary):not(.favoriteRemoveButton):not(.logoutButton),
.adminInlineButton,
.favoriteAddButton,
.searchWidgetForm .button {
color: var(--accent-text, #ffffff);
background: var(--accent);
}
.button:not(.buttonSecondary):not(.favoriteRemoveButton):not(.logoutButton):hover,
.adminInlineButton:hover,
.favoriteAddButton:hover,
.searchWidgetForm .button:hover {
border-color: var(--accent);
filter: brightness(1.05);
}
.buttonSecondary,
.profileDropdown .button,
.widgetMenuButton,
.widgetDragHandle {
color: var(--text);
background: color-mix(in srgb, var(--accent-soft) 42%, var(--surface-strong) 58%);
border-color: color-mix(in srgb, var(--accent) 28%, var(--border) 72%);
}
.buttonSecondary:hover,
.profileDropdown .button:hover,
.widgetMenuButton:hover,
.widgetDragHandle:hover {
border-color: var(--accent);
}
.input:focus,
.select:focus,
textarea:focus,
.fileInput:focus,
.colorInput:focus {
outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
border-color: var(--accent);
}
.topEditModeButtonActive {
color: var(--accent-text, #ffffff);
background: var(--accent);
border-color: var(--accent);
}
.favoriteTile:hover,
.widgetCard:hover {
border-color: color-mix(in srgb, var(--accent) 65%, var(--border) 35%);
}
.favoriteIconPlaceholder {
color: var(--accent-text, #ffffff);
background: var(--accent);
}
.equalsButton {
color: var(--accent-text, #ffffff) !important;
background: var(--accent) !important;
border-color: var(--accent) !important;
}
-2
View File
@@ -8,8 +8,6 @@ import "./favorites-widget.css";
import "./admin.css"; import "./admin.css";
import "./note-widget.css"; import "./note-widget.css";
import "./calendar-scale.css"; import "./calendar-scale.css";
import "./toolbar-fixes.css";
import "./user-theme.css";
import "./widget-density.css"; import "./widget-density.css";
import "./clock-widget.css"; import "./clock-widget.css";
import "./search-widget.css"; import "./search-widget.css";
+1 -89
View File
@@ -6,6 +6,7 @@ import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
import type { DashboardGridProps } from "@/components/DashboardGrid"; import type { DashboardGridProps } from "@/components/DashboardGrid";
import type { DashboardGridWidget, DashboardLayoutItem } from "@/lib/dashboard-layout"; import type { DashboardGridWidget, DashboardLayoutItem } from "@/lib/dashboard-layout";
import { sortLayoutForPosition } from "@/lib/dashboard-layout"; import { sortLayoutForPosition } from "@/lib/dashboard-layout";
import type { User, Settings, DashboardTab, WidgetType, Widget, SearchProvider, CalendarEvent, CalendarSource, CalendarWidgetCalendarConfig, NoteBoardItem } from "@/types/dashboard";
import CalculatorWidget from "@/components/CalculatorWidget"; import CalculatorWidget from "@/components/CalculatorWidget";
import CalendarWidget from "@/components/CalendarWidget"; import CalendarWidget from "@/components/CalendarWidget";
import ClockWidget from "@/components/ClockWidget"; import ClockWidget from "@/components/ClockWidget";
@@ -25,95 +26,6 @@ const DashboardGrid = dynamic<DashboardGridProps>(() => import("@/components/Das
) )
}); });
type User = {
id: string;
email: string;
displayName: string | null;
role: "ADMIN" | "USER";
};
type Settings = {
id: string;
userId: string;
darkMode: boolean;
calendarIcsUrl: string | null;
calendarMaxEvents: number;
calendarLookaheadDays: number;
dashboardTitle: string;
dashboardSubtitle: string | null;
logoUrl: string | null;
backgroundImageUrl: string | null;
backgroundImageOpacity: number;
primaryColor: string;
secondaryColor: string;
customCss: string;
};
type DashboardTab = {
id: string;
userId: string;
title: string;
position: number;
};
type WidgetType = "favorites" | "note" | "search" | "calendar" | "calculator" | "clock" | "domain-check";
type Widget = DashboardGridWidget & {
userId: string;
viewMode?: string;
};
type SearchProvider = {
id: string;
label: string;
urlTemplate: string;
};
type CalendarEvent = {
id: string;
title: string;
start: string;
end: string | null;
location: string | null;
};
type CalendarSourceType = "ICS" | "EXCHANGE_EWS";
type CalendarSource = {
id: string;
userId: string;
type: CalendarSourceType;
name: string;
color: string;
nextEventsCount: number;
icsUrl: string | null;
exchangeEwsUrl: string | null;
exchangeMailbox: string | null;
exchangeUsername: string | null;
exchangeDomain: string | null;
passwordConfigured: boolean;
};
type CalendarWidgetCalendarConfig = {
sources: CalendarSource[];
selectedSourceIds: string[];
nextEventsCount: number;
};
type NoteBoardItem = {
id: string;
userId: string;
type: "note" | string;
title: string;
content: string;
x: number;
y: number;
w: number;
h: number;
createdAt: string;
updatedAt: string;
};
const searchProviders: SearchProvider[] = [ const searchProviders: SearchProvider[] = [
{ {
id: "google", id: "google",
+1 -35
View File
@@ -1,41 +1,7 @@
"use client"; "use client";
import { FormEvent, useEffect, useState } from "react"; import { FormEvent, useEffect, useState } from "react";
import type { Settings, CalendarSource, CalendarSourceType } from "@/types/dashboard";
type Settings = {
id: string;
userId: string;
darkMode: boolean;
calendarIcsUrl: string | null;
calendarMaxEvents: number;
calendarLookaheadDays: number;
dashboardTitle: string;
dashboardSubtitle: string | null;
logoUrl: string | null;
faviconUrl: string | null;
backgroundImageUrl: string | null;
backgroundImageOpacity: number;
primaryColor: string;
secondaryColor: string;
customCss: string;
};
type CalendarSourceType = "ICS" | "EXCHANGE_EWS";
type CalendarSource = {
id: string;
userId: string;
type: CalendarSourceType;
name: string;
color: string;
nextEventsCount: number;
icsUrl: string | null;
exchangeEwsUrl: string | null;
exchangeMailbox: string | null;
exchangeUsername: string | null;
exchangeDomain: string | null;
passwordConfigured: boolean;
};
type CalendarSourceDraft = { type CalendarSourceDraft = {
sourceId: string | null; sourceId: string | null;
-162
View File
@@ -1,162 +0,0 @@
.editToolbar .button:first-child,
.editToolbar button:first-child {
display: inline-flex !important;
}
.editToolbar button,
.editToolbar .button {
visibility: visible !important;
opacity: 1 !important;
pointer-events: auto !important;
}
.editToolbar .topEditModeButton,
.editToolbar button[aria-label="Bearbeitungsmodus aktivieren"],
.editToolbar button[aria-label="Bearbeitungsmodus deaktivieren"] {
display: none !important;
}
.topBar {
position: relative;
z-index: 5000;
}
.topBar .profileMenu {
position: relative;
z-index: 5400;
}
.topBar .profileDropdown {
z-index: 5500;
}
.topBar .topEditModeButton {
position: absolute;
top: 50%;
right: 78px;
z-index: 5350;
transform: translateY(-50%);
}
.topEditModeButton {
width: 42px;
height: 42px;
display: inline-grid;
place-items: center;
padding: 0;
color: var(--text);
background: var(--surface-strong);
border: 1px solid var(--border);
border-radius: 999px;
cursor: pointer;
box-shadow: var(--shadow);
font-size: 0;
}
.topEditModeButton:hover {
border-color: var(--accent);
}
.topEditModeButtonActive {
color: var(--accent-text);
background: var(--accent);
border-color: var(--accent);
}
.lockIcon {
position: relative;
width: 22px;
height: 24px;
display: block;
color: currentColor;
}
.lockIconBody {
position: absolute;
left: 3px;
bottom: 2px;
width: 16px;
height: 13px;
background: transparent;
border: 2px solid currentColor;
border-radius: 4px;
}
.lockIconBody::before {
content: "";
position: absolute;
left: 50%;
top: 4px;
width: 3px;
height: 3px;
background: currentColor;
border-radius: 999px;
transform: translateX(-50%);
}
.lockIconBody::after {
content: "";
position: absolute;
left: 50%;
top: 7px;
width: 2px;
height: 4px;
background: currentColor;
border-radius: 999px;
transform: translateX(-50%);
}
.lockIconShackle {
position: absolute;
width: 12px;
height: 11px;
background: transparent;
border: 2px solid currentColor;
border-bottom: 0;
border-radius: 10px 10px 0 0;
}
.lockIconClosed .lockIconShackle {
left: 5px;
top: 1px;
transform: none;
}
.lockIconOpen .lockIconShackle {
left: 9px;
top: 1px;
transform: rotate(35deg);
transform-origin: left bottom;
}
.dashboardWorkspace,
.widgetGridShell,
.emptyDashboard,
.react-grid-layout,
.react-grid-item {
z-index: auto;
}
.react-grid-item.react-draggable-dragging,
.react-grid-item.resizing {
z-index: 300;
}
.gridItemMenuOpen {
z-index: 400 !important;
}
@media (max-width: 760px) {
.topBar .topEditModeButton {
right: 72px;
}
.topEditModeButton {
width: 38px;
height: 38px;
}
.lockIcon {
transform: scale(0.9);
}
}
-77
View File
@@ -1,77 +0,0 @@
:root {
--accent-text-fallback: #ffffff;
}
.app {
--accent-text: var(--accent-text, var(--accent-text-fallback));
}
.button,
.adminInlineButton,
.favoriteAddButton,
.searchWidgetForm .button {
border-color: color-mix(in srgb, var(--accent) 70%, var(--border) 30%);
}
.button:not(.buttonSecondary):not(.favoriteRemoveButton):not(.logoutButton),
.adminInlineButton,
.favoriteAddButton,
.searchWidgetForm .button {
color: var(--accent-text, #ffffff);
background: var(--accent);
}
.button:not(.buttonSecondary):not(.favoriteRemoveButton):not(.logoutButton):hover,
.adminInlineButton:hover,
.favoriteAddButton:hover,
.searchWidgetForm .button:hover {
border-color: var(--accent);
filter: brightness(1.05);
}
.buttonSecondary,
.profileDropdown .button,
.widgetMenuButton,
.widgetDragHandle {
color: var(--text);
background: color-mix(in srgb, var(--accent-soft) 42%, var(--surface-strong) 58%);
border-color: color-mix(in srgb, var(--accent) 28%, var(--border) 72%);
}
.buttonSecondary:hover,
.profileDropdown .button:hover,
.widgetMenuButton:hover,
.widgetDragHandle:hover {
border-color: var(--accent);
}
.input:focus,
.select:focus,
textarea:focus,
.fileInput:focus,
.colorInput:focus {
outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
border-color: var(--accent);
}
.topEditModeButtonActive {
color: var(--accent-text, #ffffff);
background: var(--accent);
border-color: var(--accent);
}
.favoriteTile:hover,
.widgetCard:hover {
border-color: color-mix(in srgb, var(--accent) 65%, var(--border) 35%);
}
.favoriteIconPlaceholder {
color: var(--accent-text, #ffffff);
background: var(--accent);
}
.equalsButton {
color: var(--accent-text, #ffffff) !important;
background: var(--accent) !important;
border-color: var(--accent) !important;
}
+1 -16
View File
@@ -1,22 +1,7 @@
"use client"; "use client";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import type { CalendarEvent, CalendarSource } from "@/types/dashboard";
type CalendarEvent = {
id: string;
title: string;
start: string;
end: string | null;
location: string | null;
};
type CalendarSource = {
id: string;
name: string;
color: string;
type: string;
passwordConfigured: boolean;
};
type CalendarWidgetProps = { type CalendarWidgetProps = {
widgetId: string; widgetId: string;
+1 -14
View File
@@ -2,20 +2,7 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useState } from "react"; import { useState } from "react";
import type { NoteBoardItem } from "@/types/dashboard";
type NoteBoardItem = {
id: string;
userId: string;
type: "note" | string;
title: string;
content: string;
x: number;
y: number;
w: number;
h: number;
createdAt: string;
updatedAt: string;
};
type NoteWidgetProps = { type NoteWidgetProps = {
note: NoteBoardItem | null; note: NoteBoardItem | null;
+91
View File
@@ -0,0 +1,91 @@
import type { DashboardGridWidget } from "@/lib/dashboard-layout";
export type User = {
id: string;
email: string;
displayName: string | null;
role: "ADMIN" | "USER";
};
export type Settings = {
id: string;
userId: string;
darkMode: boolean;
calendarIcsUrl: string | null;
calendarMaxEvents: number;
calendarLookaheadDays: number;
dashboardTitle: string;
dashboardSubtitle: string | null;
logoUrl: string | null;
faviconUrl: string | null;
backgroundImageUrl: string | null;
backgroundImageOpacity: number;
primaryColor: string;
secondaryColor: string;
customCss: string;
};
export type DashboardTab = {
id: string;
userId: string;
title: string;
position: number;
};
export type WidgetType = "favorites" | "note" | "search" | "calendar" | "calculator" | "clock" | "domain-check";
export type Widget = DashboardGridWidget & {
userId: string;
viewMode?: string;
};
export type SearchProvider = {
id: string;
label: string;
urlTemplate: string;
};
export type CalendarEvent = {
id: string;
title: string;
start: string;
end: string | null;
location: string | null;
};
export type CalendarSourceType = "ICS" | "EXCHANGE_EWS";
export type CalendarSource = {
id: string;
userId: string;
type: CalendarSourceType;
name: string;
color: string;
nextEventsCount: number;
icsUrl: string | null;
exchangeEwsUrl: string | null;
exchangeMailbox: string | null;
exchangeUsername: string | null;
exchangeDomain: string | null;
passwordConfigured: boolean;
};
export type CalendarWidgetCalendarConfig = {
sources: CalendarSource[];
selectedSourceIds: string[];
nextEventsCount: number;
};
export type NoteBoardItem = {
id: string;
userId: string;
type: "note" | string;
title: string;
content: string;
x: number;
y: number;
w: number;
h: number;
createdAt: string;
updatedAt: string;
};