diff --git a/.vscode/settings.json b/.vscode/settings.json index de28c3e..ac801b1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,9 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, + "editor.quickSuggestions": { + "strings": "on" + }, "eslint.validate": [ "javascript" ], diff --git a/next.config.js b/next.config.js index a843cbe..aba5ef1 100644 --- a/next.config.js +++ b/next.config.js @@ -1,6 +1,11 @@ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, + redirects: { + permanent: false, + destination: '/ps7', + source: '/' + } } module.exports = nextConfig diff --git a/package.json b/package.json index 332cc69..f603252 100644 --- a/package.json +++ b/package.json @@ -9,16 +9,19 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@types/content-type": "^1.1.6", "class-variance-authority": "^0.7.0", + "classnames": "^2.3.2", "clsx": "^2.0.0", "content-type": "^1.0.5", "jsdom": "^22.1.0", "lucide-react": "^0.279.0", "next": "latest", + "next-themes": "^0.2.1", "node-html-parser": "^6.1.10", "react": "latest", "react-dom": "latest", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46125fb..8a2b236 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@radix-ui/react-dropdown-menu': + specifier: ^2.0.6 + version: 2.0.6(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-label': specifier: ^2.0.2 version: 2.0.2(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) @@ -20,6 +23,9 @@ dependencies: class-variance-authority: specifier: ^0.7.0 version: 0.7.0 + classnames: + specifier: ^2.3.2 + version: 2.3.2 clsx: specifier: ^2.0.0 version: 2.0.0 @@ -35,6 +41,9 @@ dependencies: next: specifier: latest version: 13.5.3(react-dom@18.2.0)(react@18.2.0) + next-themes: + specifier: ^0.2.1 + version: 0.2.1(next@13.5.3)(react-dom@18.2.0)(react@18.2.0) node-html-parser: specifier: ^6.1.10 version: 6.1.10 @@ -446,6 +455,33 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@types/react': 18.2.24 + '@types/react-dom': 18.2.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.24)(react@18.2.0): resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} peerDependencies: @@ -519,6 +555,44 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-menu@2.0.6(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.24)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@types/react': 18.2.24 + '@types/react-dom': 18.2.8 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.24)(react@18.2.0) + dev: false + /@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==} peerDependencies: @@ -570,6 +644,28 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@types/react': 18.2.24 + '@types/react-dom': 18.2.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} peerDependencies: @@ -591,6 +687,35 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.24)(react@18.2.0) + '@types/react': 18.2.24 + '@types/react-dom': 18.2.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-select@2.0.0(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==} peerDependencies: @@ -1261,6 +1386,10 @@ packages: clsx: 2.0.0 dev: false + /classnames@2.3.2: + resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} + dev: false + /client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} dev: false @@ -2586,6 +2715,18 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /next-themes@0.2.1(next@13.5.3)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} + peerDependencies: + next: '*' + react: '*' + react-dom: '*' + dependencies: + next: 13.5.3(react-dom@18.2.0)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /next@13.5.3(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-4Nt4HRLYDW/yRpJ/QR2t1v63UOMS55A38dnWv3UDOWGezuY0ZyFO1ABNbD7mulVzs9qVhgy2+ppjdsANpKP1mg==} engines: {node: '>=16.14.0'} diff --git a/src/features/theme-switch/index.tsx b/src/features/theme-switch/index.tsx new file mode 100644 index 0000000..6c38781 --- /dev/null +++ b/src/features/theme-switch/index.tsx @@ -0,0 +1,40 @@ +'use client' + +import * as React from 'react' +import { Moon, Sun } from 'lucide-react' +import { useTheme } from 'next-themes' + +import { Button } from '@/shadcn/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/shadcn/ui/dropdown-menu' + +export function ThemeSwitcher() { + const { setTheme } = useTheme() + + return ( + + + + + + setTheme('light')}> + Светлая тема + + setTheme('dark')}> + Темная тема + + setTheme('system')}> + Как в системе + + + + ) +} diff --git a/src/pages/[group].tsx b/src/pages/[group].tsx new file mode 100644 index 0000000..ba93979 --- /dev/null +++ b/src/pages/[group].tsx @@ -0,0 +1,31 @@ +import { Schedule } from '@/widgets/schedule' +import { Day } from '@/shared/model/day' +import { GetServerSidePropsResult } from 'next' +import { getSchedule } from '@/app/agregator/schedule' +import { NextSerialized, nextDeserializer, nextSerialized } from '@/app/utils/date-serializer' +import { NavBar } from '@/widgets/navbar' + +type PageProps = NextSerialized<{ + schedule: Day[] +}> + +export default function HomePage(props: PageProps) { + const { schedule } = nextDeserializer(props) + + return ( + <> + + + + ) +} + +export async function getServerSideProps(): Promise> { + const schedule = await getSchedule(146) + + return { + props: { + schedule: nextSerialized(schedule) + } + } +} \ No newline at end of file diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 1c19372..1fd427d 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,6 +1,16 @@ import '@/shared/styles/globals.css' import type { AppProps } from 'next/app' +import { ThemeProvider } from '@/shared/providers/theme-provider' export default function App({ Component, pageProps }: AppProps) { - return + return ( + + + + ) } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 4090fc6..44b58f5 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,27 +1,12 @@ -import { Schedule } from '@/widgets/schedule' -import { Day } from '@/shared/model/day' import { GetServerSidePropsResult } from 'next' -import { getSchedule } from '@/app/agregator/schedule' -import { NextSerialized, nextDeserializer, nextSerialized } from '@/app/utils/date-serializer' -type PageProps = NextSerialized<{ - schedule: Day[] -}> - -export default function HomePage(props: PageProps) { - const { schedule } = nextDeserializer(props) - - return ( - - ) -} - -export async function getServerSideProps(): Promise> { - const schedule = await getSchedule(146) +export default function HomePage() { } +export async function getServerSideProps(): Promise>> { return { - props: { - schedule: nextSerialized(schedule) + redirect: { + destination: '/ps7', + permanent: false } } } \ No newline at end of file diff --git a/src/shadcn/ui/dropdown-menu.tsx b/src/shadcn/ui/dropdown-menu.tsx new file mode 100644 index 0000000..780b3e5 --- /dev/null +++ b/src/shadcn/ui/dropdown-menu.tsx @@ -0,0 +1,198 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/shared/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/src/shared/providers/theme-provider.tsx b/src/shared/providers/theme-provider.tsx new file mode 100644 index 0000000..007e461 --- /dev/null +++ b/src/shared/providers/theme-provider.tsx @@ -0,0 +1,7 @@ +import * as React from 'react' +import { ThemeProvider as NextThemesProvider } from 'next-themes' +import { type ThemeProviderProps } from 'next-themes/dist/types' + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children} +} diff --git a/src/styles/globals.css b/src/styles/globals.css index fd81e88..c5b513c 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -25,3 +25,7 @@ body { ) rgb(var(--background-start-rgb)); } + +* { + box-sizing: border-box; +} \ No newline at end of file diff --git a/src/widgets/navbar/index.tsx b/src/widgets/navbar/index.tsx new file mode 100644 index 0000000..29cd531 --- /dev/null +++ b/src/widgets/navbar/index.tsx @@ -0,0 +1,37 @@ +import { ThemeSwitcher } from '@/features/theme-switch' +import { Button } from '@/shadcn/ui/button' +import { useTheme } from 'next-themes' +import Link from 'next/link' +import { useRouter } from 'next/router' +import cx from 'classnames' + +export function NavBar() { + const { theme } = useTheme() + + return ( +
+ +
+ ) +} + +function NavBarItem({ url, children }: React.PropsWithChildren<{ + url: string +}>) { + const router = useRouter() + const isActive = router.asPath === url + + return ( +
  • + + + +
  • + ) +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 3ca6a9a..8392698 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -14,9 +18,23 @@ "jsx": "preserve", "incremental": true, "paths": { - "@/*": ["./src/*"] - } + "@/*": [ + "./src/*" + ] + }, + "plugins": [ + { + "name": "next" + } + ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] }