diff --git a/apps/admin/.eslintignore b/apps/admin/.eslintignore new file mode 100644 index 00000000..590ba875 --- /dev/null +++ b/apps/admin/.eslintignore @@ -0,0 +1,3 @@ +*.cjs +*.mjs +*.js \ No newline at end of file diff --git a/apps/admin/.eslintrc.js b/apps/admin/.eslintrc.js new file mode 100644 index 00000000..0c6ec010 --- /dev/null +++ b/apps/admin/.eslintrc.js @@ -0,0 +1,8 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + extends: ["@kduprey/eslint-config/next.js"], + parser: "@typescript-eslint/parser", + parserOptions: { + project: true, + }, +}; diff --git a/apps/admin/.stylelintignore b/apps/admin/.stylelintignore new file mode 100644 index 00000000..2b3533c7 --- /dev/null +++ b/apps/admin/.stylelintignore @@ -0,0 +1,2 @@ +.next +out diff --git a/apps/admin/.stylelintrc.json b/apps/admin/.stylelintrc.json new file mode 100644 index 00000000..4ea6506d --- /dev/null +++ b/apps/admin/.stylelintrc.json @@ -0,0 +1,28 @@ +{ + "extends": ["stylelint-config-standard-scss"], + "rules": { + "custom-property-pattern": null, + "selector-class-pattern": null, + "scss/no-duplicate-mixins": null, + "declaration-empty-line-before": null, + "declaration-block-no-redundant-longhand-properties": null, + "alpha-value-notation": null, + "custom-property-empty-line-before": null, + "property-no-vendor-prefix": null, + "color-function-notation": null, + "length-zero-no-unit": null, + "selector-not-notation": null, + "no-descending-specificity": null, + "comment-empty-line-before": null, + "scss/at-mixin-pattern": null, + "scss/at-rule-no-unknown": null, + "value-keyword-case": null, + "media-feature-range-notation": null, + "selector-pseudo-class-no-unknown": [ + true, + { + "ignorePseudoClasses": ["global"] + } + ] + } +} diff --git a/apps/admin/LICENCE b/apps/admin/LICENCE new file mode 100644 index 00000000..1a88111a --- /dev/null +++ b/apps/admin/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Vitaly Rtischev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/admin/README.md b/apps/admin/README.md new file mode 100644 index 00000000..f25d60af --- /dev/null +++ b/apps/admin/README.md @@ -0,0 +1,37 @@ +# Mantine Next.js template + +This is a template for [Next.js](https://nextjs.org/) app router + [Mantine](https://mantine.dev/). +If you want to use pages router instead, see [next-pages-template](https://github.com/mantinedev/next-pages-template). + +## Features + +This template comes with the following features: + +- [PostCSS](https://postcss.org/) with [mantine-postcss-preset](https://mantine.dev/styles/postcss-preset) +- [TypeScript](https://www.typescriptlang.org/) +- [Storybook](https://storybook.js.org/) +- [Jest](https://jestjs.io/) setup with [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) +- ESLint setup with [eslint-config-mantine](https://github.com/mantinedev/eslint-config-mantine) + +## npm scripts + +### Build and dev scripts + +- `dev` – start dev server +- `build` – bundle application for production +- `analyze` – analyzes application bundle with [@next/bundle-analyzer](https://www.npmjs.com/package/@next/bundle-analyzer) + +### Testing scripts + +- `typecheck` – checks TypeScript types +- `lint` – runs ESLint +- `prettier:check` – checks files with Prettier +- `jest` – runs jest tests +- `jest:watch` – starts jest watch +- `test` – runs `jest`, `prettier:check`, `lint` and `typecheck` scripts + +### Other scripts + +- `storybook` – starts storybook dev server +- `storybook:build` – build production storybook bundle to `storybook-static` +- `prettier:write` – formats all files with Prettier diff --git a/apps/admin/barrelsby.config.json b/apps/admin/barrelsby.config.json new file mode 100644 index 00000000..90992888 --- /dev/null +++ b/apps/admin/barrelsby.config.json @@ -0,0 +1,6 @@ +{ + "baseurl": "./src", + "delete": true, + "directory": ["./src/components"], + "noHeader": true +} diff --git a/apps/admin/jest.config.cjs b/apps/admin/jest.config.cjs new file mode 100644 index 00000000..a6a9e317 --- /dev/null +++ b/apps/admin/jest.config.cjs @@ -0,0 +1,17 @@ +const nextJest = require("next/jest"); + +const createJestConfig = nextJest({ + dir: "./", +}); + +const customJestConfig = { + setupFilesAfterEnv: ["/jest.setup.cjs"], + moduleNameMapper: { + "^@/components/(.*)$": "/components/$1", + "^@/pages/(.*)$": "/pages/$1", + }, + testEnvironment: "jest-environment-jsdom", + moduledirectories: ["node_modules", __dirname, "test-utils"], +}; + +module.exports = createJestConfig(customJestConfig); diff --git a/apps/admin/jest.setup.cjs b/apps/admin/jest.setup.cjs new file mode 100644 index 00000000..2f5c44da --- /dev/null +++ b/apps/admin/jest.setup.cjs @@ -0,0 +1,26 @@ +require('@testing-library/jest-dom'); + +const { getComputedStyle } = window; +window.getComputedStyle = (elt) => getComputedStyle(elt); + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); + +class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +} + +window.ResizeObserver = ResizeObserver; diff --git a/apps/admin/next-env.d.ts b/apps/admin/next-env.d.ts new file mode 100644 index 00000000..4f11a03d --- /dev/null +++ b/apps/admin/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/admin/next.config.mjs b/apps/admin/next.config.mjs new file mode 100644 index 00000000..eaccc00f --- /dev/null +++ b/apps/admin/next.config.mjs @@ -0,0 +1,26 @@ +import bundleAnalyzer from "@next/bundle-analyzer"; +import { PrismaPlugin } from "@prisma/nextjs-monorepo-workaround-plugin"; + +const withBundleAnalyzer = bundleAnalyzer({ + enabled: process.env.ANALYZE === "true", +}); + +export default withBundleAnalyzer({ + webpack: (config, { isServer }) => { + if (isServer) { + config.plugins.push(new PrismaPlugin()); + } else config.resolve.fallback.fs = false; + return config; + }, + reactStrictMode: false, + eslint: { + ignoreDuringBuilds: true, + }, + experimental: { + optimizePackageImports: [ + "@mantine/core", + "@mantine/hooks", + "@kduprey/ui", + ], + }, +}); diff --git a/apps/admin/package.json b/apps/admin/package.json new file mode 100644 index 00000000..b235188b --- /dev/null +++ b/apps/admin/package.json @@ -0,0 +1,75 @@ +{ + "name": "@kduprey/admin", + "version": "1.0.0", + "private": true, + "scripts": { + "analyze": "ANALYZE=true next build", + "build": "next build", + "dev": "next dev -p 3401", + "generate-barrels": "barrelsby -c barrelsby.config.json", + "jest": "jest", + "jest:watch": "jest --watch", + "lint": "next lint && npm run lint:stylelint", + "lint:stylelint": "stylelint '**/*.css' --cache", + "prettier:check": "prettier --check \"**/*.{ts,tsx}\"", + "prettier:write": "prettier --write \"**/*.{ts,tsx}\"", + "start": "next start", + "storybook": "storybook dev -p 6006", + "storybook:build": "storybook build", + "test": "npm run prettier:check && npm run lint && npm run typecheck && npm run jest", + "typecheck": "tsc --noEmit" + }, + "prettier": "@kduprey/eslint-config/prettier", + "dependencies": { + "@clerk/nextjs": "^5.2.1", + "@clerk/themes": "^2.1.10", + "@kduprey/config": "workspace:*", + "@kduprey/db": "workspace:*", + "@kduprey/ui": "workspace:*", + "@kduprey/utils": "workspace:*", + "@mantine/core": "7.11.1", + "@mantine/form": "^7.11.1", + "@mantine/hooks": "7.11.1", + "@mantine/notifications": "^7.11.1", + "@next/bundle-analyzer": "^14.2.4", + "@tabler/icons": "^3.8.0", + "@tabler/icons-react": "^3.8.0", + "clsx": "^2.1.1", + "mantine-datatable": "^7.11.1", + "next": "14.2.4", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@babel/core": "^7.24.7", + "@kduprey/eslint-config": "workspace:*", + "@kduprey/tsconfig": "workspace:*", + "@prisma/nextjs-monorepo-workaround-plugin": "^5.16.1", + "@storybook/addon-essentials": "^8.1.11", + "@storybook/addon-styling-webpack": "^1.0.0", + "@storybook/blocks": "^8.1.11", + "@storybook/nextjs": "^8.1.11", + "@storybook/preview-api": "^8.1.11", + "@storybook/react": "^8.1.11", + "@testing-library/dom": "^10.3.0", + "@testing-library/jest-dom": "^6.4.6", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", + "@types/jest": "^29.5.12", + "@types/node": "^20.14.9", + "@types/react": "18.3.3", + "@types/react-dom": "^18.3.0", + "babel-loader": "^9.1.3", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "postcss": "^8.4.39", + "postcss-preset-mantine": "1.15.0", + "postcss-simple-vars": "^7.0.1", + "storybook": "^8.1.11", + "storybook-dark-mode": "^4.0.2", + "stylelint": "^16.6.1", + "stylelint-config-standard-scss": "^13.1.0", + "ts-jest": "^29.1.5", + "typescript": "5.5.3" + } +} diff --git a/apps/admin/postcss.config.cjs b/apps/admin/postcss.config.cjs new file mode 100644 index 00000000..bfba0ddf --- /dev/null +++ b/apps/admin/postcss.config.cjs @@ -0,0 +1,14 @@ +module.exports = { + plugins: { + 'postcss-preset-mantine': {}, + 'postcss-simple-vars': { + variables: { + 'mantine-breakpoint-xs': '36em', + 'mantine-breakpoint-sm': '48em', + 'mantine-breakpoint-md': '62em', + 'mantine-breakpoint-lg': '75em', + 'mantine-breakpoint-xl': '88em', + }, + }, + }, +}; diff --git a/apps/admin/src/app/apple-icon.png b/apps/admin/src/app/apple-icon.png new file mode 100644 index 00000000..fa81c285 Binary files /dev/null and b/apps/admin/src/app/apple-icon.png differ diff --git a/apps/admin/src/app/dashboard/layout.tsx b/apps/admin/src/app/dashboard/layout.tsx new file mode 100644 index 00000000..cb602caf --- /dev/null +++ b/apps/admin/src/app/dashboard/layout.tsx @@ -0,0 +1,8 @@ +import { type PropsWithChildren } from "react"; +import { DashboardLayout } from "@/components"; + +const Layout = ({ children }: PropsWithChildren) => ( + {children} +); + +export default Layout; diff --git a/apps/admin/src/app/dashboard/page.tsx b/apps/admin/src/app/dashboard/page.tsx new file mode 100644 index 00000000..3203d018 --- /dev/null +++ b/apps/admin/src/app/dashboard/page.tsx @@ -0,0 +1,7 @@ +import { Title } from "@mantine/core"; + +const Page = () => { + return Dashboard; +}; + +export default Page; diff --git a/apps/admin/src/app/favicon-16x16.png b/apps/admin/src/app/favicon-16x16.png new file mode 100644 index 00000000..02d9c912 Binary files /dev/null and b/apps/admin/src/app/favicon-16x16.png differ diff --git a/apps/admin/src/app/favicon-32x32.png b/apps/admin/src/app/favicon-32x32.png new file mode 100644 index 00000000..8b862f04 Binary files /dev/null and b/apps/admin/src/app/favicon-32x32.png differ diff --git a/apps/admin/src/app/favicon.ico b/apps/admin/src/app/favicon.ico new file mode 100644 index 00000000..f1784920 Binary files /dev/null and b/apps/admin/src/app/favicon.ico differ diff --git a/apps/admin/src/app/global.css b/apps/admin/src/app/global.css new file mode 100644 index 00000000..fbbce3f1 --- /dev/null +++ b/apps/admin/src/app/global.css @@ -0,0 +1,3 @@ +@import "https://use.typekit.net/vik6drq.css"; +/* 👇 Make sure the styles are applied in the correct order */ +@layer mantine, mantine-datatable; diff --git a/apps/admin/src/app/layout.tsx b/apps/admin/src/app/layout.tsx new file mode 100644 index 00000000..868858d3 --- /dev/null +++ b/apps/admin/src/app/layout.tsx @@ -0,0 +1,46 @@ +import React, { type PropsWithChildren } from "react"; +import { ColorSchemeScript, MantineProvider } from "@mantine/core"; +import { ClerkProvider } from "@clerk/nextjs"; +import { dark } from "@clerk/themes"; +import "@mantine/core/styles.layer.css"; +import "mantine-datatable/styles.layer.css"; +import "./global.css"; +import { resolver, theme } from "../theme"; + +export const metadata = { + description: "Haus of Web - Admin", + keywords: "Haus of Web, Kenton Duprey, developer, software engineer, NYC", + title: "Haus of Web - Admin", +}; + +const RootLayout = ({ children }: Readonly) => { + return ( + + + + + + + + + + {children} + + + + + ); +}; + +export default RootLayout; diff --git a/apps/admin/src/app/page.tsx b/apps/admin/src/app/page.tsx new file mode 100644 index 00000000..52c0ca6d --- /dev/null +++ b/apps/admin/src/app/page.tsx @@ -0,0 +1,12 @@ +import { auth } from "@clerk/nextjs/server"; +import { redirect } from "next/navigation"; + +const HomePage = () => { + const { userId } = auth(); + + if (userId) redirect("/dashboard"); + + redirect("/sign-in"); +}; + +export default HomePage; diff --git a/apps/admin/src/app/sign-in/[[...sign-in]]/page.tsx b/apps/admin/src/app/sign-in/[[...sign-in]]/page.tsx new file mode 100644 index 00000000..86e82bf6 --- /dev/null +++ b/apps/admin/src/app/sign-in/[[...sign-in]]/page.tsx @@ -0,0 +1,14 @@ +import { SignIn } from "@clerk/nextjs"; +import { Center, Paper } from "@mantine/core"; + +const Page = () => { + return ( + +
+ +
+
+ ); +}; + +export default Page; diff --git a/apps/admin/src/components/ColorSchemeToggle/ColorSchemeToggle.tsx b/apps/admin/src/components/ColorSchemeToggle/ColorSchemeToggle.tsx new file mode 100644 index 00000000..682a1737 --- /dev/null +++ b/apps/admin/src/components/ColorSchemeToggle/ColorSchemeToggle.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { Button, Group, useMantineColorScheme } from "@mantine/core"; + +export const ColorSchemeToggle = () => { + const { setColorScheme } = useMantineColorScheme(); + + return ( + + + + + + ); +}; diff --git a/apps/admin/src/components/DashboardLayout/DashboardLayout.tsx b/apps/admin/src/components/DashboardLayout/DashboardLayout.tsx new file mode 100644 index 00000000..e7b5b03d --- /dev/null +++ b/apps/admin/src/components/DashboardLayout/DashboardLayout.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { + Anchor, + AppShell, + AppShellHeader, + AppShellMain, + AppShellNavbar, + AppShellSection, + Burger, + Group, + NavLink, + Title, +} from "@mantine/core"; +import { useDisclosure } from "@mantine/hooks"; +import { type PropsWithChildren } from "react"; +import { SignedIn, UserButton } from "@clerk/nextjs"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { links } from "@/src/data"; + +export const DashboardLayout = ({ children }: PropsWithChildren) => { + const [opened, { toggle }] = useDisclosure(); + const path = usePathname(); + return ( + + + + + + + + Haus of Web, LLC. + + + + + + + {links.map((link) => ( + + ))} + + + + + + + + {children} + + ); +}; diff --git a/apps/admin/src/components/index.ts b/apps/admin/src/components/index.ts new file mode 100644 index 00000000..b21193ce --- /dev/null +++ b/apps/admin/src/components/index.ts @@ -0,0 +1,2 @@ +export * from "./ColorSchemeToggle/ColorSchemeToggle"; +export * from "./DashboardLayout/DashboardLayout"; diff --git a/apps/admin/src/data/index.ts b/apps/admin/src/data/index.ts new file mode 100644 index 00000000..130ebac0 --- /dev/null +++ b/apps/admin/src/data/index.ts @@ -0,0 +1 @@ +export * from "./links"; diff --git a/apps/admin/src/data/links.ts b/apps/admin/src/data/links.ts new file mode 100644 index 00000000..02d92d2e --- /dev/null +++ b/apps/admin/src/data/links.ts @@ -0,0 +1,6 @@ +export const links = [ + { + href: "/dashboard", + label: "Dashboard", + }, +]; diff --git a/apps/admin/src/middleware.ts b/apps/admin/src/middleware.ts new file mode 100644 index 00000000..a6a10bd7 --- /dev/null +++ b/apps/admin/src/middleware.ts @@ -0,0 +1,11 @@ +import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; + +const isProtectedRoute = createRouteMatcher(["/dashboard(.*)"]); + +export default clerkMiddleware((auth, req) => { + if (isProtectedRoute(req)) auth().protect(); +}); + +export const config = { + matcher: ["/((?!.+.[w]+$|_next).*)", "/", "/(api|trpc)(.*)"], +}; diff --git a/apps/admin/src/theme.ts b/apps/admin/src/theme.ts new file mode 100644 index 00000000..52419184 --- /dev/null +++ b/apps/admin/src/theme.ts @@ -0,0 +1,71 @@ +"use client"; + +import { + type CSSVariablesResolver, + Card, + createTheme, + type MantineThemeOverride, + type VariantColorsResolver, + defaultVariantColorsResolver, + rem, + DEFAULT_THEME, + mergeMantineTheme, + rgba, +} from "@mantine/core"; +import { Raleway } from "next/font/google"; + +const raleway = Raleway({ + display: "swap", + subsets: ["latin"], +}); + +const variantColorResolver: VariantColorsResolver = (input) => { + const defaultResolvedColors = defaultVariantColorsResolver(input); + + if (input.variant === "danger") { + return { + ...defaultResolvedColors, + background: "none", + hover: rgba(theme.colors.red[6], 0.2), + color: "var(--mantine-color-red-6)", + border: `${rem(1)} solid var(--mantine-color-red-6)`, + }; + } + + return defaultResolvedColors; +}; + +const themeOverride: MantineThemeOverride = createTheme({ + black: "#000000", + components: { + Card: Card.extend({ + styles: { + root: { + border: "1px solid var(--mantine-color-default-border)", + }, + }, + }), + }, + fontFamily: `${raleway.style.fontFamily}, sans-serif`, + headings: { + fontFamily: `${raleway.style.fontFamily}, sans-serif`, + }, + primaryColor: "violet", + primaryShade: { dark: 4, light: 4 }, + variantColorResolver, +}); + +export const resolver: CSSVariablesResolver = () => ({ + variables: { + "--mantine-color-default-border": "#fff", + }, + light: { + "--mantine-color-default-border": "#fff", + }, + dark: { + "--mantine-color-default-border": "#fff", + "--mantine-color-text": "#fff", + }, +}); + +export const theme = mergeMantineTheme(DEFAULT_THEME, themeOverride); diff --git a/apps/admin/test-utils/index.ts b/apps/admin/test-utils/index.ts new file mode 100644 index 00000000..5736b547 --- /dev/null +++ b/apps/admin/test-utils/index.ts @@ -0,0 +1,5 @@ +import userEvent from "@testing-library/user-event"; + +export { render } from "./render"; +export * from "@testing-library/react"; +export { userEvent }; diff --git a/apps/admin/test-utils/render.tsx b/apps/admin/test-utils/render.tsx new file mode 100644 index 00000000..577c46cf --- /dev/null +++ b/apps/admin/test-utils/render.tsx @@ -0,0 +1,17 @@ +import { render, type RenderOptions } from "@testing-library/react"; +import { MantineProvider } from "@mantine/core"; +import type { ReactElement } from "react"; +import { theme } from "../src/theme"; + +const AllTheProviders = ({ children }: { children: React.ReactNode }) => { + return {children}; +}; + +export const customRender = ( + ui: ReactElement, + options?: Omit, +): ReturnType => + render(ui, { wrapper: AllTheProviders, ...options }); + +export * from "@testing-library/react"; +export { customRender as render }; diff --git a/apps/admin/tsconfig.json b/apps/admin/tsconfig.json new file mode 100644 index 00000000..f5376d6a --- /dev/null +++ b/apps/admin/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "@kduprey/tsconfig/nextjs.json", + "compilerOptions": { + "types": [ + "node", + "jest", + "@testing-library/jest-dom", + "@testing-library/react" + ], + "baseUrl": ".", + "paths": { + "@/*": ["./*", "./src/*"], + "test-utils": ["./src/test-utils"] + }, + "plugins": [{ "name": "next" }] + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "jest.setup.cjs" + ], + "exclude": ["node_modules"] +}