diff --git a/package.json b/package.json index ad630c009..a271b8edd 100644 --- a/package.json +++ b/package.json @@ -32,12 +32,14 @@ "eslint-config-next": "13.4.3", "home-assistant-js-websocket": "8.0.1", "materialdesign-js": "1.0.0", + "moment": "2.29.4", "mui-chips-input": "2.0.1", "next": "13.4.3", "next-auth": "4.22.1", "prisma": "4.14.1", "react": "18.2.0", "react-dom": "18.2.0", + "react-moment": "1.1.3", "tss-react": "4.8.4", "typescript": "5.0.4", "use-long-press": "3.1.3" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8c85a4ffa..55efceee3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -28,12 +28,21 @@ model Dashboard { description String? userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) - sections Section[] + headerItems HeaderItem[] homeAssistant HomeAssistant[] + sections Section[] @@index([userId, position], name: "userId_position_unique") } +model HeaderItem { + id String @id @default(cuid()) + position Int @default(10) + type String + dashboardId String? + dashboard Dashboard? @relation(fields: [dashboardId], references: [id]) +} + model Section { id String @id @default(cuid()) position Int @default(10) diff --git a/src/app/dashboards/[dashboardId]/edit/page.tsx b/src/app/dashboards/[dashboardId]/edit/page.tsx index 1aeec0393..3eee0b398 100644 --- a/src/app/dashboards/[dashboardId]/edit/page.tsx +++ b/src/app/dashboards/[dashboardId]/edit/page.tsx @@ -1,5 +1,6 @@ import type { Dashboard as DashboardModel, + HeaderItem as HeaderItemModel, HomeAssistant as HomeAssistantModel, User as UserModel, } from "@prisma/client"; @@ -10,6 +11,7 @@ import { redirect } from "next/navigation"; import { AccessDenied } from "@/components/AccessDenied"; import { EditDashboard } from "@/components/dashboard/editors/Dashboard"; import { prisma } from "@/utils/prisma"; +import { HeaderItemType } from "@/types/dashboard.type"; export const metadata: Metadata = { title: "Edit Dashboard | Home Panel", @@ -87,9 +89,59 @@ export default async function Page({ }, }); + const headerItemsConfig: Array = + await prisma.headerItem.findMany({ + where: { + dashboardId: params.dashboardId, + }, + }); + + if (headerItemsConfig.length === 0) { + headerItemsConfig.push( + await prisma.headerItem.create({ + data: { + dashboard: { + connect: { + id: params.dashboardId, + }, + }, + type: HeaderItemType.Spacer, + position: 0, + }, + }) + ); + headerItemsConfig.push( + await prisma.headerItem.create({ + data: { + dashboard: { + connect: { + id: params.dashboardId, + }, + }, + type: HeaderItemType.DateTime, + position: 10, + }, + }) + ); + headerItemsConfig.push( + await prisma.headerItem.create({ + data: { + dashboard: { + connect: { + id: params.dashboardId, + }, + }, + type: HeaderItemType.Spacer, + position: 20, + }, + }) + ); + } + return ( ); diff --git a/src/app/dashboards/[dashboardId]/page.tsx b/src/app/dashboards/[dashboardId]/page.tsx index b1a2e8bb5..c7c4ce5f8 100644 --- a/src/app/dashboards/[dashboardId]/page.tsx +++ b/src/app/dashboards/[dashboardId]/page.tsx @@ -28,6 +28,9 @@ export default async function Page({ id: params.dashboardId, }, include: { + headerItems: { + orderBy: { position: "asc" }, + }, sections: { include: { widgets: { diff --git a/src/app/error.tsx b/src/app/error.tsx index 13ddf5ede..a17d7287b 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -17,7 +17,14 @@ export default function Error({ return (
- + Something went wrong! diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 80819a364..47ad8571f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,7 +4,6 @@ import { Inter } from "next/font/google"; import { AccessDenied } from "@/components/AccessDenied"; import { AuthProvider } from "@/providers/AuthProvider"; -import { Container } from "@/components/Container"; import { DrawerComponent as Drawer } from "@/components/Drawer"; import { MUIProvider } from "@/providers/MUIProvider"; @@ -38,7 +37,7 @@ export default async function RootLayout({ - {session ? children : } + {session ? children : } diff --git a/src/components/Container.tsx b/src/components/Container.tsx deleted file mode 100644 index d3e7358ce..000000000 --- a/src/components/Container.tsx +++ /dev/null @@ -1,23 +0,0 @@ -"use client"; -import { Unstable_Grid2 as Grid2 } from "@mui/material"; - -export function Container({ - children, -}: { - children: React.ReactNode | Array; -}): JSX.Element { - return ( - - {children} - - ); -} diff --git a/src/components/dashboard/editors/Dashboard.tsx b/src/components/dashboard/editors/Dashboard.tsx index 16e074d83..c0c49c4bf 100644 --- a/src/components/dashboard/editors/Dashboard.tsx +++ b/src/components/dashboard/editors/Dashboard.tsx @@ -1,36 +1,51 @@ "use client"; import type { Dashboard as DashboardModel, + HeaderItem as HeaderItemModel, HomeAssistant as HomeAssistantModel, } from "@prisma/client"; import { - Typography, Card, CardContent, - Unstable_Grid2 as Grid2, + Divider, TextField, + Typography, + Unstable_Grid2 as Grid2, } from "@mui/material"; +import { MuiChipsInput } from "mui-chips-input"; -import { dashboardUpdate } from "@/utils/serverActions/dashboard"; +import { + dashboardHeaderUpdate, + dashboardUpdate, +} from "@/utils/serverActions/dashboard"; import { homeAssistantUpdateConfig } from "@/utils/serverActions/homeAssistant"; +import { useMemo } from "react"; export function EditDashboard({ dashboardConfig, + headerItemsConfig, homeAssistantConfig, }: { dashboardConfig: DashboardModel; + headerItemsConfig: Array; homeAssistantConfig: HomeAssistantModel; }): JSX.Element { + const headerItems = useMemo>( + () => headerItemsConfig.map((item) => item.type), + [headerItemsConfig] + ); + return ( - + Edit Dashboard + Home Assistant @@ -73,6 +89,22 @@ export function EditDashboard({ }) } /> + + + Header Items + + 0 ? "Double click to edit an item" : "" + } + onChange={async (data: Array) => { + await dashboardHeaderUpdate(dashboardConfig.id, data); + }} + /> diff --git a/src/components/dashboard/editors/Section.tsx b/src/components/dashboard/editors/Section.tsx index db802c092..f37da0662 100644 --- a/src/components/dashboard/editors/Section.tsx +++ b/src/components/dashboard/editors/Section.tsx @@ -23,8 +23,10 @@ export function EditSection({ container direction="row" alignItems="center" - spacing={2} - sx={{ width: "100%" }} + sx={{ + margin: "2.5rem 2.5rem 0", + width: "100%", + }} xs > diff --git a/src/components/dashboard/editors/Widget.tsx b/src/components/dashboard/editors/Widget.tsx index d92f39beb..e147f617e 100644 --- a/src/components/dashboard/editors/Widget.tsx +++ b/src/components/dashboard/editors/Widget.tsx @@ -79,8 +79,10 @@ export function EditWidget({ container direction="row" alignItems="center" - spacing={2} - sx={{ width: "100%" }} + sx={{ + margin: "2.5rem 2.5rem 0", + width: "100%", + }} xs > diff --git a/src/components/dashboard/views/Dashboard.tsx b/src/components/dashboard/views/Dashboard.tsx index 45a47ea14..1738f3c44 100644 --- a/src/components/dashboard/views/Dashboard.tsx +++ b/src/components/dashboard/views/Dashboard.tsx @@ -1,6 +1,9 @@ "use client"; +import { Unstable_Grid2 as Grid2, Stack } from "@mui/material"; + import type { DashboardModel } from "@/types/dashboard.type"; import type { SectionModel } from "@/types/section.type"; +import { Heading } from "@/components/dashboard/views/Heading"; import { HomeAssistantProvider } from "@/providers/HomeAssistantProvider"; import { Section } from "@/components/dashboard/views/Section"; @@ -11,11 +14,22 @@ export function Dashboard({ }): JSX.Element { return ( - <> - {dashboard.sections.map((section: SectionModel) => ( -
- ))} - + + + + {dashboard.sections.map((section: SectionModel) => ( +
+ ))} + + ); } diff --git a/src/components/dashboard/views/Heading.tsx b/src/components/dashboard/views/Heading.tsx new file mode 100644 index 000000000..1a3416492 --- /dev/null +++ b/src/components/dashboard/views/Heading.tsx @@ -0,0 +1,83 @@ +"use client"; +import { HeaderItem as HeaderItemModel } from "@prisma/client"; +import { Unstable_Grid2 as Grid2, Typography } from "@mui/material"; +import Moment from "react-moment"; + +import type { DashboardModel } from "@/types/dashboard.type"; +import { HeaderItemType } from "@/types/dashboard.type"; + +function HeaderItem({ item }: { item: HeaderItemModel }): JSX.Element | null { + switch (item.type) { + case HeaderItemType.DateTime: + return ( + <> + + + + + + + + + + + ); + case HeaderItemType.Date: + return ( + + + + ); + case HeaderItemType.Time: + return ( + + + + ); + default: + return null; + } +} + +export function Heading({ + dashboard, +}: { + dashboard: DashboardModel; +}): JSX.Element { + return ( + + {dashboard.headerItems.map((item: HeaderItemModel) => ( + + + + ))} + + ); +} diff --git a/src/components/dashboard/views/Section.tsx b/src/components/dashboard/views/Section.tsx index 79534d83c..984b15db9 100644 --- a/src/components/dashboard/views/Section.tsx +++ b/src/components/dashboard/views/Section.tsx @@ -139,7 +139,6 @@ export function Section({ data }: { data: SectionModel }): JSX.Element { justifyContent="flex-start" xs="auto" sx={{ - height: "100%", }} > {data.widgets.map((widget: WidgetModel) => ( diff --git a/src/types/dashboard.type.ts b/src/types/dashboard.type.ts index b638e139d..94bb73c92 100644 --- a/src/types/dashboard.type.ts +++ b/src/types/dashboard.type.ts @@ -1,10 +1,22 @@ -import type { Dashboard, Section, Widget } from "@prisma/client"; +import type { Dashboard, HeaderItem, Section, Widget } from "@prisma/client"; // Combined types for dashboard, section and widget export type DashboardModel = Dashboard & { + headerItems: Array; sections: Array< Section & { widgets: Array; } >; }; + +export type DashboardHeaderModel = Dashboard & { + headerItems: Array; +}; + +export enum HeaderItemType { + Date = "date", + DateTime = "dateTime", + Spacer = "spacer", + Time = "time", +} diff --git a/src/utils/serverActions/dashboard.ts b/src/utils/serverActions/dashboard.ts index 4ae70c855..49efd1556 100644 --- a/src/utils/serverActions/dashboard.ts +++ b/src/utils/serverActions/dashboard.ts @@ -1,5 +1,5 @@ "use server"; -import type { Dashboard } from "@prisma/client"; +import type { Dashboard, HeaderItem } from "@prisma/client"; import { revalidatePath } from "next/cache"; import { prisma } from "@/utils/prisma"; @@ -37,6 +37,49 @@ export async function dashboardUpdate( }); revalidatePath(`/dashboards/${dashboardId}`); + revalidatePath(`/dashboards/${dashboardId}/edit`); + + return newData; +} + +export async function dashboardHeaderUpdate( + dashboardId: string, + items: Array +): Promise> { + console.log("Update dashboard header:", { dashboardId, items }); + + await prisma.headerItem.deleteMany({ + where: { + dashboardId: dashboardId, + }, + }); + + let position = 0; + for (const type of items) { + position += 10; + await prisma.headerItem.create({ + data: { + position, + type, + dashboard: { + connect: { + id: dashboardId, + }, + }, + }, + }); + } + + const newData = await prisma.headerItem.findMany({ + where: { + dashboardId: dashboardId, + }, + }); + + console.log("New header items:", newData); + + revalidatePath(`/dashboards/${dashboardId}`); + revalidatePath(`/dashboards/${dashboardId}/edit`); return newData; } diff --git a/yarn.lock b/yarn.lock index 157121069..16d463e56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2025,6 +2025,11 @@ minimist@^1.2.0, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +moment@2.29.4: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -2392,6 +2397,11 @@ react-is@^18.2.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== +react-moment@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/react-moment/-/react-moment-1.1.3.tgz#829b21dfb279aa6db47ce4f1ac2555af17a1bcdc" + integrity sha512-8EPvlUL8u6EknPp1ISF5MQ3wx2OHJVXIP/iZc4wRh3iV3XozftZERDv9ANZeAtMlhNNQHdFoqcZHFUkBSTONfA== + react-transition-group@^4.4.5: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"