diff --git a/.eslintrc.json b/.eslintrc.json index cfc136b..4e4dede 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,7 @@ { "extends": "next", "rules": { - "no-unused-vars": "warn" + "no-unused-vars": "warn", + "react/display-name": "off" } } diff --git a/.github/workflows/push_registry.yaml b/.github/workflows/push_registry.yaml index 52854e6..2b6bb2a 100644 --- a/.github/workflows/push_registry.yaml +++ b/.github/workflows/push_registry.yaml @@ -22,7 +22,7 @@ jobs: run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin ${{ secrets.DOCKER_REGISTRY }} - name: Build Docker Image - run: docker build -t ${{ secrets.DOCKER_USERNAME }}/menuroo-admin:${{ env.SHORT_SHA }} . + run: docker build --build-arg NEXT_PUBLIC_API_URL="https://api.menuroo.cz/query" -t ${{ secrets.DOCKER_USERNAME }}/menuroo-admin:${{ env.SHORT_SHA }} . - name: Tag Docker Image run: docker tag ${{ secrets.DOCKER_USERNAME }}/menuroo-admin:${{ env.SHORT_SHA }} ${{ secrets.DOCKER_REGISTRY }}/${{ secrets.DOCKER_USERNAME }}/menuroo-admin:${{ env.SHORT_SHA }} diff --git a/.github/workflows/push_registry_dev.yaml b/.github/workflows/push_registry_dev.yaml new file mode 100644 index 0000000..e197f32 --- /dev/null +++ b/.github/workflows/push_registry_dev.yaml @@ -0,0 +1,31 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - stage + + +jobs: + build_and_push: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Get SHA for last commit + run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV + + + - name: Login to Docker Registry + run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin ${{ secrets.DOCKER_REGISTRY }} + + - name: Build Docker Image + run: docker build --build-arg NEXT_PUBLIC_API_URL="https://api.dev.menuroo.cz/query" -t ${{ secrets.DOCKER_USERNAME }}/menuroo-admin-dev:${{ env.SHORT_SHA }} . + + - name: Tag Docker Image + run: docker tag ${{ secrets.DOCKER_USERNAME }}/menuroo-admin-dev:${{ env.SHORT_SHA }} ${{ secrets.DOCKER_REGISTRY }}/${{ secrets.DOCKER_USERNAME }}/menuroo-admin-dev:${{ env.SHORT_SHA }} + + - name: Push Docker Image + run: docker push ${{ secrets.DOCKER_REGISTRY }}/${{ secrets.DOCKER_USERNAME }}/menuroo-admin-dev:${{ env.SHORT_SHA }} diff --git a/.gitignore b/.gitignore index 64fe91e..d96baf0 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ jspm_packages .node_repl_history .next .vercel + +.DS_Store diff --git a/Dockerfile b/Dockerfile index 60fd060..7f5d405 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,10 @@ FROM node:18-alpine -ARG NEXT_PUBLIC_API_URL="https://api.menuroo.com/query" +# Declare a build argument with a default value +ARG NEXT_PUBLIC_API_URL="https://api.dev.menuroo.com/query" + +# Set the environment variable with the build argument value +ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} RUN mkdir -p /app WORKDIR /app diff --git a/bun.lockb b/bun.lockb index 0fa73f3..76cbd9b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..0dd0922 --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,13 @@ +import type { Config } from "jest"; +import nextJest from "next/jest.js"; + +const createJestConfig = nextJest({ + dir: "./", +}); + +const config: Config = { + coverageProvider: "v8", + testEnvironment: "jsdom", +}; + +export default createJestConfig(config); diff --git a/package.json b/package.json index 8673098..37b4459 100644 --- a/package.json +++ b/package.json @@ -7,32 +7,41 @@ "build": "next build", "start": "next start", "lint": "next lint", - "analyze": "ANALYZE=true next build" + "analyze": "ANALYZE=true next build", + "test": "jest", + "test:watch": "jest --watch" }, "dependencies": { "@ant-design/cssinjs": "^1.17.0", "@apollo/client": "latest", - "@apollo/experimental-nextjs-app-support": "^0.4.2", + "@apollo/experimental-nextjs-app-support": "^0.5.2", "@next/bundle-analyzer": "^14.0.3", "@vercel/analytics": "^1.0.2", "antd": "^5.9.0", + "classnames": "^2.3.2", "dayjs": "^1.11.9", "dayjs-plugin-utc": "^0.1.2", "graphql": "^16.8.0", "install": "^0.13.0", - "next": "13.5.4", + "next": "14.0.4", "react": "18.2.0", "react-dom": "18.2.0" }, "devDependencies": { + "@testing-library/jest-dom": "^6.1.5", + "@testing-library/react": "^14.1.2", + "@types/jest": "^29.5.11", "@types/node": "20.6.0", "@types/react": "18.2.21", "@types/react-dom": "18.2.7", - "eslint": "8.49.0", - "eslint-config-next": "13.5.4", "autoprefixer": "10.4.15", + "eslint": "8.49.0", + "eslint-config-next": "14.0.4", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "postcss": "8.4.29", - "tailwindcss": "3.3.3", + "ts-jest": "^29.1.1", + "ts-node-dev": "^2.0.0", "typescript": "5.2.2" } } diff --git a/postcss.config.js b/postcss.config.js index 12a703d..a47ef4f 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,6 +1,5 @@ module.exports = { plugins: { - tailwindcss: {}, autoprefixer: {}, }, }; diff --git a/public/fonts/Nunito-Black.ttf b/public/fonts/Nunito-Black.ttf new file mode 100644 index 0000000..81d557c Binary files /dev/null and b/public/fonts/Nunito-Black.ttf differ diff --git a/public/fonts/Nunito-BlackItalic.ttf b/public/fonts/Nunito-BlackItalic.ttf new file mode 100644 index 0000000..b4ba5a1 Binary files /dev/null and b/public/fonts/Nunito-BlackItalic.ttf differ diff --git a/public/fonts/Nunito-Bold.ttf b/public/fonts/Nunito-Bold.ttf new file mode 100644 index 0000000..886134d Binary files /dev/null and b/public/fonts/Nunito-Bold.ttf differ diff --git a/public/fonts/Nunito-BoldItalic.ttf b/public/fonts/Nunito-BoldItalic.ttf new file mode 100644 index 0000000..0cb4efa Binary files /dev/null and b/public/fonts/Nunito-BoldItalic.ttf differ diff --git a/public/fonts/Nunito-ExtraBold.ttf b/public/fonts/Nunito-ExtraBold.ttf new file mode 100644 index 0000000..711765e Binary files /dev/null and b/public/fonts/Nunito-ExtraBold.ttf differ diff --git a/public/fonts/Nunito-ExtraBoldItalic.ttf b/public/fonts/Nunito-ExtraBoldItalic.ttf new file mode 100644 index 0000000..bffce10 Binary files /dev/null and b/public/fonts/Nunito-ExtraBoldItalic.ttf differ diff --git a/public/fonts/Nunito-ExtraLight.ttf b/public/fonts/Nunito-ExtraLight.ttf new file mode 100644 index 0000000..d9eabf9 Binary files /dev/null and b/public/fonts/Nunito-ExtraLight.ttf differ diff --git a/public/fonts/Nunito-ExtraLightItalic.ttf b/public/fonts/Nunito-ExtraLightItalic.ttf new file mode 100644 index 0000000..2037f4a Binary files /dev/null and b/public/fonts/Nunito-ExtraLightItalic.ttf differ diff --git a/public/fonts/Nunito-Italic.ttf b/public/fonts/Nunito-Italic.ttf new file mode 100644 index 0000000..f4ab114 Binary files /dev/null and b/public/fonts/Nunito-Italic.ttf differ diff --git a/public/fonts/Nunito-Light.ttf b/public/fonts/Nunito-Light.ttf new file mode 100644 index 0000000..e64c0fe Binary files /dev/null and b/public/fonts/Nunito-Light.ttf differ diff --git a/public/fonts/Nunito-LightItalic.ttf b/public/fonts/Nunito-LightItalic.ttf new file mode 100644 index 0000000..b465e5a Binary files /dev/null and b/public/fonts/Nunito-LightItalic.ttf differ diff --git a/public/fonts/Nunito-Medium.ttf b/public/fonts/Nunito-Medium.ttf new file mode 100644 index 0000000..e24c1d6 Binary files /dev/null and b/public/fonts/Nunito-Medium.ttf differ diff --git a/public/fonts/Nunito-MediumItalic.ttf b/public/fonts/Nunito-MediumItalic.ttf new file mode 100644 index 0000000..6fec095 Binary files /dev/null and b/public/fonts/Nunito-MediumItalic.ttf differ diff --git a/public/fonts/Nunito-Regular.ttf b/public/fonts/Nunito-Regular.ttf new file mode 100644 index 0000000..9411bfb Binary files /dev/null and b/public/fonts/Nunito-Regular.ttf differ diff --git a/public/fonts/Nunito-SemiBold.ttf b/public/fonts/Nunito-SemiBold.ttf new file mode 100644 index 0000000..1326a7d Binary files /dev/null and b/public/fonts/Nunito-SemiBold.ttf differ diff --git a/public/fonts/Nunito-SemiBoldItalic.ttf b/public/fonts/Nunito-SemiBoldItalic.ttf new file mode 100644 index 0000000..d20e95f Binary files /dev/null and b/public/fonts/Nunito-SemiBoldItalic.ttf differ diff --git a/public/img/arrowUp.svg b/public/img/arrowUp.svg new file mode 100644 index 0000000..134c1d8 --- /dev/null +++ b/public/img/arrowUp.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/closeCrossRed.svg b/public/img/closeCrossRed.svg new file mode 100644 index 0000000..e208ce0 --- /dev/null +++ b/public/img/closeCrossRed.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/cross.svg b/public/img/cross.svg new file mode 100644 index 0000000..a4857b6 --- /dev/null +++ b/public/img/cross.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/img/headerIcon.svg b/public/img/headerIcon.svg new file mode 100644 index 0000000..758a8e2 --- /dev/null +++ b/public/img/headerIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/redCross.svg b/public/img/redCross.svg new file mode 100644 index 0000000..881e5a1 --- /dev/null +++ b/public/img/redCross.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/smallArrow.svg b/public/img/smallArrow.svg new file mode 100644 index 0000000..733f876 --- /dev/null +++ b/public/img/smallArrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/splitter.svg b/public/img/splitter.svg new file mode 100644 index 0000000..82a9a0c --- /dev/null +++ b/public/img/splitter.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/(admin)/[date]/page.tsx b/src/app/(admin)/[date]/page.tsx new file mode 100644 index 0000000..3e890d1 --- /dev/null +++ b/src/app/(admin)/[date]/page.tsx @@ -0,0 +1,46 @@ +"use server"; + +import CreateMenu from "@/modules/CreateMenu/CreateMenu"; +import getToken from "@/utils/getToken"; +import getMenusByDate from "@/queries/getMenusByDate"; +import { getClient } from "@/lib/ssrApolloClient"; +import getTime from "@/lib/getTime"; + +const CreateMenuPage = async ({ params }: { params: { date: string } }) => { + const token = getToken(); + + const { data: menusByDate } = await getClient() + .query({ + query: getMenusByDate, + variables: { + dateFrom: getTime({ + date: params.date, + day: "middle", + }), + dateTo: getTime({ + date: params.date, + day: "middle", + }), + }, + context: { + headers: { + Authorization: getToken(), + }, + fetchOptions: { + next: { tags: ["menusByDate"] }, + }, + }, + fetchPolicy: "network-only", + }) + .catch((err) => err); + + return ( + + ); +}; + +export default CreateMenuPage; diff --git a/src/app/(admin)/finance/page.tsx b/src/app/(admin)/finance/page.tsx new file mode 100644 index 0000000..e0cc2f0 --- /dev/null +++ b/src/app/(admin)/finance/page.tsx @@ -0,0 +1,7 @@ +import { Fragment } from "react"; + +const Page = async () => { + return ; +}; + +export default Page; diff --git a/src/app/(admin)/jidelni-listek/page.tsx b/src/app/(admin)/jidelni-listek/page.tsx new file mode 100644 index 0000000..13d98bd --- /dev/null +++ b/src/app/(admin)/jidelni-listek/page.tsx @@ -0,0 +1,12 @@ +"use server"; + +import AdminPage from "@/modules/AdminPage/AdminPage"; +import getMenus from "@/modules/AdminPage/actions/getMenus"; + +const Page = async () => { + const menusByDate = await getMenus(); + + return ; +}; + +export default Page; diff --git a/src/app/(admin)/layout.tsx b/src/app/(admin)/layout.tsx new file mode 100644 index 0000000..4fab7fb --- /dev/null +++ b/src/app/(admin)/layout.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import type { Metadata } from "next"; +import { ApolloWrapper } from "../ApolloWrapper"; +import { Analytics } from "@vercel/analytics/react"; +import { redirect } from "next/navigation"; +import { cookies } from "next/headers"; +import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; +import updateLocale from "dayjs/plugin/updateLocale"; +import { AdminLayout } from "@/components/AdminLayout"; + +import "../globals.css"; + +export const metadata: Metadata = { + title: "Menuroo", + description: "Menuroo administrace", +}; + +const Layout = ({ children }: { children: React.ReactNode }) => { + dayjs.extend(updateLocale, utc); + dayjs.updateLocale("en", { + weekStart: 1, + }); + + const cookieStore = cookies(); + + const isAdmin = () => { + const jwt = cookieStore.get("token")?.value; + if (jwt) { + const decodedToken = JSON.parse(atob(jwt.split(".")[1])); + return decodedToken.role === "admin"; + } + + return false; + }; + + if (!isAdmin()) { + return redirect("/"); + } + + return ( + + + + + {children} + + + + + ); +}; + +export default Layout; diff --git a/src/app/(admin)/nastaveni/page.tsx b/src/app/(admin)/nastaveni/page.tsx new file mode 100644 index 0000000..a2244a8 --- /dev/null +++ b/src/app/(admin)/nastaveni/page.tsx @@ -0,0 +1,9 @@ +"use server"; + +import SettingsPage from "@/modules/Settings/SettingsPage"; + +const Page = async () => { + return ; +}; + +export default Page; diff --git a/src/app/(admin)/not-found.tsx b/src/app/(admin)/not-found.tsx new file mode 100644 index 0000000..abf4006 --- /dev/null +++ b/src/app/(admin)/not-found.tsx @@ -0,0 +1,11 @@ +import { cookies } from "next/headers"; +import Link from "next/link"; +import React from "react"; + +const RootNotFound = () => { + const redirectUrl = cookies().get("token") ? "/jidelni-listek" : "/"; + + return Domů; +}; + +export default RootNotFound; diff --git a/src/app/(admin)/objednavky/page.tsx b/src/app/(admin)/objednavky/page.tsx new file mode 100644 index 0000000..ab04818 --- /dev/null +++ b/src/app/(admin)/objednavky/page.tsx @@ -0,0 +1,12 @@ +"use server"; + +import MenuOrdersPage from "@/modules/MenuOrdersPage/MenuOrdersPage"; +import getMenuOrders from "@/modules/MenuOrdersPage/actions/getOrders"; + +const Page = async () => { + const menuOrders = await getMenuOrders(); + + return ; +}; + +export default Page; diff --git a/src/app/admin/resources/page.tsx b/src/app/(admin)/resources/page.tsx similarity index 100% rename from src/app/admin/resources/page.tsx rename to src/app/(admin)/resources/page.tsx diff --git a/src/app/admin/settings/page.tsx b/src/app/(admin)/settings/page.tsx similarity index 52% rename from src/app/admin/settings/page.tsx rename to src/app/(admin)/settings/page.tsx index 9132a92..f75e76e 100644 --- a/src/app/admin/settings/page.tsx +++ b/src/app/(admin)/settings/page.tsx @@ -1,56 +1,59 @@ -import { PriceLevel, PriceLevelGroup } from "@/components/Settings/components"; +import { PriceLevel, PriceLevelGroup } from "@/modules/Settings/components"; import adminGetAllPriceLvlGroups from "@/queries/adminGetAllPriceLvlGroups"; import getToken from "@/utils/getToken"; import adminGetAllFoodPriceLvls from "@/queries/adminGetAllFoodPriceLvls"; -import UserGroup from "@/components/Settings/components/UserGroup/UserGroup"; +import UserGroup from "@/modules/Settings/components/UserGroup/UserGroup"; import { getClient } from "@/lib/ssrApolloClient"; import adminGetAllUserGroups from "@/queries/adminGetAllUserGroups"; -import { revalidatePath } from "next/cache"; - const getData = async () => { - "use server"; +const getData = async () => { + "use server"; - revalidatePath("/admin/settings"); + const { data: allPriceLvlGroupsData } = await getClient().query({ + query: adminGetAllPriceLvlGroups, + context: { + headers: { + Authorization: getToken(), + }, + fetchOptions: { + next: { tags: ["priceLvlGroups"] }, + }, + }, + fetchPolicy: "network-only", + }); - const { data: allPriceLvlGroupsData } = await getClient().query({ - query: adminGetAllPriceLvlGroups, - context: { - headers: { - Authorization: getToken(), - }, - }, - fetchPolicy: "network-only", - }); + const { data: priceLevelsData } = await getClient().query({ + query: adminGetAllFoodPriceLvls, + context: { + headers: { + Authorization: getToken(), + }, + fetchOptions: { + next: { tags: ["priceLevels"] }, + }, + }, + fetchPolicy: "network-only", + }); - const { data: priceLevelsData } = await getClient().query({ - query: adminGetAllFoodPriceLvls, - context: { - headers: { - Authorization: getToken(), - }, - }, - fetchPolicy: "network-only", - }); + const { data: userGroupsData } = await getClient().query({ + query: adminGetAllUserGroups, + context: { + headers: { + Authorization: getToken(), + }, + fetchOptions: { + next: { tags: ["userGroups"] }, + }, + }, + fetchPolicy: "network-only", + }); - const { data: userGroupsData } = await getClient().query({ - query: adminGetAllUserGroups, - context: { - headers: { - Authorization: getToken(), - }, - fetchOptions: { - next: { revalidate: 5 }, - }, - }, - fetchPolicy: "network-only", - }); - - return { - allPriceLvlGroupsData, - priceLevelsData, - userGroupsData, - }; - }; + return { + allPriceLvlGroupsData, + priceLevelsData, + userGroupsData, + }; +}; const Page = async () => { const token = getToken(); @@ -67,18 +70,15 @@ const Page = async () => {
{ const token = getToken(); diff --git a/src/app/(admin)/vydej/page.tsx b/src/app/(admin)/vydej/page.tsx new file mode 100644 index 0000000..5acaab3 --- /dev/null +++ b/src/app/(admin)/vydej/page.tsx @@ -0,0 +1,11 @@ +import WaiterOrdersPage from "@/modules/WaiterOrdersPage/WaiterOrdersPage"; +import getWaiterOrders from "@/modules/WaiterOrdersPage/actions/getWaiterOrders"; + +const Page = async () => { + // TODO: remove the "1" + const waiterOrders = await getWaiterOrders(1); + + return ; +}; + +export default Page; diff --git a/src/app/layout.tsx b/src/app/(login)/layout.tsx similarity index 85% rename from src/app/layout.tsx rename to src/app/(login)/layout.tsx index efaa66d..120890a 100644 --- a/src/app/layout.tsx +++ b/src/app/(login)/layout.tsx @@ -1,40 +1,29 @@ import React from "react"; import type { Metadata } from "next"; -import { ConfigProvider } from "antd"; -import { ApolloWrapper } from "./ApolloWrapper"; +import { ApolloWrapper } from "../ApolloWrapper"; import { Analytics } from "@vercel/analytics/react"; -import themeConfig from "@/theme/themeConfig"; - -import StyledComponentsRegistry from "@/lib/AntdRegistry"; - -import "./globals.css"; +import "../globals.css"; export const metadata: Metadata = { title: "Menuroo", description: "Menuroo administrace", }; -const RootLayout = ({ children }: { children: React.ReactNode }) => { +const LoginLayout = ({ children }: { children: React.ReactNode }) => { return ( - - -
- {children} - -
-
-
+ {children} +
); }; -export default RootLayout; +export default LoginLayout; // ⠀⢀⣒⠒⠆⠤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ // ⢠⡛⠛⠻⣷⣶⣦⣬⣕⡒⠤⢀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ diff --git a/src/app/page.tsx b/src/app/(login)/page.tsx similarity index 65% rename from src/app/page.tsx rename to src/app/(login)/page.tsx index 2d7b6d4..47912e6 100644 --- a/src/app/page.tsx +++ b/src/app/(login)/page.tsx @@ -1,14 +1,15 @@ import { cookies } from "next/headers"; -import LoginPage from "@/components/Login/LoginPage"; +import { LoginPage } from "@/modules/login/screens"; -const HomePage = () => { +const Page = () => { const setToken = async (token: string) => { "use server"; cookies().set("token", token); }; + return ; }; -export default HomePage; +export default Page; diff --git a/src/app/admin/[date]/page.tsx b/src/app/admin/[date]/page.tsx deleted file mode 100644 index 0718d4d..0000000 --- a/src/app/admin/[date]/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -"use server"; - -import CreateMenu from "@/components/CreateMenu/CreateMenu"; -import getToken from "@/utils/getToken"; - -const CreateMenuPage = ({ params }: { params: { date: string } }) => { - const token = getToken(); - - return ; -}; - -export default CreateMenuPage; diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx deleted file mode 100644 index f92e028..0000000 --- a/src/app/admin/layout.tsx +++ /dev/null @@ -1,146 +0,0 @@ -"use client"; - -import React, { useLayoutEffect } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { usePathname } from "next/navigation"; -import { Button } from "antd"; -import dayjs from "dayjs"; -import utc from "dayjs/plugin/utc"; -import { LogoutOutlined } from "@ant-design/icons"; -import updateLocale from "dayjs/plugin/updateLocale"; -import { navbarItems } from "@/components/Layout/constants"; -import isTokenInLocalStorage from "@/utils/isTokenInLocalStorage"; - -const AdminLayout = ({ children }: { children: React.ReactNode }) => { - const [selectedKey, setSelectedKey] = React.useState(""); - const [isLogged, setIsLogged] = React.useState(false); - const [isMenuOpen, setIsMenuOpen] = React.useState(false); - const pathname = usePathname(); - const router = useRouter(); - - dayjs.extend(updateLocale, utc); - dayjs.updateLocale("en", { - weekStart: 1, - }); - - useLayoutEffect(() => { - const isAdmin = () => { - const token = localStorage.getItem("token"); - if (token) { - const decodedToken = JSON.parse(atob(token.split(".")[1])); - return decodedToken.role === "admin"; - } - }; - - if (isTokenInLocalStorage() && isAdmin()) { - setIsLogged(true); - } else { - router.push("/"); - } - - setSelectedKey( - pathname === "/admin" - ? "1" - : pathname === "/admin/queue" - ? "2" - : pathname === "/admin/statistics" - ? "3" - : pathname === "/admin/resources" - ? "4" - : pathname === "/admin/settings" - ? "5" - : "1" - ); - }, [pathname, router]); - - const handleLogout = () => { - localStorage.removeItem("token"); - router.push("/"); - }; - - const onTabClick = (key: string) => { - switch (key) { - case "1": - return "/admin"; - case "2": - return "/admin/queue"; - case "3": - return "/admin/statistics"; - case "4": - // return "/admin/resources"; - case "5": - return "/admin/settings"; - default: - return "/admin"; - } - }; - - const HamburgerIcon = ( - - - - - - ); - - const CloseIcon = ( - - - - - ); - - return ( - <> - {isLogged && ( -
-
-
- setIsMenuOpen((prevState) => !prevState)} - > - {isMenuOpen ? CloseIcon : HamburgerIcon} - -
-
- {navbarItems.map((item) => { - const isSelected = item.key === selectedKey; - return ( - - {item.label} - - ); - })} -
- -
- {children} -
- )} - - ); -}; - -export default AdminLayout; diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx deleted file mode 100644 index 33d44d8..0000000 --- a/src/app/admin/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -"use server"; - -import getToken from "@/utils/getToken"; -import AdminPage from "@/components/AdminPage/AdminPage"; - -const Page = () => { - const token = getToken(); - - return ; -}; - -export default Page; diff --git a/src/app/admin/queue/page.tsx b/src/app/admin/queue/page.tsx deleted file mode 100644 index f3146e2..0000000 --- a/src/app/admin/queue/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import QueuePage from "@/components/QueuePage/QueuePage"; -import getToken from "@/utils/getToken"; - -const Page = () => { - const token = getToken(); - - return ; -}; - -export default Page; diff --git a/src/app/globals.css b/src/app/globals.css index 3727e2e..dcee558 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,10 +1,116 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@font-face { + font-family: "Nunito"; + src: url("/fonts/Nunito-Black.ttf") format("truetype"); + font-weight: 900; + font-style: normal; +} + +@font-face { + font-family: "Nunito"; + src: url("/fonts/Nunito-BlackItalic.ttf") format("truetype"); + font-weight: 900; + font-style: italic; +} + +@font-face { + font-family: "Nunito"; + src: url("/fonts/Nunito-Bold.ttf") format("truetype"); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: "Nunito"; + src: url("/fonts/Nunito-BoldItalic.ttf") format("truetype"); + font-weight: 700; + font-style: italic; +} + +@font-face { + font-family: "Nunito"; + src: url("/fonts/Nunito-ExtraBold.ttf") format("truetype"); + font-weight: 800; + font-style: normal; +} + +@font-face { + font-family: "Nunito"; + src: url("/fonts/Nunito-ExtraBoldItalic.ttf") format("truetype"); + font-weight: 800; + font-style: italic; +} + +@font-face { + font-family: "Nunito"; + src: url("/fonts/Nunito-Italic.ttf") format("truetype"); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: "Nunito"; + src: url("/fonts/Nunito-Light.ttf") format("truetype"); + font-weight: 300; + font-style: normal; +} +@font-face { + font-family: "Nunito"; + src: url("/fonts/Nunito-LightItalic.ttf") format("truetype"); + font-weight: 300; + font-style: italic; +} + +@font-face { + font-family: "Nunito"; + src: url("/fonts/Nunito-Medium.ttf") format("truetype"); + font-weight: 500; + font-style: normal; +} + +@font-face { + font-family: "Nunito"; + src: url("/fonts/Nunito-MediumItalic.ttf") format("truetype"); + font-weight: 500; + font-style: italic; +} + +@font-face { + font-family: "Nunito"; + src: url("/fonts/Nunito-Regular.ttf") format("truetype"); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: "Nunito"; + src: url("/fonts/Nunito-SemiBold.ttf") format("truetype"); + font-weight: 600; + font-style: normal; +} + +@font-face { + font-family: "Nunito"; + src: url("/fonts/Nunito-SemiBoldItalic.ttf") format("truetype"); + font-weight: 600; + font-style: italic; +} + +html, body { - color: #1f2937; - background: #fff; + font-family: "Nunito", sans-serif; + color: #030202; + background: var(--beige-web, #f9f7f2); +} + +* { + margin: 0; + padding: 0; + outline: none; + border: none; + box-sizing: border-box; + text-decoration: none; + font-family: "Nunito", sans-serif; } /* Hide scrollbar for Chrome, Safari and Opera */ diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx deleted file mode 100644 index 7dd4232..0000000 --- a/src/app/not-found.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import Link from "next/link"; -import React from "react"; -import { Button, Result } from "antd"; -import isTokenInLocalStorage from "@/utils/isTokenInLocalStorage"; - -const RootNotFound = () => { - const redirectUrl = isTokenInLocalStorage() ? "/admin" : "/"; - - return ( - - Domů - - } - /> - ); -}; - -export default RootNotFound; diff --git a/src/app/privacy/layout.tsx b/src/app/privacy/layout.tsx new file mode 100644 index 0000000..ccd16be --- /dev/null +++ b/src/app/privacy/layout.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import type { Metadata } from "next"; +import { Analytics } from "@vercel/analytics/react"; + +import "../globals.css"; + +export const metadata: Metadata = { + title: "Menuroo - Privacy", + description: "Menuroo administrace", +}; + +const PrivacyLayout = ({ children }: { children: React.ReactNode }) => { + return ( + + + {children} + + + + ); +}; + +export default PrivacyLayout; diff --git a/src/components/AdminLayout/AdminLayout.module.css b/src/components/AdminLayout/AdminLayout.module.css new file mode 100644 index 0000000..c09c75f --- /dev/null +++ b/src/components/AdminLayout/AdminLayout.module.css @@ -0,0 +1,12 @@ +.container { + display: flex; + flex-direction: row; + min-height: 100vh; + height: 100vh; +} + +.children { + width: 100%; + padding: 8rem 6rem 11.69rem 6rem; + box-sizing: border-box; +} \ No newline at end of file diff --git a/src/components/AdminLayout/AdminLayout.tsx b/src/components/AdminLayout/AdminLayout.tsx new file mode 100644 index 0000000..f86c6e4 --- /dev/null +++ b/src/components/AdminLayout/AdminLayout.tsx @@ -0,0 +1,19 @@ +import { ReactNode } from "react"; +import { Sidebar } from "./components/Sidebar"; + +import styles from "./AdminLayout.module.css"; + +interface Props { + children: ReactNode; +} + +const Adminlayout = (props: Props) => { + return ( +
+ +
{props.children}
+
+ ); +}; + +export default Adminlayout; diff --git a/src/components/AdminLayout/components/Sidebar/Sidebar.module.css b/src/components/AdminLayout/components/Sidebar/Sidebar.module.css new file mode 100644 index 0000000..20f594c --- /dev/null +++ b/src/components/AdminLayout/components/Sidebar/Sidebar.module.css @@ -0,0 +1,44 @@ +.container { + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 4rem 2.62rem 2.5rem 0; + width: 21.9375rem; + height: 100%; + box-sizing: border-box; + border-right: 1px solid var(--storking-my-shi, #EBEBEB); +} + +.content { + display: flex; + flex-direction: column; + justify-content: space-between; + width: 100%; + gap: 4.5rem; +} + +.nav { + width: 100%; + height: 100%; +} + +.menu { + display: flex; + flex-direction: column; +} + +.footer { + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: flex-start; + width: 100%; +} + +.footer h3 { + padding-left: 4.5rem; +} + +.restaurantTitle { + color: #9E9E9E; +} \ No newline at end of file diff --git a/src/components/AdminLayout/components/Sidebar/Sidebar.tsx b/src/components/AdminLayout/components/Sidebar/Sidebar.tsx new file mode 100644 index 0000000..231f34c --- /dev/null +++ b/src/components/AdminLayout/components/Sidebar/Sidebar.tsx @@ -0,0 +1,57 @@ +"use server"; + +import { headers } from "next/headers"; +import { Typography } from "@/components/Typography"; +import { LogoHeader } from "@/components/LogoHeader"; +import sidebarItems from "./constants/sidebarItems"; +import { Logout } from "./components/Logout"; +import { SidebarItem } from "./components/SidebarItem"; + +import styles from "./Sidebar.module.css"; + +const Sidebar = () => { + const url = headers().get("x-url"); + + const isActive = (link: string) => { + const path = url?.split("/"); + const length = path?.length; + + if (length && "/" + path[length - 1] === link) { + return true; + } + + return false; + }; + + return ( +
+
+ +
+ {sidebarItems.map((item) => ( + + {item.name} + + ))} +
+
+
+ + + Zlatá Štika + +
+
+ ); +}; + +export default Sidebar; diff --git a/src/components/AdminLayout/components/Sidebar/components/Logout/Logout.module.css b/src/components/AdminLayout/components/Sidebar/components/Logout/Logout.module.css new file mode 100644 index 0000000..3d5423f --- /dev/null +++ b/src/components/AdminLayout/components/Sidebar/components/Logout/Logout.module.css @@ -0,0 +1,3 @@ +.logout { + cursor: pointer; +} \ No newline at end of file diff --git a/src/components/AdminLayout/components/Sidebar/components/Logout/Logout.tsx b/src/components/AdminLayout/components/Sidebar/components/Logout/Logout.tsx new file mode 100644 index 0000000..fbad0f4 --- /dev/null +++ b/src/components/AdminLayout/components/Sidebar/components/Logout/Logout.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { Typography } from "@/components/Typography"; + +import styles from "./Logout.module.css"; + +const Logout = () => { + const router = useRouter(); + + const handleLogout = () => { + document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; + router.push("/"); + }; + + return ( + + Odhlásit se + + ); +}; + +export default Logout; diff --git a/src/components/AdminLayout/components/Sidebar/components/Logout/index.tsx b/src/components/AdminLayout/components/Sidebar/components/Logout/index.tsx new file mode 100644 index 0000000..3ba3c9b --- /dev/null +++ b/src/components/AdminLayout/components/Sidebar/components/Logout/index.tsx @@ -0,0 +1 @@ +export { default as Logout } from "./Logout"; diff --git a/src/components/AdminLayout/components/Sidebar/components/SidebarItem/SidebarItem.module.css b/src/components/AdminLayout/components/Sidebar/components/SidebarItem/SidebarItem.module.css new file mode 100644 index 0000000..e765069 --- /dev/null +++ b/src/components/AdminLayout/components/Sidebar/components/SidebarItem/SidebarItem.module.css @@ -0,0 +1,25 @@ +.sidebarItemContainer { + display: flex; + justify-content: flex-start; + align-items: center; + width: 19.3125rem; + height: 3.75rem; + + &.isActive { + border-radius: 0rem 6.25rem 6.25rem 0rem; + background: rgba(255, 0, 0, 0.03); + } +} + +.sidebarItem { + display: block; + color: #000; + font-size: 1.5rem; + font-weight: 800; + padding-left: 4.5rem; + + &.isActive { + color: var(--red, #D51F2C); + text-decoration: underline; + } +} \ No newline at end of file diff --git a/src/components/AdminLayout/components/Sidebar/components/SidebarItem/SidebarItem.tsx b/src/components/AdminLayout/components/Sidebar/components/SidebarItem/SidebarItem.tsx new file mode 100644 index 0000000..3d19e1c --- /dev/null +++ b/src/components/AdminLayout/components/Sidebar/components/SidebarItem/SidebarItem.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { Typography } from "@/components/Typography"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useState, useEffect } from "react"; + +import styles from "./SidebarItem.module.css"; + +interface Props { + active: boolean; + link: string; + children: string; +} + +const SidebarItem = (props: Props) => { + const pathname = usePathname(); + + const [isActiveStyle, setIsActiveStyle] = useState( + props.active ? styles.isActive : "" + ); + + useEffect(() => { + if (pathname.includes(props.link)) { + setIsActiveStyle(styles.isActive); + } else { + setIsActiveStyle(""); + } + }, [pathname]); + + return ( +
+ + + {props.children} + + +
+ ); +}; + +export default SidebarItem; diff --git a/src/components/AdminLayout/components/Sidebar/components/SidebarItem/index.tsx b/src/components/AdminLayout/components/Sidebar/components/SidebarItem/index.tsx new file mode 100644 index 0000000..0a11928 --- /dev/null +++ b/src/components/AdminLayout/components/Sidebar/components/SidebarItem/index.tsx @@ -0,0 +1 @@ +export { default as SidebarItem } from "./SidebarItem"; diff --git a/src/components/AdminLayout/components/Sidebar/constants/sidebarItems.ts b/src/components/AdminLayout/components/Sidebar/constants/sidebarItems.ts new file mode 100644 index 0000000..86e2922 --- /dev/null +++ b/src/components/AdminLayout/components/Sidebar/constants/sidebarItems.ts @@ -0,0 +1,24 @@ +const sidebarItems = [ + { + name: "Jídelní lístky", + link: "/jidelni-listek", + }, + { + name: "Výdej", + link: "/vydej", + }, + { + name: "Objednávky", + link: "/objednavky", + }, + { + name: "Finance", + link: "/finance", + }, + { + name: "Nastavení", + link: "/nastaveni", + }, +]; + +export default sidebarItems; diff --git a/src/components/AdminLayout/components/Sidebar/index.tsx b/src/components/AdminLayout/components/Sidebar/index.tsx new file mode 100644 index 0000000..c3c6406 --- /dev/null +++ b/src/components/AdminLayout/components/Sidebar/index.tsx @@ -0,0 +1 @@ +export { default as Sidebar } from "./Sidebar"; diff --git a/src/components/AdminLayout/index.tsx b/src/components/AdminLayout/index.tsx new file mode 100644 index 0000000..4e9ad31 --- /dev/null +++ b/src/components/AdminLayout/index.tsx @@ -0,0 +1 @@ +export { default as AdminLayout } from "./AdminLayout"; diff --git a/src/components/Dropdown/Dropdown.module.css b/src/components/Dropdown/Dropdown.module.css new file mode 100644 index 0000000..09a7b24 --- /dev/null +++ b/src/components/Dropdown/Dropdown.module.css @@ -0,0 +1,64 @@ +.container { + width: 100%; + min-height: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 0.875rem 1.5rem; + cursor: pointer; + position: relative; + + border-radius: 0.75rem; + border: 2px solid var(--Beige, #EEE8DA); + background: var(--Light-beige-copy, #F5F2EB); +} + +.container h4 { + z-index: 1; +} + +.chevron { + position: relative; + width: 1rem; + height: 1rem; + rotate: 180deg; + transition: rotate 0.2s ease-in-out; + z-index: 1; + + &.isOpen { + rotate: 0deg; + } +} + +.dropdownMenu { + position: absolute; + display: none; + padding-top: 3rem; + flex-direction: column; + width: 100%; + left: 0; + top: 0; + + &.isOpen { + display: flex; + } +} + +.dropdownMenu>div:last-child { + border-bottom-left-radius: 0.75rem; + border-bottom-right-radius: 0.75rem; + border-bottom: 2px solid var(--Beige, #EEE8DA); + +} + +.dropdownItem { + display: flex; + height: 2.5rem; + padding: 0.5rem 1.5rem; + width: 100%; + border-left: 2px solid var(--Beige, #EEE8DA); + border-right: 2px solid var(--Beige, #EEE8DA); + background: var(--Light-beige-copy, #F5F2EB); + cursor: pointer; +} diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx new file mode 100644 index 0000000..d2d589d --- /dev/null +++ b/src/components/Dropdown/Dropdown.tsx @@ -0,0 +1,63 @@ +import { useRef, useState, Ref, MouseEvent, MutableRefObject } from "react"; +import { Typography } from "@/components/Typography"; +import Image from "next/image" +import type { dropdownItem } from "./types/dropdownItem" +import cx from "classnames"; + +import styles from "./Dropdown.module.css" + +interface Props { + items: dropdownItem[] + className?: string + valueRef: MutableRefObject +} + +const Dropdown = (props: Props) => { + const [isOpened, setIsOpened] = useState(false) + + const handleOpen = () => { + setIsOpened(!isOpened) + } + + const handleSelect = (index: number) => { + const dropdownItem = props.items.at(index)?.index + + if (dropdownItem != null) { + props.valueRef.current = dropdownItem + } + } + + return ( +
+
+ +

Vyberte

+
+
+ expand setting +
+
+
+
+ {props.items.map((item: dropdownItem, index: number) => { + return ( +
handleSelect(index)}> + +

{item.value}

+
+
+ ) + })} +
+
+
+ ) +} + +export default Dropdown; diff --git a/src/components/Dropdown/index.tsx b/src/components/Dropdown/index.tsx new file mode 100644 index 0000000..ade5256 --- /dev/null +++ b/src/components/Dropdown/index.tsx @@ -0,0 +1 @@ +export { default as Dropdown } from './Dropdown'; diff --git a/src/components/Dropdown/types/dropdownItem.ts b/src/components/Dropdown/types/dropdownItem.ts new file mode 100644 index 0000000..da735cb --- /dev/null +++ b/src/components/Dropdown/types/dropdownItem.ts @@ -0,0 +1,6 @@ +export interface dropdownItem { + value: string; + index: number; + selectable?: boolean; + selected?: boolean; +} diff --git a/src/components/Input/Input.module.css b/src/components/Input/Input.module.css new file mode 100644 index 0000000..2f32631 --- /dev/null +++ b/src/components/Input/Input.module.css @@ -0,0 +1,15 @@ +.input { + display: flex; + width: 100%; + padding: 1rem; + height: 3.375rem; + align-self: stretch; + border-radius: 1rem; + border: 1px solid var(--Divider, #CACACA); + color: var(--black, #030202); + font-size: 1rem; + font-style: normal; + font-weight: 500; + line-height: normal; + outline: none; +} \ No newline at end of file diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx new file mode 100644 index 0000000..39db162 --- /dev/null +++ b/src/components/Input/Input.tsx @@ -0,0 +1,42 @@ +import { + ChangeEvent, + forwardRef, + DetailedHTMLProps, + InputHTMLAttributes, +} from "react"; +import cx from "classnames"; + +import styles from "./Input.module.css"; + +interface Props { + type: string; + required?: boolean; + className?: string; + onChange?: (e: ChangeEvent) => void; + children: string; +} + +const Input = forwardRef< + HTMLInputElement, + Props & + DetailedHTMLProps, HTMLInputElement> +>( + ( + { type, required = true, className, onChange, children, ...other }: Props, + ref + ) => { + return ( + + ); + } +); + +export default Input; diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx new file mode 100644 index 0000000..a5f313c --- /dev/null +++ b/src/components/Input/index.tsx @@ -0,0 +1 @@ +export { default as Input } from "./Input"; diff --git a/src/components/Login/LoginPage.tsx b/src/components/Login/LoginPage.tsx deleted file mode 100644 index 6d4c828..0000000 --- a/src/components/Login/LoginPage.tsx +++ /dev/null @@ -1,114 +0,0 @@ -"use client"; - -import React, { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { LockOutlined, UserOutlined } from "@ant-design/icons"; -import { Button, Form, Input, message, Typography } from "antd"; -import { useLazyQuery } from "@apollo/client"; -import login from "@/queries/login"; - -interface Props { - setToken: (token: string) => void; -} - -const LoginPage = (props: Props) => { - const [messageApi, contextHolder] = message.useMessage(); - const { Title } = Typography; - const router = useRouter(); - - const [loginQuery, { data, error }] = useLazyQuery(login, { - fetchPolicy: "network-only", - }); - - useEffect(() => { - if (!data?.login) return; - - props.setToken(data.login); - localStorage.setItem("token", data.login); - router.push("/admin"); - }, [data]); - - useEffect(() => { - if (error) { - messageApiError(); - } - }, [error]); - - const messageApiError = () => { - messageApi.open({ - type: "error", - content: "Zadané údaje nejsou správné!", - }); - }; - - const onFinish = (values: { email: string; password: string }) => { - loginQuery({ - variables: { - email: values.email, - password: values.password, - }, - }); - }; - - return ( - <> - {contextHolder} -
-
-

Vítejte v

- - Menuroo - -
- - } - placeholder="Email" - size="large" - /> - - - } - type="password" - placeholder="Heslo" - size="large" - /> - - - - -
-
-
- - ); -}; - -export default LoginPage; diff --git a/src/components/LogoHeader/LoginTitle.test.tsx b/src/components/LogoHeader/LoginTitle.test.tsx new file mode 100644 index 0000000..e0b7d53 --- /dev/null +++ b/src/components/LogoHeader/LoginTitle.test.tsx @@ -0,0 +1,14 @@ +import "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import LogoHeader from "./LogoHeader"; + +describe("Login title", () => { + it("renders the application title", () => { + render(); + + const heading = screen.getByRole("heading", { level: 1 }); + + expect(heading).toBeInTheDocument(); + expect(heading).toHaveTextContent("Menuroo"); + }); +}); diff --git a/src/components/LogoHeader/LogoHeader.module.css b/src/components/LogoHeader/LogoHeader.module.css new file mode 100644 index 0000000..913d9fb --- /dev/null +++ b/src/components/LogoHeader/LogoHeader.module.css @@ -0,0 +1,29 @@ +.container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + gap: 0.5rem; +} + +.container > h1 { + color: #000; + text-align: center; + font-family: 'Nunito', sans-serif; + font-weight: 900; + font-size: 2rem; + font-style: normal; + line-height: normal; +} + +.container > h2 { + color: #000; + font-family: "Nunito", sans-serif; + font-weight: 900; + text-align: center; + font-size: 1rem; + font-style: normal; + line-height: normal; +} + diff --git a/src/components/LogoHeader/LogoHeader.tsx b/src/components/LogoHeader/LogoHeader.tsx new file mode 100644 index 0000000..e5719e0 --- /dev/null +++ b/src/components/LogoHeader/LogoHeader.tsx @@ -0,0 +1,25 @@ +import Image from "next/image"; +import { Typography } from "@/components/Typography"; + +import styles from "./LogoHeader.module.css"; + +interface Props { + header: string; + subheader: string; +} + +const LogoHeader = (props: Props) => { + return ( +
+ header + + {props.header} + + + {props.subheader} + +
+ ); +}; + +export default LogoHeader; diff --git a/src/components/LogoHeader/index.tsx b/src/components/LogoHeader/index.tsx new file mode 100644 index 0000000..901c27d --- /dev/null +++ b/src/components/LogoHeader/index.tsx @@ -0,0 +1 @@ +export { default as LogoHeader } from "./LogoHeader"; diff --git a/src/components/Table/Table.module.css b/src/components/Table/Table.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx new file mode 100644 index 0000000..fa7c507 --- /dev/null +++ b/src/components/Table/Table.tsx @@ -0,0 +1,41 @@ +type TableDataTypes = string | number | boolean | JSX.Element; + +interface Props { + headings: Array; + data: Array>; +} + +const Table = (props: Props) => { + return ( + + + + {props.headings.map((heading, index) => ( + + ))} + + + + {props.data.map((row, index) => ( + + {Object.values(row).map((value, index) => ( + + ))} + + ))} + +
+ {heading} +
+ {value} +
+ ); +}; + +export default Table; diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx new file mode 100644 index 0000000..6c9ff96 --- /dev/null +++ b/src/components/Table/index.tsx @@ -0,0 +1 @@ +export { default as Table } from "./Table"; diff --git a/src/components/Typography/Typography.module.css b/src/components/Typography/Typography.module.css new file mode 100644 index 0000000..304a942 --- /dev/null +++ b/src/components/Typography/Typography.module.css @@ -0,0 +1,69 @@ +.element { + &.light { + font-weight: 300; + } + + &.normal { + font-weight: 600; + } + + &.base { + font-weight: 500; + } + + &.semibold { + font-weight: 700; + } + + &.semibolder { + font-weight: 800; + } + + &.bold { + font-weight: 900; + } + + &.heading { + font-size: 1rem; + + &.sm { + font-size: 0.75rem; + } + + &.smMd { + font-size: 1.125rem; + } + + &.XLsmMd { + font-size: 1.25rem; + } + + &.md { + font-size: 1.5rem; + } + + &.mdLg { + font-size: 1.75rem; + } + + &.lg { + font-size: 2rem; + } + } + + &.button { + font-size: 1rem; + + &.sm { + font-size: 0.75rem; + } + + &.md { + font-size: 1.5rem; + } + + &.lg { + font-size: 2rem; + } + } +} diff --git a/src/components/Typography/Typography.tsx b/src/components/Typography/Typography.tsx new file mode 100644 index 0000000..2de6d68 --- /dev/null +++ b/src/components/Typography/Typography.tsx @@ -0,0 +1,108 @@ +import { + ReactElement, + ReactNode, + cloneElement, + forwardRef, + DetailedHTMLProps, + ButtonHTMLAttributes, + HTMLAttributes, +} from "react"; +import cx from "classnames"; + +import styles from "./Typography.module.css"; + +type Variant = "heading" | "button"; +type Size = "sm" | "smMd" | "XLsmMd" | "md" | "mdLg" | "lg" | "xl"; +type Weight = "light" | "normal" | "base" | "semibold" | "semibolder" | "bold"; + +interface Props { + size?: Size; + weight?: Weight; + className?: string; + children: ReactNode; +} + +const Typography = ({ + size, + weight, + variant, + className, + children, +}: Props & { children: ReactElement; variant: Variant }) => { + const isVariant = (styleVariant: Variant) => variant === styleVariant; + const isSize = (styleSize: Size) => size === styleSize; + const isWeight = (styleWeight: Weight) => weight === styleWeight; + + return cloneElement(children, { + className: cx( + styles.element, + { + [styles.heading]: isVariant("heading"), + [styles.button]: isVariant("button"), + [styles.sm]: isSize("sm"), + [styles.smMd]: isSize("smMd"), + [styles.XLsmMd]: isSize("XLsmMd"), + [styles.md]: isSize("md"), + [styles.mdLg]: isSize("mdLg"), + [styles.lg]: isSize("lg"), + [styles.xl]: isSize("xl"), + [styles.light]: isWeight("light"), + [styles.normal]: isWeight("normal"), + [styles.base]: isWeight("base"), + [styles.semibold]: isWeight("semibold"), + [styles.semibolder]: isWeight("semibolder"), + [styles.bold]: isWeight("bold"), + }, + className + ), + }); +}; + +interface HeadingProps extends Props { + Tag: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; +} +const TypographyHeading = forwardRef< + HTMLHeadingElement, + HeadingProps & + DetailedHTMLProps, HTMLHeadingElement> +>(({ Tag, size, weight, className, children, ...other }: HeadingProps, ref) => { + return ( + + + {children} + + + ); +}); + +const TypographyButton = forwardRef< + HTMLButtonElement, + Props & + DetailedHTMLProps< + ButtonHTMLAttributes, + HTMLButtonElement + > +>(({ size, weight, className, children, ...other }: Props, ref) => { + return ( + + + + ); +}); + +Typography.Heading = TypographyHeading; +Typography.Button = TypographyButton; + +export default Typography; diff --git a/src/components/Typography/index.tsx b/src/components/Typography/index.tsx new file mode 100644 index 0000000..d9c03aa --- /dev/null +++ b/src/components/Typography/index.tsx @@ -0,0 +1 @@ +export { default as Typography } from "./Typography"; diff --git a/src/lib/dayjs.ts b/src/lib/dayjs.ts new file mode 100644 index 0000000..f63645e --- /dev/null +++ b/src/lib/dayjs.ts @@ -0,0 +1,8 @@ +import dayjs from "dayjs"; +import "dayjs/locale/cs"; +import utc from "dayjs/plugin/utc"; + +dayjs.locale("cs"); +dayjs.extend(utc); + +export default dayjs; diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..fe0996a --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from "next/server"; + +const middleware = (request: NextRequest) => { + if ( + request.method !== "GET" || + request.nextUrl.pathname.startsWith("/_next/") || + request.nextUrl.pathname.startsWith("/_vercel/") + ) { + return NextResponse.next(); + } + + const requestHeaders = new Headers(request.headers); + + if (request?.url) { + requestHeaders.set("x-url", request.url); + } else { + return NextResponse.next(); + } + + return NextResponse.next({ + request: { + headers: requestHeaders, + }, + }); +}; + +export default middleware; diff --git a/src/modules/AdminPage/AdminPage.tsx b/src/modules/AdminPage/AdminPage.tsx new file mode 100644 index 0000000..5c46e91 --- /dev/null +++ b/src/modules/AdminPage/AdminPage.tsx @@ -0,0 +1,27 @@ +"use client"; + +import React, { useState } from "react"; +import { MenuInterface, DayMenu } from "@/types/MenuInterface"; +import { Table } from "./components/Table"; +import mapMenusToSequencedMonthDays from "./utils/mapMenusToSequencedMonthDays"; +import getMenus from "./actions/getMenus"; + +interface Props { + menusByDate: MenuInterface[]; +} + +const AdminPage = (props: Props) => { + const [groupedMenus, setGroupedMenus] = useState( + mapMenusToSequencedMonthDays(props.menusByDate) + ); + + const updateMenus = async (unix: number) => { + const newMenus = await getMenus(unix); + setGroupedMenus(mapMenusToSequencedMonthDays(newMenus, unix)); + }; + + return ( + + ); +}; +export default AdminPage; diff --git a/src/modules/AdminPage/actions/getMenus.ts b/src/modules/AdminPage/actions/getMenus.ts new file mode 100644 index 0000000..2c81584 --- /dev/null +++ b/src/modules/AdminPage/actions/getMenus.ts @@ -0,0 +1,36 @@ +"use server"; + +import getToken from "@/utils/getToken"; +import getMenusByDate from "@/queries/getMenusByDate"; +import { getClient } from "@/lib/ssrApolloClient"; +import updateMenuInterval from "@/modules/AdminPage/utils/updateMenuInterval"; + +const getMenus = async (unix?: number) => { + try { + const viewedDatesUnix = updateMenuInterval(unix); + + const { data: menusByDate } = await getClient().query({ + query: getMenusByDate, + variables: { + dateFrom: viewedDatesUnix[0], + dateTo: viewedDatesUnix[1], + }, + context: { + headers: { + Authorization: getToken(), + }, + fetchOptions: { + next: { tags: ["menusByDate"] }, + }, + }, + fetchPolicy: "network-only", + }); + + return menusByDate.getMenusByDate ?? []; + } catch (error) { + return []; + } +}; + +export default getMenus; + diff --git a/src/modules/AdminPage/components/AdminPageContent/AdminPageContent.module.css b/src/modules/AdminPage/components/AdminPageContent/AdminPageContent.module.css new file mode 100644 index 0000000..a472b62 --- /dev/null +++ b/src/modules/AdminPage/components/AdminPageContent/AdminPageContent.module.css @@ -0,0 +1,4 @@ +.container { + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/src/components/AdminPage/AdminPage.tsx b/src/modules/AdminPage/components/AdminPageContent/AdminPageContent.tsx similarity index 62% rename from src/components/AdminPage/AdminPage.tsx rename to src/modules/AdminPage/components/AdminPageContent/AdminPageContent.tsx index d345a05..53257ec 100644 --- a/src/components/AdminPage/AdminPage.tsx +++ b/src/modules/AdminPage/components/AdminPageContent/AdminPageContent.tsx @@ -3,31 +3,33 @@ import React, { useEffect, useState } from "react"; import { useLazyQuery } from "@apollo/client"; import { useRouter } from "next/navigation"; -import { Typography, Calendar } from "antd"; +import { Calendar } from "antd"; import type { Dayjs } from "dayjs"; -import dayjs from "dayjs"; +import dayjs from "@/lib/dayjs"; import utc from "dayjs/plugin/utc"; import pickerLocale from "@/constants/PickerLocale"; import getMenusByDate from "@/queries/getMenusByDate"; import { MenuInterface } from "@/types/MenuInterface"; +import styles from "./AdminPageContent.module.css"; + interface Props { token: string; + menusByDate: MenuInterface[]; } -const AdminPage = (props: Props) => { - dayjs.extend(utc); +const AdminPageContent = (props: Props) => { const [menuMap, setMenuMap] = useState | undefined>( undefined ); + const [viewedDatesUnix, setViewedDatesUnix] = useState([ dayjs().startOf("month").subtract(5, "day").unix(), dayjs().endOf("month").add(15, "day").unix(), ]); - const { Title } = Typography; + const [value, setValue] = useState(dayjs().format("YYYY-MM-DD")); const router = useRouter(); - const currentDate = dayjs().utc(); const [menusByDate, { data }] = useLazyQuery(getMenusByDate, { context: { @@ -56,9 +58,9 @@ const AdminPage = (props: Props) => { }, [viewedDatesUnix]); useEffect(() => { - if (!data?.getMenusByDate) return; + if (!data?.getMenusByDate && !props.menusByDate) return; - const menuList: MenuInterface[] = data.getMenusByDate; + const menuList: MenuInterface[] = data?.getMenusByDate ?? props.menusByDate; const menuMap = new Map(); @@ -82,7 +84,7 @@ const AdminPage = (props: Props) => { } return newMenuMap; }); - }, [data]); + }, [props.menusByDate, data]); const mapHasValue = (timestamp: number) => { return menuMap?.has(timestamp); @@ -93,37 +95,8 @@ const AdminPage = (props: Props) => { return menuMap?.get(date); }; - return ( -
-
- - Jídelní lístek - -
- - setViewedDatesUnix([ - e.startOf("month").unix(), - e.endOf("month").unix(), - ]) - } - locale={pickerLocale} - cellRender={(date: Dayjs) => { - return ( -
-

- {mapHasValue(date.utc().startOf("day").unix()) - ? `(${getMapValueByDate(date.unix())})` - : null} -

-
- ); - }} - /> -
-
-
- ); + return
+ +
; }; -export default AdminPage; +export default AdminPageContent; diff --git a/src/modules/AdminPage/components/Table/Table.module.css b/src/modules/AdminPage/components/Table/Table.module.css new file mode 100644 index 0000000..ced0eae --- /dev/null +++ b/src/modules/AdminPage/components/Table/Table.module.css @@ -0,0 +1,24 @@ +.container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.headerContainer { + width: 100%; + height: fit-content; +} + +.rowWrapper { + border-radius: 1rem; + border: 1px solid var(--Beige, #EEE8DA); + overflow-y: scroll; + max-height: 45rem; + background-color: #F5F2EB; +} + +.rowWrapper::-webkit-scrollbar { + display: none; +} \ No newline at end of file diff --git a/src/modules/AdminPage/components/Table/Table.tsx b/src/modules/AdminPage/components/Table/Table.tsx new file mode 100644 index 0000000..7f6bfbe --- /dev/null +++ b/src/modules/AdminPage/components/Table/Table.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { useState } from "react"; +import dynamic from "next/dynamic"; +import dayjs from "@/lib/dayjs"; +import { Row } from "./components/Row"; +import { DateHeader } from "./components/DateHeader"; +import { DayMenu } from "@/types/MenuInterface"; +import getMonthName from "../../../../utils/getMonthName"; +import { MonthType } from "./types"; + +import styles from "./Table.module.css"; + +interface Props { + menus: DayMenu[]; + updateMenus: (unix: number) => void; +} + +const Modal = dynamic(() => import("./components/Modal/Modal")); + +const Table = (props: Props) => { + const currentMonth = getMonthName(dayjs().format("MM-DD-YYYY")) ?? ""; + const [modalData, setModalData] = useState(); + const [month, setMonth] = useState({ + displayName: currentMonth, + value: parseInt(dayjs().format("MM")), + }); + const [year, setYear] = useState(parseInt(dayjs().format("YYYY"))); + + const [modalVisible, setModalVisible] = useState(false); + + const handleMonthChange = (direction: -1 | 1) => { + const monthValue = month.value + direction; + let newMonth = monthValue; + + if (newMonth <= 0) { + newMonth = 12; + setYear(year - 1); + } else if (newMonth > 12) { + newMonth = 1; + setYear(year + 1); + } + + const newMonthName = getMonthName( + dayjs() + .month(newMonth - 1) + .format("MM-DD-YYYY") + ); + + setMonth({ + displayName: newMonthName ?? "", + value: newMonth, + }); + + props.updateMenus( + dayjs() + .utc() + .month(newMonth - 1) + .year(year) + .startOf("month") + .unix() + ); + }; + + const showModal = (date: number) => { + setModalVisible(true); + }; + + const hideModal = () => { + setModalVisible(false); + }; + + return ( +
+ {modalVisible && } +
+ +
+
+ {props.menus.map((menu: DayMenu, index: number) => ( + + ))} +
+
+ ); +}; + +export default Table; diff --git a/src/modules/AdminPage/components/Table/components/DateHeader/DateHeader.tsx b/src/modules/AdminPage/components/Table/components/DateHeader/DateHeader.tsx new file mode 100644 index 0000000..1fea6e0 --- /dev/null +++ b/src/modules/AdminPage/components/Table/components/DateHeader/DateHeader.tsx @@ -0,0 +1,49 @@ +import Image from "next/image"; +import styles from "./HeaderHeader.module.css"; +import cx from "classnames"; +import { Typography } from "@/components/Typography"; +import { MonthType } from "../../types"; + +interface Props { + month: MonthType; + year: number; + handleMonthChange: (direction: -1 | 1) => void; +} + +const DateHeader = (props: Props) => { + return ( +
+ +
+ + {props.month.displayName} {props.year} + +
+ +
+ ); +}; + +export default DateHeader; diff --git a/src/modules/AdminPage/components/Table/components/DateHeader/HeaderHeader.module.css b/src/modules/AdminPage/components/Table/components/DateHeader/HeaderHeader.module.css new file mode 100644 index 0000000..aad8d13 --- /dev/null +++ b/src/modules/AdminPage/components/Table/components/DateHeader/HeaderHeader.module.css @@ -0,0 +1,38 @@ +.dateHeader { + height: 100%; + width: 100%; + display: flex; + flex-direction: row; + align-items: center; +} + +.arrow { + &.isRotated { + transform: rotate(180deg); + } +} + +.arrowButton { + background: none; + border: none; + padding: 0; + cursor: pointer; + width: 1rem; + height: 100%; + min-height: 2rem; + display: flex; + justify-content: flex-end; + align-items: center; + &.isRotated { + justify-content: start; + } +} + +.heading { + width: 11rem; + min-width: fit-content; + display: flex; + justify-content: center; + align-items: center; + user-select: none; +} diff --git a/src/modules/AdminPage/components/Table/components/DateHeader/index.tsx b/src/modules/AdminPage/components/Table/components/DateHeader/index.tsx new file mode 100644 index 0000000..68a68da --- /dev/null +++ b/src/modules/AdminPage/components/Table/components/DateHeader/index.tsx @@ -0,0 +1 @@ +export { default as DateHeader } from "./DateHeader"; diff --git a/src/modules/AdminPage/components/Table/components/Modal/Modal.module.css b/src/modules/AdminPage/components/Table/components/Modal/Modal.module.css new file mode 100644 index 0000000..7722a75 --- /dev/null +++ b/src/modules/AdminPage/components/Table/components/Modal/Modal.module.css @@ -0,0 +1,117 @@ +.container { + width: 100vw; + height: 100vh; + top: 0; + left: 0; + z-index: 1; + position: absolute; + display: flex; + justify-content: center; + align-items: center; + background: rgba(0, 0, 0, 0.5); + + @media (max-height: 768px) { + align-items: start; + } +} + +.modalContainer { + width: 94.31rem; + max-width: 90%; + height: 54.81rem; + max-height: 90%; + box-sizing: border-box; + border-radius: 2rem; + background: var(--beige-web, #F9F7F2); + position: absolute; + overflow: scroll; +} + +.modalContainer::-webkit-scrollbar { + display: none; +} + +.content { + display: flex; + justify-content: center; + align-items: start; + flex-direction: column; + gap: 1.5rem; +} + +.itemsContainer { + display: flex; + justify-content: center; + align-items: start; + flex-direction: column; + gap: 1.5rem; + width: 100%; + height: 100%; + padding: 3rem 4.5rem 4.375rem 4.5rem; +} + +.heading { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + border-bottom: 2px solid var(--Beige, #EEE8DA); + box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.06); + padding-left: 4.5rem; + padding-right: 4.5rem; + height: 8.3rem; + + @media (max-width: 1080px) { + flex-direction: column; + align-items: start; + } +} + +.buttonControls { + display: flex; + flex-direction: row; + justify-content: space-between; + gap: 1.5rem; +} + +.button { + display: flex; + justify-content: center; + align-items: center; + padding: 1rem 1.44rem 1rem 1.44rem; + border-radius: 1rem; + gap: 0.62rem; + cursor: pointer; + background: var(--red, #D51F2C); + border: 1px solid var(--red, #D51F2C); +} + +.closeButton { + background-color: var(--red, #D51F2C); + color: var(--white, #FFF); +} + +.addButton { + background-color: transparent; + color: var(--red, #D51F2C); +} + +.buttonIcon { + position: relative; + width: 1.25rem; + height: 1.25rem; +} + +.splitContainer { + display: flex; + align-items: center; + justify-content: center; + width: 100%; +} + +.splitContainer div { + position: relative; + width: 95%; + height: 0.1rem; +} diff --git a/src/modules/AdminPage/components/Table/components/Modal/Modal.tsx b/src/modules/AdminPage/components/Table/components/Modal/Modal.tsx new file mode 100644 index 0000000..b8a6c87 --- /dev/null +++ b/src/modules/AdminPage/components/Table/components/Modal/Modal.tsx @@ -0,0 +1,113 @@ +import Image from "next/image"; +import type { OfferType } from "../../types" +import { DayMenu, MenuInterface } from "@/types/MenuInterface"; +import { Typography } from "@/components/Typography"; +import { MenuItem } from "./components/MenuItem"; +import unixGetDate from "@/modules/AdminPage/utils/unixGetDate"; +import cx from "classnames"; + +import styles from "./Modal.module.css"; +import { TypeItem } from "./components/TypeItem"; + +interface Props { + data: DayMenu | undefined; + hideModal: () => void; +} + +const Modal = (props: Props) => { + + const mockedOffers: OfferType[] = [ + { + name: "Hlavní jídlo", + items: [ + { name: "svickova", bookingDays: 1 }, + { name: "gulas", bookingDays: 2 }, + { name: "neco", bookingDays: 3 }, + ] + }, + { + name: "Polévka", + items: [ + { name: "svickova", bookingDays: 1 }, + { name: "gulas", bookingDays: 2 }, + { name: "neco", bookingDays: 3 }, + ] + }, + { + + name: "Pití", + items: [ + { name: "svickova", bookingDays: 1 }, + { name: "gulas", bookingDays: 2 }, + { name: "neco", bookingDays: 3 }, + ] + } + ] + + return ( +
+
+
+
+ + JÍDELNÍ LÍSTEK {props.data?.date && unixGetDate(props.data.date)} + +
+ + Přidat menu +
+ +
+
+ + Zavřít +
+ +
+
+
+
+
+ + Typy + + {mockedOffers.map((offer: OfferType, index: number) => ( + + ))} +
+
+ +
+
+ {props.data?.menus.length && ( + <> + + Menu + + {props.data?.menus.map((menu: MenuInterface, index: number) => ( + + ))} + + )} +
+
+
+
+ ); +}; + +export default Modal; diff --git a/src/modules/AdminPage/components/Table/components/Modal/components/Item/Item.module.css b/src/modules/AdminPage/components/Table/components/Modal/components/Item/Item.module.css new file mode 100644 index 0000000..a36128a --- /dev/null +++ b/src/modules/AdminPage/components/Table/components/Modal/components/Item/Item.module.css @@ -0,0 +1,83 @@ +.container { + display: flex; + flex-direction: column; + justify-content: start; + align-items: start; + width: 100%; + height: 6.5rem; + padding: 0 2.5rem 0 2.5rem; + border-radius: 1rem; + overflow: visible; + + border: 2px solid var(--Beige, #EEE8DA); + background: var(--beige-web, #F9F7F2); + + transition: height 0.2s ease-in-out; + + &.isOpened { + height: fit-content; + } +} + +.heading { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + min-height: 6rem; + user-select: none; + margin-bottom: 0.5rem; +} + +.heading div { + display: flex; + align-items: center; + gap: 1.25rem; +} + +.wrapper { + width: 100%; + height: 100%; + position: relative; +} + +.childWrapper { + display: flex; + flex-direction: column; + justify-content: center; + align-items: start; + gap: 1.5rem; + position: absolute; + height: fit-content; + width: 100%; + transition: opacity 0.2s ease-in-out; + padding-bottom: 2rem; + + opacity: 0; + + &.isOpened { + opacity: 1; + } +} + +.dropDownIcon { + width: 1.25rem; + height: 1.25rem; + position: relative; + transition: transform 0.2s ease-in-out; + transform: rotate(180deg); + + &.isRotated { + transform: rotate(0deg); + } +} + +.closeButton { + width: 1rem; + height: 1rem; + cursor: pointer; + outline: none; + border: none; + background: transparent; + position: relative; +} diff --git a/src/modules/AdminPage/components/Table/components/Modal/components/Item/Item.tsx b/src/modules/AdminPage/components/Table/components/Modal/components/Item/Item.tsx new file mode 100644 index 0000000..7c33463 --- /dev/null +++ b/src/modules/AdminPage/components/Table/components/Modal/components/Item/Item.tsx @@ -0,0 +1,71 @@ +import Image from "next/image"; +import { ReactNode, useRef, useState } from "react"; +import { Typography } from "@/components/Typography"; +import cx from "classnames"; + +import styles from "./Item.module.css" + +interface Props { + header: string; + children: ReactNode; +} + +const Item = (props: Props) => { + const [isOpened, setIsOpened] = useState(false); + const dropDownRef = useRef(null) + + const handleOpen = () => { + if (!dropDownRef.current) { + return + } + + // UGLY + if (!isOpened) { + dropDownRef.current.style.height = 'auto'; + const elementHeight = dropDownRef.current.scrollHeight; + dropDownRef.current.style.height = '6.5rem'; + + void dropDownRef.current.offsetHeight; + + dropDownRef.current.style.height = elementHeight + 'px'; + } else { + dropDownRef.current.style.height = '6.5rem'; + } + + setIsOpened(!isOpened); + } + return ( +
+
+
+
+ + + +
+ + {props.header} + +
+ + + +
+
+
+ {props.children} +
+
+
+ ); +} + +export default Item; diff --git a/src/modules/AdminPage/components/Table/components/Modal/components/Item/index.tsx b/src/modules/AdminPage/components/Table/components/Modal/components/Item/index.tsx new file mode 100644 index 0000000..684ee9c --- /dev/null +++ b/src/modules/AdminPage/components/Table/components/Modal/components/Item/index.tsx @@ -0,0 +1 @@ +export { default as Item } from "./Item" diff --git a/src/modules/AdminPage/components/Table/components/Modal/components/MenuItem/MenuItem.module.css b/src/modules/AdminPage/components/Table/components/Modal/components/MenuItem/MenuItem.module.css new file mode 100644 index 0000000..0d56f38 --- /dev/null +++ b/src/modules/AdminPage/components/Table/components/Modal/components/MenuItem/MenuItem.module.css @@ -0,0 +1,36 @@ +.menus { + display: flex; + justify-content: start; + flex-direction: column; + gap: 1.5rem; + height: 100%; + width: 100%; +} + +.horizontalMenu { + display: flex; + flex-direction: row; + gap: 1.5rem; + width: 100%; +} + +.horizontalMenu div { + flex: 1; +} + +.inputSection { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.levelDropdown { + width: 20.875rem; + position: relative; +} + +.input { + border-radius: 0.75rem; + border: 2px solid var(--Beige, #EEE8DA); + background: var(--Light-beige-copy, #F5F2EB); +} diff --git a/src/modules/AdminPage/components/Table/components/Modal/components/MenuItem/MenuItem.tsx b/src/modules/AdminPage/components/Table/components/Modal/components/MenuItem/MenuItem.tsx new file mode 100644 index 0000000..8ec11de --- /dev/null +++ b/src/modules/AdminPage/components/Table/components/Modal/components/MenuItem/MenuItem.tsx @@ -0,0 +1,65 @@ +import { useEffect, useRef, useState } from "react" +import { Typography } from "@/components/Typography"; +import { Dropdown } from "@/components/Dropdown"; +import { Input } from "@/components/Input"; +import { MenuInterface } from "@/types/MenuInterface"; +import { Item } from "../Item" + +import styles from "./MenuItem.module.css" + +interface Props { + menu: MenuInterface; + menuOrder: number; + header: string; +} + +const MenuItem = (props: Props) => { + const priceLevelRef = useRef() + + const [buttonState, setButtonState] = useState(false) + + useEffect(() => { + console.log(priceLevelRef) + }, [buttonState]) + + return ( + +
+
+ + Hlavní jídlo: + + + ... + +
+
+
+ + Polévka: + + + ... + +
+
+ + Pití: + + + ... + +
+
+
+
+
+ +
+
+ +
+ ); +} + +export default MenuItem; diff --git a/src/modules/AdminPage/components/Table/components/Modal/components/MenuItem/index.tsx b/src/modules/AdminPage/components/Table/components/Modal/components/MenuItem/index.tsx new file mode 100644 index 0000000..cf0669a --- /dev/null +++ b/src/modules/AdminPage/components/Table/components/Modal/components/MenuItem/index.tsx @@ -0,0 +1 @@ +export { default as MenuItem } from "./MenuItem"; diff --git a/src/modules/AdminPage/components/Table/components/Modal/components/TypeItem/TypeItem.module.css b/src/modules/AdminPage/components/Table/components/Modal/components/TypeItem/TypeItem.module.css new file mode 100644 index 0000000..b053588 --- /dev/null +++ b/src/modules/AdminPage/components/Table/components/Modal/components/TypeItem/TypeItem.module.css @@ -0,0 +1,39 @@ +.menus { + display: flex; + justify-content: start; + flex-direction: column; + gap: 1.5rem; + height: 100%; + width: 100%; +} + +.horizontalMenu { + display: flex; + flex-direction: row; + gap: 1.5rem; + width: 100%; +} + +.horizontalMenu div { + flex: 1; +} + +.inputSection { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.levelDropdown { + width: 20.875rem; + position: relative; + background: none; + outline: none; + border: none; +} + +.input { + border-radius: 0.75rem; + border: 2px solid var(--Beige, #EEE8DA); + background: var(--Light-beige-copy, #F5F2EB); +} diff --git a/src/modules/AdminPage/components/Table/components/Modal/components/TypeItem/TypeItem.tsx b/src/modules/AdminPage/components/Table/components/Modal/components/TypeItem/TypeItem.tsx new file mode 100644 index 0000000..313fb68 --- /dev/null +++ b/src/modules/AdminPage/components/Table/components/Modal/components/TypeItem/TypeItem.tsx @@ -0,0 +1,31 @@ +import { Typography } from "@/components/Typography"; +import { Input } from "@/components/Input"; +import { Item } from "../Item" +import type { OfferType, OfferTypeItem } from "../../../../types"; + +import styles from "./TypeItem.module.css" + +interface Props { + offer: OfferType; + header: string; +} + +const TypeItem = (props: Props) => { + return ( + +
+
+ {props.offer.items.map((item: OfferTypeItem) => { + return ( + + ... + + ) + })} +
+
+
+ ); +} + +export default TypeItem; diff --git a/src/modules/AdminPage/components/Table/components/Modal/components/TypeItem/index.tsx b/src/modules/AdminPage/components/Table/components/Modal/components/TypeItem/index.tsx new file mode 100644 index 0000000..eabaf8b --- /dev/null +++ b/src/modules/AdminPage/components/Table/components/Modal/components/TypeItem/index.tsx @@ -0,0 +1 @@ +export { default as TypeItem } from "./TypeItem" diff --git a/src/modules/AdminPage/components/Table/components/Row/Row.module.css b/src/modules/AdminPage/components/Table/components/Row/Row.module.css new file mode 100644 index 0000000..a9e8830 --- /dev/null +++ b/src/modules/AdminPage/components/Table/components/Row/Row.module.css @@ -0,0 +1,27 @@ +.container { + display: flex; + justify-content: space-between; + height: 5.6rem; + padding: 1.0625rem 2.5rem 1.0625rem 1.75rem; + + &.whiteBackground { + background-color: var(--beige-web, #F9F7F2); + } +} + +.actionContainer { + display: flex; + justify-content: space-between; + align-items: center; + gap: 4.44rem; + min-width: fit-content; +} + +.arrowButton { + background: none; + border: none; + cursor: pointer; + display: flex; + justify-content: flex-end; + align-items: center; +} \ No newline at end of file diff --git a/src/modules/AdminPage/components/Table/components/Row/Row.tsx b/src/modules/AdminPage/components/Table/components/Row/Row.tsx new file mode 100644 index 0000000..51feb0e --- /dev/null +++ b/src/modules/AdminPage/components/Table/components/Row/Row.tsx @@ -0,0 +1,56 @@ +import { Dispatch, SetStateAction } from "react"; +import Image from "next/image"; +import dayjs from "@/lib/dayjs"; +import getDayOfWeek from "@/utils/getWeekDay"; +import { Typography } from "@/components/Typography"; +import { DayMenu } from "@/types/MenuInterface"; +import unixGetDate from "@/modules/AdminPage/utils/unixGetDate"; +import cx from "classnames"; + +import styles from "./Row.module.css"; + +interface Props { + rowData: DayMenu; + oddRow: boolean; + setModalData: Dispatch>; + showModal: (date: number) => void; +} + +const Row = (props: Props) => { + const handleModalOpen = () => { + props.setModalData(props.rowData); + props.showModal(props.rowData.date); + }; + + return ( +
+
+ + {getDayOfWeek(dayjs.unix(props.rowData.date).format("MM-DD-YYYY"))} + + + {unixGetDate(props.rowData.date)} + +
+
+ + {props.rowData.menus.length} Menu + + +
+
+ ); +}; + +export default Row; diff --git a/src/modules/AdminPage/components/Table/components/Row/index.tsx b/src/modules/AdminPage/components/Table/components/Row/index.tsx new file mode 100644 index 0000000..9abd55d --- /dev/null +++ b/src/modules/AdminPage/components/Table/components/Row/index.tsx @@ -0,0 +1 @@ +export { default as Row } from "./Row"; diff --git a/src/modules/AdminPage/components/Table/index.tsx b/src/modules/AdminPage/components/Table/index.tsx new file mode 100644 index 0000000..6c9ff96 --- /dev/null +++ b/src/modules/AdminPage/components/Table/index.tsx @@ -0,0 +1 @@ +export { default as Table } from "./Table"; diff --git a/src/modules/AdminPage/components/Table/types.ts b/src/modules/AdminPage/components/Table/types.ts new file mode 100644 index 0000000..43a0655 --- /dev/null +++ b/src/modules/AdminPage/components/Table/types.ts @@ -0,0 +1,14 @@ +export type MonthType = { + displayName: string; + value: number; +}; + +export type OfferTypeItem = { + name: string + bookingDays: number; +} + +export type OfferType = { + name: string; + items: OfferTypeItem[] +} diff --git a/src/modules/AdminPage/utils/mapMenusToSequencedMonthDays.ts b/src/modules/AdminPage/utils/mapMenusToSequencedMonthDays.ts new file mode 100644 index 0000000..10384a3 --- /dev/null +++ b/src/modules/AdminPage/utils/mapMenusToSequencedMonthDays.ts @@ -0,0 +1,63 @@ +import dayjs from "@/lib/dayjs"; +import type { MenuInterface, DayMenu } from "src/types/MenuInterface"; + +const getUnixDaysForGivenMonth = (unix: number) => { + const days = dayjs.unix(unix).daysInMonth(); + + const daysArray = Array.from(Array(days).keys()).map((day) => day + 1); + + const unixDays = daysArray.map((day) => { + return dayjs.unix(unix).date(day).utc().startOf("day").unix(); + }); + + return unixDays; +}; + +const mapMenusToSequencedMonthDays = ( + menus: MenuInterface[], + dateUnix?: number +): DayMenu[] => { + const unixDays = getUnixDaysForGivenMonth(dateUnix ?? dayjs().unix()); + + const grouped = menus.reduce( + (acc: { [key: number]: MenuInterface[] }, menu) => { + if (!acc[menu.date]) { + acc[menu.date] = []; + } + acc[menu.date].push(menu); + return acc; + }, + {} + ); + + const mutatedMenu: DayMenu[] = Object.keys(grouped).map((date) => ({ + date: parseInt(date), + menus: grouped[parseInt(date)], + })); + + const menuMap: Map = new Map(); + mutatedMenu.map((menu: DayMenu) => { + menuMap.set(menu.date, menu); + }); + + const daysWithMenus: DayMenu[] = []; + + unixDays.map((day: number) => { + if (!menuMap.has(day)) { + daysWithMenus.push({ + date: day, + menus: [], + }); + return; + } + + daysWithMenus.push({ + date: day, + menus: menuMap.get(day)?.menus || [], + }); + }); + + return daysWithMenus; +}; + +export default mapMenusToSequencedMonthDays; diff --git a/src/modules/AdminPage/utils/unixGetDate.ts b/src/modules/AdminPage/utils/unixGetDate.ts new file mode 100644 index 0000000..cbb951f --- /dev/null +++ b/src/modules/AdminPage/utils/unixGetDate.ts @@ -0,0 +1,7 @@ +import dayjs from "@/lib/dayjs"; + +const unixGetDate = (unix: number) => { + return dayjs.unix(unix).format("DD.MM. YYYY"); +}; + +export default unixGetDate; diff --git a/src/modules/AdminPage/utils/updateMenuInterval.ts b/src/modules/AdminPage/utils/updateMenuInterval.ts new file mode 100644 index 0000000..bdbed5f --- /dev/null +++ b/src/modules/AdminPage/utils/updateMenuInterval.ts @@ -0,0 +1,17 @@ +import dayjs from "@/lib/dayjs"; + +const updateMenuInterval = (unix?: number) => { + if (!unix) { + return [ + dayjs().utc().startOf("month").unix(), + dayjs().utc().endOf("month").unix(), + ]; + } + + return [ + dayjs.unix(unix).utc().startOf("month").unix(), + dayjs.unix(unix).utc().endOf("month").unix(), + ]; +}; + +export default updateMenuInterval; diff --git a/src/components/CreateMenu/CreateMenu.tsx b/src/modules/CreateMenu/CreateMenu.tsx similarity index 77% rename from src/components/CreateMenu/CreateMenu.tsx rename to src/modules/CreateMenu/CreateMenu.tsx index 1249d47..8e2bfa8 100644 --- a/src/components/CreateMenu/CreateMenu.tsx +++ b/src/modules/CreateMenu/CreateMenu.tsx @@ -4,8 +4,8 @@ import React, { useEffect, useState } from "react"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; import { useRouter } from "next/navigation"; -import { Typography, Card, Button, Spin, message } from "antd"; -import MenuItem from "@/components/MenuItem/MenuItem"; +import { Typography, Card, Button, message } from "antd"; +import MenuItem from "@/modules/MenuItem/MenuItem"; import { PlusOutlined } from "@ant-design/icons"; import { MealOptionsInterface } from "@/types/MealOptionsInterface"; import { useQuery } from "@apollo/experimental-nextjs-app-support/ssr"; @@ -17,62 +17,41 @@ import adminCreateMenus from "@/queries/adminCreateMenus"; import adminCreateMenu from "@/queries/adminCreateMenu"; import adminUpdateMenu from "@/queries/adminUpdateMenus"; import adminDeleteMenu from "@/queries/adminDeleteMenu"; -import getMenusByDate from "@/queries/getMenusByDate"; -import { MenuInterface } from "@/types/MenuInterface"; +// import { any } from "@/types/any"; import adminGetAllFoodPriceLvls from "@/queries/adminGetAllFoodPriceLvls"; import { NoticeType } from "antd/lib/message/interface"; import getTime from "@/lib/getTime"; import pickerLocale from "@/constants/PickerLocale"; +import revalidate from "@/utils/revalidate"; - -const CreateMenu = ({ - params, - token, -}: { +interface Props { params: { date: string }; token: string; -}) => { + menus: any[]; +} + +const CreateMenu = (props: Props) => { const router = useRouter(); dayjs.extend(utc); - const menuByDate = useQuery(getMenusByDate, { - variables: { - dateFrom: getTime({ - date: params.date, - day: "middle", - }), - dateTo: getTime({ - date: params.date, - day: "middle", - }), - }, - context: { - headers: { - Authorization: token, - }, - }, - fetchPolicy: "network-only", - }); const [menuIsSet, setMenuIsSet] = useState(false); const [createMenus] = useMutation(adminCreateMenus); const [createMenu] = useMutation(adminCreateMenu); const [updateMenu] = useMutation(adminUpdateMenu); const [deleteMenu] = useMutation(adminDeleteMenu); - const parsedMenu: MenuInterface[] = menuByDate.data?.getMenusByDate.map( - (item: any) => { - return { - id: item.id, - order: item.Order, - key: item.id, - level: item.priceLvlId, - meal: item.meal, - soup: item.soup, - drink: item.drink, - }; - } - ); - const [menu, setMenu] = useState(undefined); + const parsedMenu: any[] = props?.menus.map((item: any) => { + return { + id: item.id, + order: item.Order, + key: item.id, + level: item.priceLvlId, + meal: item.meal, + soup: item.soup, + drink: item.drink, + }; + }); + const [menu, setMenu] = useState(undefined); const [menuOptions, setMenuOptions] = useState< MealOptionsInterface | undefined >(); @@ -83,7 +62,7 @@ const CreateMenu = ({ const options = { context: { headers: { - Authorization: token, + Authorization: props.token, }, }, }; @@ -127,10 +106,10 @@ const CreateMenu = ({ createMenus({ variables: { input: menu - ? menu?.map((item: MenuInterface) => { + ? menu?.map((item: any) => { return { date: getTime({ - date: params.date, + date: props.params.date, day: "middle", }), order: item.order, @@ -144,12 +123,13 @@ const CreateMenu = ({ }, context: { headers: { - Authorization: token, + Authorization: props.token, }, }, }); - router.push("/admin"); + revalidate("menusByDate"); + router.push("/jidelni-listek"); return; } @@ -158,14 +138,13 @@ const CreateMenu = ({ input: menu ? menu ?.filter( - (item: MenuInterface) => - item.id && !idsForDeletion.includes(item.id) + (item: any) => item.id && !idsForDeletion.includes(item.id) ) - .map((item: MenuInterface) => { + .map((item: any) => { return { id: item.id, date: getTime({ - date: params.date, + date: props.params.date, day: "middle", }), order: item.order, @@ -179,19 +158,19 @@ const CreateMenu = ({ }, context: { headers: { - Authorization: token, + Authorization: props.token, }, }, }); (menu ?? []) - .filter((item: MenuInterface) => !item.id) - .forEach((item: MenuInterface) => { + .filter((item: any) => !item.id) + .forEach((item: any) => { createMenu({ variables: { input: { date: getTime({ - date: params.date, + date: props.params.date, day: "middle", }), order: item.order, @@ -203,7 +182,7 @@ const CreateMenu = ({ }, context: { headers: { - Authorization: token, + Authorization: props.token, }, }, }); @@ -216,7 +195,7 @@ const CreateMenu = ({ }, context: { headers: { - Authorization: token, + Authorization: props.token, }, }, }); @@ -233,13 +212,14 @@ const CreateMenu = ({ }, context: { headers: { - Authorization: token, + Authorization: props.token, }, }, }); }); - router.push("/admin"); + revalidate("menusByDate"); + router.push("/jidelni-listek"); }; const messageApiSuccess = (message: string, type: NoticeType | undefined) => { @@ -249,10 +229,33 @@ const CreateMenu = ({ }); }; - const dateTitle = new Date(params.date).toLocaleDateString(pickerLocale.lang.locale, { weekday: 'long', month: 'numeric', day: 'numeric'}); + const dayNamesDict = { + neděle: "neděli", + pondělí: "pondělí", + úterý: "úterý", + středa: "středu", + čtvrtek: "čtvrtek", + pátek: "pátek", + sobota: "sobotu", + }; + + const date = new Date(props.params.date); + + const weekDay = date.toLocaleDateString(pickerLocale.lang.locale, { + weekday: "long", + }); + + const customDayName = dayNamesDict[weekDay as keyof typeof dayNamesDict]; + + const restOfDate = date.toLocaleDateString(pickerLocale.lang.locale, { + month: "numeric", + day: "numeric", + }); + + const dateTitle = `${customDayName} ${restOfDate}`; - if (!dayjs(params.date).date()) { - router.push("/admin"); + if (!dayjs(props.params.date).date()) { + router.push("/jidelni-listek"); return; } @@ -310,7 +313,6 @@ const CreateMenu = ({ - {menuByDate.loading && } ); diff --git a/src/components/Layout/constants.ts b/src/modules/Layout/constants.ts similarity index 100% rename from src/components/Layout/constants.ts rename to src/modules/Layout/constants.ts diff --git a/src/components/LunchItem/LunchItem.tsx b/src/modules/LunchItem/LunchItem.tsx similarity index 100% rename from src/components/LunchItem/LunchItem.tsx rename to src/modules/LunchItem/LunchItem.tsx diff --git a/src/components/LunchOrderTable.tsx b/src/modules/LunchOrderTable.tsx similarity index 100% rename from src/components/LunchOrderTable.tsx rename to src/modules/LunchOrderTable.tsx diff --git a/src/components/MenuItem/AutoCompleteInput.tsx b/src/modules/MenuItem/AutoCompleteInput.tsx similarity index 100% rename from src/components/MenuItem/AutoCompleteInput.tsx rename to src/modules/MenuItem/AutoCompleteInput.tsx diff --git a/src/components/MenuItem/MenuItem.tsx b/src/modules/MenuItem/MenuItem.tsx similarity index 96% rename from src/components/MenuItem/MenuItem.tsx rename to src/modules/MenuItem/MenuItem.tsx index c335824..5687148 100644 --- a/src/components/MenuItem/MenuItem.tsx +++ b/src/modules/MenuItem/MenuItem.tsx @@ -55,7 +55,7 @@ const MenuItem = ({ }, [menu]); useEffect(() => { - const updatedMenu: MenuInterface[] = (menu ?? []).map((item) => { + const updatedMenu: any[] = (menu ?? []).map((item) => { if (item.key === index) { return { id: item.id, key: index, level, meal, soup, drink, order }; } @@ -66,7 +66,7 @@ const MenuItem = ({ }, [meal, soup, drink, level, order]); const handleDelete = () => { - const updatedMenu: MenuInterface[] = (menu ?? []).filter( + const updatedMenu: any[] = (menu ?? []).filter( (item) => item.key !== index ); diff --git a/src/modules/MenuOrdersPage/MenuOrdersPage.module.css b/src/modules/MenuOrdersPage/MenuOrdersPage.module.css new file mode 100644 index 0000000..c676d50 --- /dev/null +++ b/src/modules/MenuOrdersPage/MenuOrdersPage.module.css @@ -0,0 +1,11 @@ +.container { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.boxWrapper { + display: flex; + flex-direction: column; + gap: 2.5rem; +} \ No newline at end of file diff --git a/src/modules/MenuOrdersPage/MenuOrdersPage.tsx b/src/modules/MenuOrdersPage/MenuOrdersPage.tsx new file mode 100644 index 0000000..64ddd8a --- /dev/null +++ b/src/modules/MenuOrdersPage/MenuOrdersPage.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Typography } from "@/components/Typography"; +import { MenuOrdersInterface } from "./types/MenuOrders"; +import { DayBox } from "./components/DayBox"; + +import styles from "./MenuOrdersPage.module.css"; + +interface Props { + menuOrders: MenuOrdersInterface[]; +} + +const MenuOrdersPage = (props: Props) => { + console.log(props.menuOrders); + return ( +
+ + Objednávky + +
+ {props.menuOrders.map((day: MenuOrdersInterface) => ( + + ))} +
+
+ ); +}; + +export default MenuOrdersPage; diff --git a/src/modules/MenuOrdersPage/actions/getOrders.ts b/src/modules/MenuOrdersPage/actions/getOrders.ts new file mode 100644 index 0000000..9c8edfc --- /dev/null +++ b/src/modules/MenuOrdersPage/actions/getOrders.ts @@ -0,0 +1,34 @@ +"use server"; + +import getToken from "@/utils/getToken"; +import adminGetMenuOrders from "@/queries/adminGetMenuOrders"; +import dayjs from "@/lib/dayjs"; +import { getClient } from "@/lib/ssrApolloClient"; + +const getMenuOrders = async (unix?: number) => { + // TODO: add dateFrom and dateTo + try { + const { data: getMenuOrders } = await getClient().query({ + query: adminGetMenuOrders, + variables: { + dateFrom: 0, + dateTo: dayjs().utc().endOf("day").unix(), + }, + context: { + headers: { + Authorization: getToken(), + }, + fetchOptions: { + next: { tags: ["menusByDate"] }, + }, + }, + fetchPolicy: "network-only", + }); + + return getMenuOrders.adminGetMenuOrders ?? []; + } catch (error) { + return []; + } +}; + +export default getMenuOrders; diff --git a/src/modules/MenuOrdersPage/components/DayBox/DayBox.module.css b/src/modules/MenuOrdersPage/components/DayBox/DayBox.module.css new file mode 100644 index 0000000..83c1e06 --- /dev/null +++ b/src/modules/MenuOrdersPage/components/DayBox/DayBox.module.css @@ -0,0 +1,16 @@ +.container { + height: fit-content; + display: flex; + width: 55.375rem; + padding: 2rem 3rem 4rem 3rem; + flex-direction: column; + align-items: flex-start; + gap: 1.5rem; + border-radius: 1rem; + border: 1px solid var(--Beige, #EEE8DA); + background: var(--beige-light, #F5F2EB); +} + +.header { + +} \ No newline at end of file diff --git a/src/modules/MenuOrdersPage/components/DayBox/DayBox.tsx b/src/modules/MenuOrdersPage/components/DayBox/DayBox.tsx new file mode 100644 index 0000000..238b189 --- /dev/null +++ b/src/modules/MenuOrdersPage/components/DayBox/DayBox.tsx @@ -0,0 +1,35 @@ +import { Typography } from "@/components/Typography"; +import dayjs from "@/lib/dayjs"; +import { MenuOrdersInterface } from "../../types/MenuOrders"; +import styles from "./DayBox.module.css"; + +interface Props { + data: MenuOrdersInterface; +} + +const DayBox = (props: Props) => { + return ( +
+
+ + {dayjs.unix(props.data.date).utc().format("DD.MM.YYYY")} + +
+
+ + Polévky + + {props.data.soup} +
+
+ + Hlavní jídla + + {props.data.meal} +
+
+
+ ); +}; + +export default DayBox; diff --git a/src/modules/MenuOrdersPage/components/DayBox/index.tsx b/src/modules/MenuOrdersPage/components/DayBox/index.tsx new file mode 100644 index 0000000..77f561e --- /dev/null +++ b/src/modules/MenuOrdersPage/components/DayBox/index.tsx @@ -0,0 +1 @@ +export { default as DayBox } from "./DayBox"; diff --git a/src/modules/MenuOrdersPage/types/MenuOrders.ts b/src/modules/MenuOrdersPage/types/MenuOrders.ts new file mode 100644 index 0000000..cc4cb4a --- /dev/null +++ b/src/modules/MenuOrdersPage/types/MenuOrders.ts @@ -0,0 +1,9 @@ +export interface MenuOrdersInterface { + id: string; + date: number; + meal: string; + drink: string; + soup: string; + Order: number; + Count: number; +} diff --git a/src/components/OrderItem.tsx b/src/modules/OrderItem.tsx similarity index 93% rename from src/components/OrderItem.tsx rename to src/modules/OrderItem.tsx index dfd8ca8..6a794dd 100644 --- a/src/components/OrderItem.tsx +++ b/src/modules/OrderItem.tsx @@ -1,7 +1,7 @@ "use client"; import React from "react"; -import type { LunchOrder } from "@/types/LunchOrder"; +import type { WaiterOrder } from "./WaiterOrdersPage/types/waiterOrder"; import dayjs from "dayjs"; interface ColInterface { @@ -15,7 +15,7 @@ const OrderItem = ({ index, columns, }: { - order: LunchOrder; + order: WaiterOrder; index: number; columns: ColInterface[]; }) => { diff --git a/src/components/PageTitle.tsx b/src/modules/PageTitle.tsx similarity index 100% rename from src/components/PageTitle.tsx rename to src/modules/PageTitle.tsx diff --git a/src/components/QueuePage/QueuePage.tsx b/src/modules/QueuePage/QueuePage.tsx similarity index 92% rename from src/components/QueuePage/QueuePage.tsx rename to src/modules/QueuePage/QueuePage.tsx index 33d7d96..1340115 100644 --- a/src/components/QueuePage/QueuePage.tsx +++ b/src/modules/QueuePage/QueuePage.tsx @@ -2,10 +2,10 @@ import React, { useEffect, useState } from "react"; import { useLazyQuery } from "@apollo/client"; -import PageTitle from "@/components/PageTitle"; -import LunchOrderTable from "@/components/LunchOrderTable"; +import PageTitle from "@/modules/PageTitle"; +import LunchOrderTable from "@/modules/LunchOrderTable"; import { Button, DatePicker } from "antd"; -import RefreshButton from "@/components/RefreshButton"; +import RefreshButton from "@/modules/RefreshButton"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; import pickerLocale from "@/constants/PickerLocale"; diff --git a/src/components/RefreshButton.tsx b/src/modules/RefreshButton.tsx similarity index 100% rename from src/components/RefreshButton.tsx rename to src/modules/RefreshButton.tsx diff --git a/src/modules/Settings/SettingsPage.module.css b/src/modules/Settings/SettingsPage.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/Settings/SettingsPage.tsx b/src/modules/Settings/SettingsPage.tsx new file mode 100644 index 0000000..cac493c --- /dev/null +++ b/src/modules/Settings/SettingsPage.tsx @@ -0,0 +1,32 @@ +import { Table } from "./components/Table"; + +import styles from "./SettingsPage.module.css"; + +export interface SettingItem { + rowTitle: string; +} + +const Settings = () => { + const settingsItems: SettingItem[] = [ + { + rowTitle: "Uživatelé restaurace", + }, + { + rowTitle: "Nápoje", + }, + { + rowTitle: "Skupiny strávníků", + }, + { + rowTitle: "Cenové hladiny", + }, + ]; + + return ( +
+
+ + ); +}; + +export default Settings; diff --git a/src/modules/Settings/components/Table/Table.module.css b/src/modules/Settings/components/Table/Table.module.css new file mode 100644 index 0000000..50b2021 --- /dev/null +++ b/src/modules/Settings/components/Table/Table.module.css @@ -0,0 +1,14 @@ +.container { + width: 100%; + max-width: 41.875rem; + max-height: 45rem; + display: flex; + flex-direction: column; + border-radius: 1rem; + border: 1px solid var(--Beige, #EEE8DA); + overflow-y: scroll; +} + +.container::-webkit-scrollbar { + display: none; +} \ No newline at end of file diff --git a/src/modules/Settings/components/Table/Table.tsx b/src/modules/Settings/components/Table/Table.tsx new file mode 100644 index 0000000..9bb3e9d --- /dev/null +++ b/src/modules/Settings/components/Table/Table.tsx @@ -0,0 +1,21 @@ +import { Row } from "./components"; +import type { SettingItem } from "../../SettingsPage"; + +import styles from "./Table.module.css"; + +interface Props { + data: SettingItem[]; +} +const Table = (props: Props) => { + return ( +
+ {props.data.map((item: SettingItem, index: number) => ( + + {item} + + ))} +
+ ); +}; + +export default Table; diff --git a/src/modules/Settings/components/Table/components/Row.module.css b/src/modules/Settings/components/Table/components/Row.module.css new file mode 100644 index 0000000..817aa59 --- /dev/null +++ b/src/modules/Settings/components/Table/components/Row.module.css @@ -0,0 +1,31 @@ +.row { + display: flex; + height: 5.625rem; + padding: 0 3rem 0 3rem; + justify-content: space-between; + align-items: center; + + &.beigeBackground { + background: #F5F2EB; + } +} + + +.editRowButton { + outline: none; + border: none; + background: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + text-decoration: underline; +} + +.rightArrow { + width: 0.5625rem; + height: 0.9375rem; + position: relative; +} + diff --git a/src/modules/Settings/components/Table/components/Row.tsx b/src/modules/Settings/components/Table/components/Row.tsx new file mode 100644 index 0000000..c7babf3 --- /dev/null +++ b/src/modules/Settings/components/Table/components/Row.tsx @@ -0,0 +1,35 @@ +import Image from "next/image"; +import { Typography } from "@/components/Typography"; +import type { SettingItem } from "@/modules/Settings/SettingsPage"; +import cx from "classnames"; + +import styles from "./Row.module.css"; + +interface Props { + children: SettingItem; + isEven: boolean; +} + +const Row = (props: Props) => { + return ( +
+ + {props.children.rowTitle} + +
+ + Upravit +
+ +
+
+
+
+ ); +}; + +export default Row; diff --git a/src/modules/Settings/components/Table/components/index.tsx b/src/modules/Settings/components/Table/components/index.tsx new file mode 100644 index 0000000..9abd55d --- /dev/null +++ b/src/modules/Settings/components/Table/components/index.tsx @@ -0,0 +1 @@ +export { default as Row } from "./Row"; diff --git a/src/modules/Settings/components/Table/index.tsx b/src/modules/Settings/components/Table/index.tsx new file mode 100644 index 0000000..6c9ff96 --- /dev/null +++ b/src/modules/Settings/components/Table/index.tsx @@ -0,0 +1 @@ +export { default as Table } from "./Table"; diff --git a/src/components/Settings/components/PriceLevel/PriceLevel.tsx b/src/modules/SettingsOld/components/PriceLevel/PriceLevel.tsx similarity index 97% rename from src/components/Settings/components/PriceLevel/PriceLevel.tsx rename to src/modules/SettingsOld/components/PriceLevel/PriceLevel.tsx index 93be87d..36d27df 100644 --- a/src/components/Settings/components/PriceLevel/PriceLevel.tsx +++ b/src/modules/SettingsOld/components/PriceLevel/PriceLevel.tsx @@ -16,9 +16,9 @@ import adminCreateFoodPriceLvl from "@/queries/adminCreateFoodPriceLvl"; import adminDeleteFoodPriceLvl from "@/queries/adminDeleteFoodPriceLvl"; import adminUpdateFoodPriceLvl from "@/queries/adminUpdateFoodPriceLvl"; import { tableSortLocale } from "@/constants/PickerLocale"; +import revalidate from "@/utils/revalidate"; interface Props { - revalidateData: () => void; data: PriceLevelInterface[]; token: string; } @@ -76,7 +76,7 @@ const PriceLevel = (props: Props) => { }, }); - props.revalidateData(); + revalidate("priceLevels"); }; const deleteModalSubmitCallback = () => { @@ -95,7 +95,7 @@ const PriceLevel = (props: Props) => { }, }); - props.revalidateData(); + revalidate("priceLevels"); }; const updateModalSubmitCallback = (data: PriceLevelInterface) => { @@ -117,7 +117,7 @@ const PriceLevel = (props: Props) => { }, }); - props.revalidateData(); + revalidate("priceLevels"); }; return ( diff --git a/src/components/Settings/components/PriceLevelGroup/PriceLevelGroup.tsx b/src/modules/SettingsOld/components/PriceLevelGroup/PriceLevelGroup.tsx similarity index 98% rename from src/components/Settings/components/PriceLevelGroup/PriceLevelGroup.tsx rename to src/modules/SettingsOld/components/PriceLevelGroup/PriceLevelGroup.tsx index d12d370..d65d6cd 100644 --- a/src/components/Settings/components/PriceLevelGroup/PriceLevelGroup.tsx +++ b/src/modules/SettingsOld/components/PriceLevelGroup/PriceLevelGroup.tsx @@ -17,7 +17,7 @@ import type { CreatePriceTypeGroupInterface, GetPriceLevelGroupInterface, MutatedPriceLevelGroupInterface, -} from "@/components/Settings/types"; +} from "@/modules/Settings/types"; import adminCreatePriceLvlGroup from "@/queries/adminCreatePriceLvlGroup"; import adminDeletePriceLvlGroup from "@/queries/adminDeletePriceLvlGroup"; import adminUpdatePriceLvlGroup from "@/queries/adminUpdatePriceLvlGroup"; @@ -29,13 +29,13 @@ import pickerLocale from "@/constants/PickerLocale"; import { PriceLevelInterface } from "@/types/PriceLevelInterface"; import { tableSortLocale } from "@/constants/PickerLocale"; import filterSelectOptions from "@/utils/filterSelectOption"; +import revalidate from "@/utils/revalidate"; interface Props { data: GetPriceLevelGroupInterface[]; priceLevels: PriceLevelInterface[]; userGroups: PriceLevelInterface[]; token: string; - revalidateData: () => void; } const PriceLevelGroup = (props: Props) => { @@ -149,7 +149,7 @@ const PriceLevelGroup = (props: Props) => { }, }); - props.revalidateData(); + revalidate("priceLvlGroups"); }; const deleteModalSubmitCallback = () => { @@ -168,7 +168,7 @@ const PriceLevelGroup = (props: Props) => { }, }); - props.revalidateData(); + revalidate("priceLvlGroups"); }; const updateModalSubmitCallback = (data: MutatedPriceLevelGroupInterface) => { @@ -216,7 +216,7 @@ const PriceLevelGroup = (props: Props) => { }, }); - props.revalidateData(); + revalidate("priceLvlGroups"); }; const mutateDataToSelect = (data: PriceLevelInterface[]) => { diff --git a/src/components/Settings/components/UserGroup/EmailHandles.tsx b/src/modules/SettingsOld/components/UserGroup/EmailHandles.tsx similarity index 100% rename from src/components/Settings/components/UserGroup/EmailHandles.tsx rename to src/modules/SettingsOld/components/UserGroup/EmailHandles.tsx diff --git a/src/components/Settings/components/UserGroup/UserGroup.tsx b/src/modules/SettingsOld/components/UserGroup/UserGroup.tsx similarity index 97% rename from src/components/Settings/components/UserGroup/UserGroup.tsx rename to src/modules/SettingsOld/components/UserGroup/UserGroup.tsx index 65aee4b..b25c71b 100644 --- a/src/components/Settings/components/UserGroup/UserGroup.tsx +++ b/src/modules/SettingsOld/components/UserGroup/UserGroup.tsx @@ -17,13 +17,13 @@ import { CreateUserGroupInterface, EmailHandleInterface, UserGroupInterface, -} from "@/components/Settings/types"; +} from "@/modules/Settings/types"; import EmailHandles from "./EmailHandles"; import { UserGroupContextProvider } from "./UserGroupContext"; import { tableSortLocale } from "@/constants/PickerLocale"; +import revalidate from "@/utils/revalidate"; interface Props { - revalidateData: () => void; data: UserGroupInterface[]; token: string; } @@ -121,7 +121,7 @@ const UserGroup = (props: Props) => { }); setEditPriceModalOpen(false); - props.revalidateData(); + revalidate("userGroups"); }; const createModalSubmitCallback = (data: CreateUserGroupInterface) => { @@ -136,7 +136,7 @@ const UserGroup = (props: Props) => { }, }); - props.revalidateData(); + revalidate("userGroups"); }; const deleteModalSubmitCallback = () => { @@ -155,7 +155,7 @@ const UserGroup = (props: Props) => { }, }); - props.revalidateData(); + revalidate("userGroups"); }; const updateModalSubmitCallback = (data: UserGroupInterface) => { @@ -189,7 +189,7 @@ const UserGroup = (props: Props) => { }, }); - props.revalidateData(); + revalidate("userGroups"); }; const memoizedContextValue = useMemo(() => { diff --git a/src/components/Settings/components/UserGroup/UserGroupContext.tsx b/src/modules/SettingsOld/components/UserGroup/UserGroupContext.tsx similarity index 96% rename from src/components/Settings/components/UserGroup/UserGroupContext.tsx rename to src/modules/SettingsOld/components/UserGroup/UserGroupContext.tsx index 8cc6bca..72fda76 100644 --- a/src/components/Settings/components/UserGroup/UserGroupContext.tsx +++ b/src/modules/SettingsOld/components/UserGroup/UserGroupContext.tsx @@ -14,7 +14,7 @@ interface UserGroupContextValue { const UserGroupContext = createContext({ emailHandles: [], - setEmailHandles: () => {}, + setEmailHandles: () => { }, }); export const useUserGroupContext = () => { diff --git a/src/components/Settings/components/index.ts b/src/modules/SettingsOld/components/index.ts similarity index 100% rename from src/components/Settings/components/index.ts rename to src/modules/SettingsOld/components/index.ts diff --git a/src/components/Settings/components/modals/ModifyModal/ModifyModal.tsx b/src/modules/SettingsOld/components/modals/ModifyModal/ModifyModal.tsx similarity index 100% rename from src/components/Settings/components/modals/ModifyModal/ModifyModal.tsx rename to src/modules/SettingsOld/components/modals/ModifyModal/ModifyModal.tsx diff --git a/src/components/Settings/types.ts b/src/modules/SettingsOld/types.ts similarity index 100% rename from src/components/Settings/types.ts rename to src/modules/SettingsOld/types.ts diff --git a/src/components/Settings/utils.ts b/src/modules/SettingsOld/utils.ts similarity index 100% rename from src/components/Settings/utils.ts rename to src/modules/SettingsOld/utils.ts diff --git a/src/components/Sidebar/Sidebar.tsx b/src/modules/Sidebar/Sidebar.tsx similarity index 100% rename from src/components/Sidebar/Sidebar.tsx rename to src/modules/Sidebar/Sidebar.tsx diff --git a/src/components/Statistics/StatisticsPage.tsx b/src/modules/Statistics/StatisticsPage.tsx similarity index 98% rename from src/components/Statistics/StatisticsPage.tsx rename to src/modules/Statistics/StatisticsPage.tsx index 6412f89..c82f924 100644 --- a/src/components/Statistics/StatisticsPage.tsx +++ b/src/modules/Statistics/StatisticsPage.tsx @@ -12,7 +12,7 @@ import type { ValueKeyInterface, } from "@/types/OrdersInterface"; import pickerLocale from "@/constants/PickerLocale"; -import { StatisticsTable } from "@/components/Statistics"; +import { StatisticsTable } from "@/modules/Statistics"; import getTime from "@/lib/getTime"; interface Props { diff --git a/src/components/Statistics/Table.tsx b/src/modules/Statistics/Table.tsx similarity index 100% rename from src/components/Statistics/Table.tsx rename to src/modules/Statistics/Table.tsx diff --git a/src/components/Statistics/index.ts b/src/modules/Statistics/index.ts similarity index 100% rename from src/components/Statistics/index.ts rename to src/modules/Statistics/index.ts diff --git a/src/modules/WaiterOrdersPage/WaiterOrdersPage.module.css b/src/modules/WaiterOrdersPage/WaiterOrdersPage.module.css new file mode 100644 index 0000000..87de2ea --- /dev/null +++ b/src/modules/WaiterOrdersPage/WaiterOrdersPage.module.css @@ -0,0 +1,5 @@ +.container { + display: flex; + flex-direction: column; + gap: 1rem; +} \ No newline at end of file diff --git a/src/modules/WaiterOrdersPage/WaiterOrdersPage.tsx b/src/modules/WaiterOrdersPage/WaiterOrdersPage.tsx new file mode 100644 index 0000000..8c2fe9a --- /dev/null +++ b/src/modules/WaiterOrdersPage/WaiterOrdersPage.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { useState, useRef } from "react"; +import { Typography } from "@/components/Typography"; +import { Table } from "./components/Table"; +import { Footer } from "./components/Footer"; +import dayjs from "@/lib/dayjs"; +import transformWaiterOrders from "./utils/transformWaiterOrders"; +import type { WaiterOrder } from "./types/waiterOrder"; +import getWaiterOrders from "./actions/getWaiterOrders"; + +import styles from "./WaiterOrdersPage.module.css"; + +interface Props { + waiterOrders: WaiterOrder[]; +} + +const WaiterOrdersPage = (props: Props) => { + const initialSSRData = transformWaiterOrders(props.waiterOrders); + + const [waiterOrders, setWaiterOrders] = + useState(initialSSRData); + + const ordersDateRef = useRef(dayjs().utc().startOf("day").unix()); + + const handleOrdersUpdate = async () => { + const newOrders = await getWaiterOrders(1); + setWaiterOrders(transformWaiterOrders(newOrders)); + }; + + return ( +
+ + Výdej + +
+
+ + ); +}; + +export default WaiterOrdersPage; diff --git a/src/modules/WaiterOrdersPage/actions/getWaiterOrders.ts b/src/modules/WaiterOrdersPage/actions/getWaiterOrders.ts new file mode 100644 index 0000000..7196d30 --- /dev/null +++ b/src/modules/WaiterOrdersPage/actions/getWaiterOrders.ts @@ -0,0 +1,33 @@ +"use server"; + +import dayjs from "@/lib/dayjs" +import getToken from "@/utils/getToken"; +import waiterOrders from "@/queries/waiterOrders"; +import { getClient } from "@/lib/ssrApolloClient"; + +const getWaiterOrders = async (unix?: number) => { + try { + const { data: getWaiterOrders } = await getClient().query({ + query: waiterOrders, + variables: { + date: unix ? unix : dayjs().utc().startOf("day").unix(), + }, + context: { + headers: { + Authorization: getToken(), + }, + fetchOptions: { + next: { tags: ["waiterOrders"] }, + }, + }, + fetchPolicy: "network-only", + }); + + return getWaiterOrders.waiterOrders ?? []; + } catch (error) { + return []; + } +}; + +export default getWaiterOrders; + diff --git a/src/modules/WaiterOrdersPage/components/Footer/Footer.module.css b/src/modules/WaiterOrdersPage/components/Footer/Footer.module.css new file mode 100644 index 0000000..b60b2ad --- /dev/null +++ b/src/modules/WaiterOrdersPage/components/Footer/Footer.module.css @@ -0,0 +1,20 @@ +.container { + width: 100%; + display: flex; + margin-top: 1rem; + justify-content: end; + gap: 1.25rem; +} + +.button { + display: flex; + justify-content: center; + align-items: center; + padding: 1rem 1.4375rem; + border-radius: 1rem; + gap: 0.5rem; + cursor: pointer; + border: 1px solid var(--red, #D51F2C); + background-color: var(--red, #D51F2C); + color: var(--white, #FFF); +} \ No newline at end of file diff --git a/src/modules/WaiterOrdersPage/components/Footer/Footer.tsx b/src/modules/WaiterOrdersPage/components/Footer/Footer.tsx new file mode 100644 index 0000000..6bbaeef --- /dev/null +++ b/src/modules/WaiterOrdersPage/components/Footer/Footer.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { MutableRefObject } from "react"; +import { Typography } from "@/components/Typography"; +import cx from "classnames"; + +import styles from "./Footer.module.css"; + +interface Props { + refreshOrders: () => void; + orderDateRef: MutableRefObject; +} + +const Footer = (props: Props) => { + return ( +
+ + Obnovit seznam + +
+ ); +}; + +export default Footer; diff --git a/src/modules/WaiterOrdersPage/components/Footer/index.tsx b/src/modules/WaiterOrdersPage/components/Footer/index.tsx new file mode 100644 index 0000000..090a49a --- /dev/null +++ b/src/modules/WaiterOrdersPage/components/Footer/index.tsx @@ -0,0 +1 @@ +export { default as Footer } from "./Footer"; diff --git a/src/modules/WaiterOrdersPage/components/Table/Table.module.css b/src/modules/WaiterOrdersPage/components/Table/Table.module.css new file mode 100644 index 0000000..55da7da --- /dev/null +++ b/src/modules/WaiterOrdersPage/components/Table/Table.module.css @@ -0,0 +1,13 @@ +.container { + width: 100%; + max-height: 45rem; + display: flex; + flex-direction: column; + border-radius: 1rem; + border: 1px solid var(--Beige, #EEE8DA); + overflow-y: scroll; +} + +.container::-webkit-scrollbar { + display: none; +} \ No newline at end of file diff --git a/src/modules/WaiterOrdersPage/components/Table/Table.tsx b/src/modules/WaiterOrdersPage/components/Table/Table.tsx new file mode 100644 index 0000000..27938ef --- /dev/null +++ b/src/modules/WaiterOrdersPage/components/Table/Table.tsx @@ -0,0 +1,19 @@ +import { Row } from "./components/Row"; +import { WaiterOrder } from "../../types/waiterOrder"; +import styles from "./Table.module.css"; + +interface Props { + data: WaiterOrder[]; +} + +const Table = (props: Props) => { + return ( +
+ {props.data.map((row: WaiterOrder, index: number) => ( + + ))} +
+ ); +}; + +export default Table; diff --git a/src/modules/WaiterOrdersPage/components/Table/components/Row/Row.module.css b/src/modules/WaiterOrdersPage/components/Table/components/Row/Row.module.css new file mode 100644 index 0000000..7eabb4d --- /dev/null +++ b/src/modules/WaiterOrdersPage/components/Table/components/Row/Row.module.css @@ -0,0 +1,10 @@ +.row { + display: flex; + height: 7.5rem; + padding: 2rem 2.5625rem 1.625rem 3rem; + gap: 6rem; + + &.beigeBackground { + background: #F5F2EB; + } +} diff --git a/src/modules/WaiterOrdersPage/components/Table/components/Row/Row.tsx b/src/modules/WaiterOrdersPage/components/Table/components/Row/Row.tsx new file mode 100644 index 0000000..2150bb2 --- /dev/null +++ b/src/modules/WaiterOrdersPage/components/Table/components/Row/Row.tsx @@ -0,0 +1,23 @@ +import { WaiterOrder } from "@/modules/WaiterOrdersPage/types/waiterOrder"; +import dayjs from "@/lib/dayjs"; +import cx from "classnames"; + +import styles from "./Row.module.css"; + +interface Props { + rowData: WaiterOrder; + oddRow: boolean; +} +const Row = (props: Props) => { + return ( +
+
+

{props.rowData.name}

+

{dayjs.unix(props.rowData.date).utc().format("HH:MM")}

+
+

{props.rowData.meal}

+
+ ); +}; + +export default Row; diff --git a/src/modules/WaiterOrdersPage/components/Table/components/Row/index.tsx b/src/modules/WaiterOrdersPage/components/Table/components/Row/index.tsx new file mode 100644 index 0000000..9abd55d --- /dev/null +++ b/src/modules/WaiterOrdersPage/components/Table/components/Row/index.tsx @@ -0,0 +1 @@ +export { default as Row } from "./Row"; diff --git a/src/modules/WaiterOrdersPage/components/Table/index.tsx b/src/modules/WaiterOrdersPage/components/Table/index.tsx new file mode 100644 index 0000000..6c9ff96 --- /dev/null +++ b/src/modules/WaiterOrdersPage/components/Table/index.tsx @@ -0,0 +1 @@ +export { default as Table } from "./Table"; diff --git a/src/modules/WaiterOrdersPage/types/waiterOrder.ts b/src/modules/WaiterOrdersPage/types/waiterOrder.ts new file mode 100644 index 0000000..cef09bb --- /dev/null +++ b/src/modules/WaiterOrdersPage/types/waiterOrder.ts @@ -0,0 +1,7 @@ +export interface WaiterOrder { + orderId: string; + date: number; + meal: string; + name: string; + order: number; +} diff --git a/src/modules/WaiterOrdersPage/utils/transformWaiterOrders.ts b/src/modules/WaiterOrdersPage/utils/transformWaiterOrders.ts new file mode 100644 index 0000000..357d837 --- /dev/null +++ b/src/modules/WaiterOrdersPage/utils/transformWaiterOrders.ts @@ -0,0 +1,14 @@ +import { WaiterOrder } from "../types/waiterOrder"; + +const transformWaiterOrders = (waiterOrders: WaiterOrder[]) => + waiterOrders.map((order) => { + return { + name: order.name, + date: order.date, + orderId: order.orderId, + meal: order.meal, + order: order.order, + }; + }); + +export default transformWaiterOrders; diff --git a/src/modules/login/components/Footer/Footer.module.css b/src/modules/login/components/Footer/Footer.module.css new file mode 100644 index 0000000..5c1b191 --- /dev/null +++ b/src/modules/login/components/Footer/Footer.module.css @@ -0,0 +1,20 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + width: 100%; +} + +.divider { + width: 16.5rem; + height: 0.0625rem; + background: var(--Divider, #CACACA); +} + +.menurooLink { + color: var(--red, #D51F2C); + text-decoration-line: underline; + cursor: pointer; + margin-top: 0.3rem; +} diff --git a/src/modules/login/components/Footer/Footer.tsx b/src/modules/login/components/Footer/Footer.tsx new file mode 100644 index 0000000..8bd0658 --- /dev/null +++ b/src/modules/login/components/Footer/Footer.tsx @@ -0,0 +1,23 @@ +import Link from "next/link"; +import { Typography } from "@/components/Typography"; +import styles from "./Footer.module.css"; + +const Footer = () => { + return ( +
+
+
+ + Nevlastníte restauraci? + + + + Vraťte se do Menuroo + + +
+
+ ); +}; + +export default Footer; diff --git a/src/modules/login/components/Footer/index.tsx b/src/modules/login/components/Footer/index.tsx new file mode 100644 index 0000000..090a49a --- /dev/null +++ b/src/modules/login/components/Footer/index.tsx @@ -0,0 +1 @@ +export { default as Footer } from "./Footer"; diff --git a/src/modules/login/components/Form/Form.module.css b/src/modules/login/components/Form/Form.module.css new file mode 100644 index 0000000..0e9b2e2 --- /dev/null +++ b/src/modules/login/components/Form/Form.module.css @@ -0,0 +1,35 @@ +.container { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.inputsContainer { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.loginButton { + color: white; + padding: 1rem 0.5rem; + width: 100%; + border-radius: 1rem; + background: var(--red, #D51F2C); + transition: background 0.15s ease-in-out; + cursor: pointer; + + &.buttonDisabled { + background: var(--DISABLED, #5A5A5A); + cursor: not-allowed; + } +} + +.input { + background-color: var(--beige-web, #F9F7F2); + transition: border 0.15s ease-in-out; + + &.inputError { + border: 1px solid var(--red, #D51F2C); + } +} \ No newline at end of file diff --git a/src/modules/login/components/Form/Form.tsx b/src/modules/login/components/Form/Form.tsx new file mode 100644 index 0000000..fd8ba05 --- /dev/null +++ b/src/modules/login/components/Form/Form.tsx @@ -0,0 +1,104 @@ +import { useState, useEffect } from "react"; +import { Input } from "@/components/Input"; +import { Typography } from "@/components/Typography"; +import { useLoginContext } from "../../context/LoginContext"; +import cx from "classnames"; + +import styles from "./Form.module.css"; + +const Form = () => { + const { authenticate, emailRef, passwordRef } = useLoginContext(); + + const [emailValid, setEmailValid] = useState(false); + const [passwordValid, setPasswordValid] = useState(false); + const [buttonDisabled, setButtonDisabled] = useState(true); + + const [showEmailError, setShowEmailError] = useState(false); + const [showPasswordError, setShowPasswordError] = useState(false); + + const verifyEmailInput = () => { + if (emailRef?.current?.value.length === 0) { + setEmailValid(false); + setShowEmailError(false); + return; + } + + const emailRegex = /\S+@\S+\.\S+/; + + if (emailRef?.current && emailRegex.test(emailRef?.current?.value)) { + setEmailValid(true); + setShowEmailError(false); + } else { + setEmailValid(false); + setShowEmailError(true); + } + }; + + const verifyPasswordInput = () => { + if (passwordRef?.current?.value.length === 0) { + setPasswordValid(false); + setShowPasswordError(false); + return; + } + + if (passwordRef?.current && passwordRef?.current?.value.length > 0) { + setPasswordValid(true); + setShowPasswordError(false); + } else { + setPasswordValid(false); + setShowPasswordError(true); + } + }; + + useEffect(() => { + verifyEmailInput(); + verifyPasswordInput(); + }, []); + + useEffect(() => { + if (emailValid && passwordValid) { + setButtonDisabled(false); + } else { + setButtonDisabled(true); + } + }, [emailValid, passwordValid]); + + return ( +
+
+ + Email + + + Heslo + +
+ + Přihlásit se + +
+ ); +}; + +export default Form; diff --git a/src/modules/login/components/Form/index.tsx b/src/modules/login/components/Form/index.tsx new file mode 100644 index 0000000..e4500b6 --- /dev/null +++ b/src/modules/login/components/Form/index.tsx @@ -0,0 +1 @@ +export { default as Form } from "./Form"; diff --git a/src/modules/login/context/LoginContext.tsx b/src/modules/login/context/LoginContext.tsx new file mode 100644 index 0000000..daec266 --- /dev/null +++ b/src/modules/login/context/LoginContext.tsx @@ -0,0 +1,40 @@ +import { createContext, useContext, ReactNode, MutableRefObject } from "react"; + +interface LoginContextValue { + authenticate: () => void; + emailRef: MutableRefObject; + passwordRef: MutableRefObject; +} + +const LoginContext = createContext({ + authenticate: () => {}, + emailRef: { current: null }, + passwordRef: { current: null }, +}); + +export const useLoginContext = () => { + return useContext(LoginContext); +}; + +interface LoginContextProviderProps { + value: LoginContextValue; + children: ReactNode; +} + +export const LoginContextProvider = (props: LoginContextProviderProps) => { + const { + value: { authenticate, emailRef, passwordRef }, + } = props; + + return ( + + {props.children} + + ); +}; diff --git a/src/modules/login/screens/LoginPage.module.css b/src/modules/login/screens/LoginPage.module.css new file mode 100644 index 0000000..ab9f3e4 --- /dev/null +++ b/src/modules/login/screens/LoginPage.module.css @@ -0,0 +1,16 @@ +.container { + min-height: 100vh; + widht: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.innerContainer { + display: flex; + flex-direction: column; + width: 20.5rem; + gap: 2rem; +} + diff --git a/src/modules/login/screens/LoginPage.tsx b/src/modules/login/screens/LoginPage.tsx new file mode 100644 index 0000000..c17f452 --- /dev/null +++ b/src/modules/login/screens/LoginPage.tsx @@ -0,0 +1,68 @@ +"use client"; + +import React, { useEffect, useMemo, useRef } from "react"; +import { useRouter } from "next/navigation"; +import { useLazyQuery } from "@apollo/client"; +import login from "@/queries/login"; +import { LogoHeader } from "@/components/LogoHeader"; +import { Form } from "../components/Form"; +import { Footer } from "../components/Footer"; +import { LoginContextProvider } from "../context/LoginContext"; + +import styles from "./LoginPage.module.css"; + +interface Props { + setToken: (token: string) => void; +} + +const LoginPage = (props: Props) => { + const emailRef = useRef(null); + const passwordRef = useRef(null); + + const router = useRouter(); + + const [loginQuery, { data, error }] = useLazyQuery(login, { + fetchPolicy: "network-only", + }); + + const authenticate = () => { + if (emailRef.current?.value && passwordRef.current?.value) { + loginQuery({ + variables: { + email: emailRef.current.value, + password: passwordRef.current.value, + }, + }); + } + }; + + useEffect(() => { + if (!data?.login) return; + + props.setToken(data.login); + router.push("/jidelni-listek"); + }, [data]); + + const memoizedValue = useMemo( + () => ({ + authenticate, + emailRef, + passwordRef, + }), + [authenticate, emailRef, passwordRef] + ); + + return ( + +
+
+ +
+
+
+
+
+ ); +}; + +export default LoginPage; diff --git a/src/modules/login/screens/index.tsx b/src/modules/login/screens/index.tsx new file mode 100644 index 0000000..62cd443 --- /dev/null +++ b/src/modules/login/screens/index.tsx @@ -0,0 +1 @@ +export { default as LoginPage } from "./LoginPage"; diff --git a/src/modules/menu/screens/Menu.module.css b/src/modules/menu/screens/Menu.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/menu/screens/Menu.tsx b/src/modules/menu/screens/Menu.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/menu/screens/index.tsx b/src/modules/menu/screens/index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/theme/themeConfig.ts b/src/theme/themeConfig.ts index aa34f3a..e0cf7c0 100644 --- a/src/theme/themeConfig.ts +++ b/src/theme/themeConfig.ts @@ -4,6 +4,7 @@ const theme: ThemeConfig = { token: { colorPrimary: "#37D399", colorText: "#1F2937", + fontFamily: "Nunito", }, }; diff --git a/src/types/LunchOrder.ts b/src/types/LunchOrder.ts deleted file mode 100644 index 357faca..0000000 --- a/src/types/LunchOrder.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface LunchOrder { - key: string; - orderId: number; - date: number; - name: string; - meal: string; - order: number; -} - -export interface NormalizedLunchOrder { - key: string; - orderId: number; - date: string; - name: string; - meal: string; - order: number; -} diff --git a/src/types/MenuInterface.ts b/src/types/MenuInterface.ts index 4959a65..95d178c 100644 --- a/src/types/MenuInterface.ts +++ b/src/types/MenuInterface.ts @@ -6,5 +6,10 @@ export interface MenuInterface { meal: string; soup: string; drink: string; - date?: number; + date: number; } + +export type DayMenu = { + date: number; + menus: MenuInterface[]; +}; diff --git a/src/utils/getMonthName.ts b/src/utils/getMonthName.ts new file mode 100644 index 0000000..833f93c --- /dev/null +++ b/src/utils/getMonthName.ts @@ -0,0 +1,22 @@ +// MM-DD-YYYY +const getMonthName = (date: string) => { + const month = new Date(date).getMonth(); + return isNaN(month) + ? null + : [ + "Leden", + "Únor", + "Březen", + "Duben", + "Květen", + "Červen", + "Červenec", + "Srpen", + "Září", + "Říjen", + "Listopad", + "Prosinec", + ][month]; +}; + +export default getMonthName; diff --git a/src/utils/getWeekDay.ts b/src/utils/getWeekDay.ts index 78a1e5b..53ce5e3 100644 --- a/src/utils/getWeekDay.ts +++ b/src/utils/getWeekDay.ts @@ -1,3 +1,4 @@ +// MM-DD-YYYY const getDayOfWeek = (date: string) => { const dayOfWeek = new Date(date).getDay(); return isNaN(dayOfWeek) diff --git a/src/utils/isTokenInLocalStorage.ts b/src/utils/isTokenInLocalStorage.ts deleted file mode 100644 index ef65b9f..0000000 --- a/src/utils/isTokenInLocalStorage.ts +++ /dev/null @@ -1,9 +0,0 @@ -const isTokenInLocalStorage = () => { - if (typeof window === "undefined") { - return false; - } - - return localStorage.getItem("token"); -}; - -export default isTokenInLocalStorage; diff --git a/src/utils/revalidate.ts b/src/utils/revalidate.ts new file mode 100644 index 0000000..3c2ceba --- /dev/null +++ b/src/utils/revalidate.ts @@ -0,0 +1,9 @@ +"use server"; + +import { revalidateTag } from "next/cache"; + +const revalidate = (tag: string) => { + revalidateTag(tag); +}; + +export default revalidate; diff --git a/tailwind.config.ts b/tailwind.config.ts deleted file mode 100644 index 2716bcb..0000000 --- a/tailwind.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { Config } from "tailwindcss"; - -const config: Config = { - preflight: false, - important: true, - content: [ - "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", - "./src/components/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", - ], - theme: { - extend: { - screens: { - xxl: "1440px", - }, - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", - }, - colors: { - primary: "#37d399", - }, - }, - }, - plugins: [], -}; -export default config; diff --git a/tsconfig.json b/tsconfig.json index 1a65918..aae095a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,12 @@ "compilerOptions": { "target": "es5", "downlevelIteration": true, - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "es2015", + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -21,9 +26,18 @@ ], "baseUrl": ".", "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] }