├── src ├── styles │ └── globals.css ├── app │ ├── favicon.ico │ ├── apple-icon.png │ ├── classes │ │ ├── add │ │ │ ├── (svg) │ │ │ │ ├── index.tsx │ │ │ │ └── ExtensionIcon.tsx │ │ │ ├── manual │ │ │ │ └── page.tsx │ │ │ ├── blocked │ │ │ │ ├── page.tsx │ │ │ │ └── BlockedForm.tsx │ │ │ ├── layout.tsx │ │ │ ├── PageSelector.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── [class] │ │ │ ├── not-found.tsx │ │ │ ├── layout.tsx │ │ │ ├── DeleteButton.tsx │ │ │ └── page.tsx │ │ └── (ClassesComponents) │ │ │ ├── LinkedMobileClassList.tsx │ │ │ └── LinkedClassList.tsx │ ├── page.tsx │ ├── (Home) │ │ ├── SidebarLayout.tsx │ │ ├── ClassListSidebar.tsx │ │ ├── DraggableClassList.tsx │ │ ├── DraggableMobileClassList.tsx │ │ └── Calendar.tsx │ ├── (Navbar) │ │ ├── ActiveLink.tsx │ │ ├── navbar.tsx │ │ └── bmc.tsx │ ├── error.tsx │ ├── icon.svg │ ├── layout.tsx │ └── help │ │ └── page.tsx ├── components │ ├── Tabs │ │ └── Tabs.tsx │ ├── Calendar │ │ ├── generated │ │ │ ├── generate.worker.ts │ │ │ ├── GeneratedPreferences.tsx │ │ │ ├── SortablePopover.tsx │ │ │ └── generate.ts │ │ ├── CalendarTimeSlot.tsx │ │ ├── CalendarLayout.tsx │ │ ├── CalendarToolbar.tsx │ │ ├── CalendarHeader.tsx │ │ └── AddFriendModal.tsx │ ├── Badge │ │ └── Badge.tsx │ ├── Event │ │ ├── Clash.tsx │ │ ├── BlockedEventList.tsx │ │ ├── EventBody.tsx │ │ ├── FriendEventList.tsx │ │ ├── EventList.tsx │ │ ├── EventClientWrapper.tsx │ │ └── PreviewEventClient.tsx │ ├── Link │ │ └── RetainParamsLink.tsx │ ├── DragOverlay │ │ ├── DragOverlay.tsx │ │ ├── ClassCardDragOverlay.tsx │ │ └── EventDragOverlay.tsx │ ├── Button │ │ ├── ShareButton.tsx │ │ ├── ClearPreferenceButton.tsx │ │ └── Button.tsx │ ├── Tooltip │ │ └── Tooltip.tsx │ ├── Dialog │ │ ├── DialogLayout.tsx │ │ └── BlockedTimeDialog.tsx │ ├── index.tsx │ ├── ClassCard │ │ ├── ClassCardClient.tsx │ │ └── ClassCard.tsx │ ├── ThemeSelectors │ │ └── ThemeSelector.tsx │ └── Popovers │ │ ├── FriendPopover.tsx │ │ └── AllocatedPopover.tsx ├── lib │ ├── utils.ts │ ├── definitions.ts │ └── functions.ts ├── hooks │ ├── useMounted.tsx │ └── useUrlState.tsx ├── contexts │ ├── ThemeProvider.tsx │ ├── FriendContext.tsx │ ├── PreviewContext.tsx │ └── DndProvider.tsx └── env.mjs ├── postcss.config.cjs ├── prettier.config.mjs ├── next.config.mjs ├── .env.example ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── .gitignore ├── tsconfig.json ├── .eslintrc.cjs ├── tailwind.config.ts ├── package.json ├── README.md └── public └── bookmarklet.js /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maximusdionyssopoulos/PlanMyTimetable/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maximusdionyssopoulos/PlanMyTimetable/HEAD/src/app/apple-icon.png -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /src/components/Tabs/Tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Tabs, TabsList, TabsTrigger, TabsContent } from "@radix-ui/react-tabs"; 3 | 4 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 5 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').options} */ 2 | const config = { 3 | plugins: ["prettier-plugin-tailwindcss"], 4 | }; 5 | 6 | export default config; 7 | -------------------------------------------------------------------------------- /src/app/classes/add/(svg)/index.tsx: -------------------------------------------------------------------------------- 1 | export { 2 | ChromeIcon, 3 | FirefoxIcon, 4 | ChromiumIcon, 5 | ArcIcon, 6 | BraveIcon, 7 | OperaIcon, 8 | EdgeIcon, 9 | } from "./BrowserIcons"; 10 | export { ExtensionIcon } from "./ExtensionIcon"; 11 | -------------------------------------------------------------------------------- /src/app/classes/layout.tsx: -------------------------------------------------------------------------------- 1 | import { PreviewProvider } from "~/contexts/PreviewContext"; 2 | export default function ClassesLayout({ 3 | children, 4 | }: { 5 | children: React.ReactNode; 6 | }) { 7 | return {children}; 8 | } 9 | -------------------------------------------------------------------------------- /src/hooks/useMounted.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export function useMounted() { 4 | const [mounted, setMounted] = React.useState(false); 5 | 6 | React.useEffect(() => { 7 | setMounted(true); 8 | }, []); 9 | 10 | return mounted; 11 | } 12 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful 3 | * for Docker builds. 4 | */ 5 | await import("./src/env.mjs"); 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const config = {}; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /src/app/classes/add/manual/page.tsx: -------------------------------------------------------------------------------- 1 | import ClassForm from "../../(ClassesComponents)/ClassForm"; 2 | import type { Metadata } from "next"; 3 | 4 | export const metadata: Metadata = { 5 | title: "Manual class add", 6 | }; 7 | export default function Page() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Calendar/generated/generate.worker.ts: -------------------------------------------------------------------------------- 1 | import { generate } from "./generate"; 2 | import type { GenerateEvent } from "./generate"; 3 | 4 | addEventListener("message", (event: MessageEvent) => { 5 | const result = generate(event.data.courses, event.data.options); 6 | postMessage(result); 7 | }); 8 | -------------------------------------------------------------------------------- /src/contexts/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import { type ThemeProviderProps } from "next-themes/dist/types"; 6 | 7 | export default function ThemeProvider({ 8 | children, 9 | ...props 10 | }: ThemeProviderProps) { 11 | return {children}; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Calendar/CalendarTimeSlot.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function TimeSlot({ col, row }: { col: number; row: number }) { 4 | return ( 5 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Badge/Badge.tsx: -------------------------------------------------------------------------------- 1 | interface BadgeProps { 2 | children: React.ReactNode; 3 | className?: string; 4 | } 5 | 6 | function Badge({ children, className }: BadgeProps) { 7 | return ( 8 |
14 | {children} 15 |
16 | ); 17 | } 18 | 19 | export default Badge; 20 | -------------------------------------------------------------------------------- /src/components/Calendar/CalendarLayout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { usePreview } from "~/contexts/PreviewContext"; 4 | 5 | export default function CalendarLayout({ 6 | children, 7 | }: { 8 | children: React.ReactNode; 9 | }) { 10 | const { courseData } = usePreview(); 11 | 12 | return ( 13 |
18 | {children} 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/classes/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { usePreview } from "~/contexts/PreviewContext"; 3 | import { useUrlState } from "~/hooks/useUrlState"; 4 | 5 | export default function Page() { 6 | const { courseData } = usePreview(); 7 | const { redirect } = useUrlState(); 8 | if (courseData.length !== 0) { 9 | const redirectURL = `/classes/${courseData[0]?.id}`; 10 | // console.log(redirectURL); 11 | redirect(redirectURL); 12 | } else { 13 | redirect("/classes/add"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Event/Clash.tsx: -------------------------------------------------------------------------------- 1 | export default function Clash({ 2 | col, 3 | row, 4 | span, 5 | children, 6 | }: { 7 | col: number; 8 | row: number; 9 | span: number; 10 | children: React.ReactNode; 11 | }) { 12 | return ( 13 |
21 | {children} 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/classes/add/blocked/page.tsx: -------------------------------------------------------------------------------- 1 | import BlockedForm from "./BlockedForm"; 2 | import type { Metadata } from "next"; 3 | 4 | export const metadata: Metadata = { 5 | title: "Add blocked time", 6 | }; 7 | export default function Page() { 8 | return ( 9 | <> 10 |

11 | Please note blocked times hide options for being displayed if they are 12 | in that blocked time. 13 |

14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/app/classes/add/layout.tsx: -------------------------------------------------------------------------------- 1 | import PageSelector from "./PageSelector"; 2 | export default function AddLayout({ children }: { children: React.ReactNode }) { 3 | return ( 4 |
5 |

Add classes

6 |

7 | Add class details, times, locations and block out times in your 8 | calendar. 9 |

10 | 11 | {children} 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Since the ".env" file is gitignored, you can use the ".env.example" file to 2 | # build a new ".env" file when you clone the repo. Keep this file up-to-date 3 | # when you add new variables to `.env`. 4 | 5 | # This file will be committed to version control, so make sure not to have any 6 | # secrets in it. If you are cloning this repo, create a copy of this file named 7 | # ".env" and populate it with your secrets. 8 | 9 | # When adding additional environment variables, the schema in "/src/env.mjs" 10 | # should be updated accordingly. 11 | 12 | # Example: 13 | # SERVERVAR="foo" 14 | # NEXT_PUBLIC_CLIENTVAR="bar" 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/components/Link/RetainParamsLink.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useSearchParams } from "next/navigation"; 3 | import Link from "next/link"; 4 | import type { LinkProps } from "next/link"; 5 | interface RetainParamsLink extends LinkProps { 6 | children?: React.ReactNode; 7 | href: string; 8 | className?: string; 9 | } 10 | export default function RetainParamsLink({ 11 | href, 12 | children, 13 | className, 14 | ...props 15 | }: RetainParamsLink) { 16 | const searchParams = useSearchParams(); 17 | return ( 18 | 23 | {children} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/DragOverlay/DragOverlay.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import type { Preference } from "~/lib/definitions"; 3 | import { usePreview } from "~/contexts/PreviewContext"; 4 | import ClassCardDragOverlay from "./ClassCardDragOverlay"; 5 | import EventDragOverlay from "./EventDragOverlay"; 6 | import { useDnD, DragType } from "~/contexts/DndProvider"; 7 | 8 | export default function DragOverlay() { 9 | const { events, activeCourse } = usePreview(); 10 | const { dragType } = useDnD(); 11 | const event: Preference | undefined = events.find( 12 | (course) => course.id === activeCourse?.id, 13 | ); 14 | return event && dragType === DragType.event ? ( 15 | 16 | ) : ( 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | next-env.d.ts 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | *.vscode 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | .pnpm-debug.log* 33 | 34 | # local env files 35 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables 36 | .env 37 | .env*.local 38 | 39 | # vercel 40 | .vercel 41 | 42 | # typescript 43 | *.tsbuildinfo 44 | 45 | 46 | src/data/timetable/*.json 47 | -------------------------------------------------------------------------------- /src/components/Button/ShareButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button, Tooltip } from "~/components"; 4 | import { IoIosShareAlt } from "react-icons/io"; 5 | import { useSearchParams } from "next/navigation"; 6 | import { useCallback } from "react"; 7 | 8 | export default function ShareButton() { 9 | const p = useSearchParams(); 10 | const copyLink = useCallback(async () => { 11 | // console.log(pathname); 12 | const markdown = `[View My Timetable](https://planmytimetable.vercel.app/?${p.toString()})`; 13 | await navigator.clipboard.writeText(markdown); 14 | }, [p]); 15 | 16 | return ( 17 | 22 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "checkJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "noUncheckedIndexedAccess": true, 19 | "baseUrl": ".", 20 | "paths": { 21 | "~/*": ["./src/*"] 22 | }, 23 | "plugins": [{ "name": "next" }] 24 | }, 25 | "include": [ 26 | ".eslintrc.cjs", 27 | "next-env.d.ts", 28 | "**/*.ts", 29 | "**/*.tsx", 30 | "**/*.cjs", 31 | "**/*.mjs", 32 | ".next/types/**/*.ts" 33 | ], 34 | "exclude": ["node_modules"] 35 | } 36 | -------------------------------------------------------------------------------- /src/app/classes/[class]/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { buttonVariants } from "~/components"; 2 | import { HiOutlineExclamationCircle } from "react-icons/hi2"; 3 | import RetainParamsLink from "~/components/Link/RetainParamsLink"; 4 | import { cn } from "~/lib/utils"; 5 | export default function NotFound() { 6 | return ( 7 |
8 |
9 | 10 |
11 |

Not found

12 |

13 | This class could not be found. Please try again. 14 |

15 | 19 | Return home 20 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/Button/ClearPreferenceButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import type { SetStateAction } from "react"; 3 | import { HiXMark } from "react-icons/hi2"; 4 | import { useUrlState } from "~/hooks/useUrlState"; 5 | 6 | export default function ClearPreferencesButton({ 7 | setIsOpen, 8 | }: { 9 | setIsOpen: (value: SetStateAction) => void; 10 | }) { 11 | const { replaceState } = useUrlState(); 12 | // const searchParams = useSearchParams(); 13 | return ( 14 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/components/DragOverlay/ClassCardDragOverlay.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { DragOverlay } from "@dnd-kit/core"; 3 | import { usePreview } from "~/contexts/PreviewContext"; 4 | import ClassCard from "../ClassCard/ClassCard"; 5 | import { colourVariants } from "~/lib/definitions"; 6 | import { RxDragHandleDots2 } from "react-icons/rx"; 7 | 8 | export default function ClassCardDragOverlay() { 9 | const { activeCourse } = usePreview(); 10 | return ( 11 | 16 | {activeCourse && ( 17 | <> 18 |
19 | 20 |
21 | 22 | 23 | )} 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Tooltip/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/lib/utils"; 2 | 3 | interface ToolTipProps extends React.HtmlHTMLAttributes { 4 | message: React.ReactNode; 5 | children: React.ReactNode; 6 | position: "top" | "left" | "right" | "bottom"; 7 | } 8 | 9 | export default function Tooltip({ 10 | message, 11 | children, 12 | position, 13 | className, 14 | ...props 15 | }: ToolTipProps) { 16 | const positionClass = { 17 | bottom: "top-10", 18 | top: "bottom-10", 19 | left: "right-10", 20 | right: "left-10", 21 | }; 22 | return ( 23 |
24 | {children} 25 | 32 | {message} 33 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Calendar/CalendarToolbar.tsx: -------------------------------------------------------------------------------- 1 | import { AddFriend, FriendPopover, Share } from "~/components"; 2 | import { FriendProvider } from "~/contexts/FriendContext"; 3 | import { GeneratedPreferences } from "./generated/GeneratedPreferences"; 4 | export default function CalendarToolbar() { 5 | return ( 6 |
7 | }> 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 |
16 | ); 17 | } 18 | 19 | function Loading() { 20 | return ( 21 |
22 |
23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import ClassListSidebar from "./(Home)/ClassListSidebar"; 2 | import Calendar from "./(Home)/Calendar"; 3 | import ClassList from "./(Home)/DraggableClassList"; 4 | import { PreviewProvider } from "~/contexts/PreviewContext"; 5 | import { DndProvider } from "~/contexts/DndProvider"; 6 | import { AllocatedPopover, DragOverlay } from "~/components"; 7 | 8 | export default function HomePage() { 9 | return ( 10 | 11 | 12 | 13 |
14 | 15 |
16 |
19 | 20 |
21 |
22 | 23 | 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/app/(Home)/SidebarLayout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { usePreview } from "~/contexts/PreviewContext"; 3 | import { HiOutlineAcademicCap } from "react-icons/hi2"; 4 | import { RetainLink } from "~/components"; 5 | import { buttonVariants } from "~/components"; 6 | 7 | export default function SidebarLayout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | const { courseData } = usePreview(); 13 | 14 | if (!courseData || courseData.length === 0) { 15 | return ( 16 |
17 | 18 |

{`You haven't added any classes yet.`}

19 | 23 | Add a class now 24 | 25 |
26 | ); 27 | } 28 | 29 | return ( 30 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | const config = { 3 | parser: "@typescript-eslint/parser", 4 | parserOptions: { 5 | project: true, 6 | }, 7 | plugins: ["@typescript-eslint"], 8 | extends: [ 9 | "next/core-web-vitals", 10 | "plugin:@typescript-eslint/recommended-type-checked", 11 | "plugin:@typescript-eslint/stylistic-type-checked", 12 | ], 13 | rules: { 14 | // These opinionated rules are enabled in stylistic-type-checked above. 15 | // Feel free to reconfigure them to your own preference. 16 | "@typescript-eslint/array-type": "off", 17 | "@typescript-eslint/consistent-type-definitions": "off", 18 | 19 | "@typescript-eslint/consistent-type-imports": [ 20 | "warn", 21 | { 22 | prefer: "type-imports", 23 | fixStyle: "inline-type-imports", 24 | }, 25 | ], 26 | "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], 27 | "@typescript-eslint/no-misused-promises": [ 28 | 2, 29 | { 30 | checksVoidReturn: { attributes: false }, 31 | }, 32 | ], 33 | }, 34 | }; 35 | 36 | module.exports = config; 37 | -------------------------------------------------------------------------------- /src/app/(Home)/ClassListSidebar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | RetainLink, 3 | buttonVariants, 4 | AllocatedPopover, 5 | BlockedDialog, 6 | } from "~/components"; 7 | import { HiOutlinePlusCircle } from "react-icons/hi"; 8 | import SidebarLayout from "./SidebarLayout"; 9 | import MobileClassList from "./DraggableMobileClassList"; 10 | 11 | export default function ClassListSidebar({ 12 | children, 13 | }: { 14 | children: React.ReactNode; 15 | }) { 16 | return ( 17 | 18 |
19 |

20 | 21 | Classes 22 |

23 |
24 |
25 | 26 |
27 | 32 | Add 33 | 34 | 35 |
36 |
37 | {children} 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/app/(Home)/DraggableClassList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { ClassCard, ClassCardClient } from "~/components"; 3 | import { CourseType } from "~/lib/definitions"; 4 | import { usePreview } from "~/contexts/PreviewContext"; 5 | import { RxDragHandleDots2 } from "react-icons/rx"; 6 | 7 | export default function ClassList({ isMobile }: { isMobile: boolean }) { 8 | const { courseData } = usePreview(); 9 | 10 | return ( 11 |
12 | {courseData.map((item) => ( 13 |
17 | 21 |
22 | 26 |
27 | 28 |
29 |
30 | ))} 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/(Navbar)/ActiveLink.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { usePathname, useSelectedLayoutSegment } from "next/navigation"; 3 | import { cn } from "~/lib/utils"; 4 | import { RetainLink } from "~/components"; 5 | import type { LinkProps } from "next/link"; 6 | interface RetainParamsLink extends LinkProps { 7 | children?: React.ReactNode; 8 | href: string; 9 | className?: string; 10 | } 11 | 12 | export default function ActiveLink({ 13 | href, 14 | children, 15 | className, 16 | ...props 17 | }: RetainParamsLink) { 18 | const segement = usePathname(); 19 | const layoutSegement = useSelectedLayoutSegment(); 20 | // console.log(segement); 21 | const active = 22 | segement === href || (layoutSegement && href.includes(layoutSegement)); 23 | 24 | const activeStyle = 25 | "[&[data-state=active]]:bg-neutral-50 [&[data-state=active]]:font-medium [&[data-state=active]]:dark:bg-neutral-800"; 26 | return ( 27 | 38 | {children} 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/env.mjs: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-nextjs"; 2 | import { z } from "zod"; 3 | 4 | export const env = createEnv({ 5 | /** 6 | * Specify your server-side environment variables schema here. This way you can ensure the app 7 | * isn't built with invalid env vars. 8 | */ 9 | server: { 10 | NODE_ENV: z.enum(["development", "test", "production"]), 11 | }, 12 | 13 | /** 14 | * Specify your client-side environment variables schema here. This way you can ensure the app 15 | * isn't built with invalid env vars. To expose them to the client, prefix them with 16 | * `NEXT_PUBLIC_`. 17 | */ 18 | client: { 19 | // NEXT_PUBLIC_CLIENTVAR: z.string(), 20 | }, 21 | 22 | /** 23 | * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. 24 | * middlewares) or client-side so we need to destruct manually. 25 | */ 26 | runtimeEnv: { 27 | NODE_ENV: process.env.NODE_ENV, 28 | // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, 29 | }, 30 | /** 31 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. 32 | * This is especially useful for Docker builds. 33 | */ 34 | skipValidation: !!process.env.SKIP_ENV_VALIDATION, 35 | /** 36 | * Makes it so that empty strings are treated as undefined. 37 | * `SOME_VAR: z.string()` and `SOME_VAR=''` will throw an error. 38 | */ 39 | emptyStringAsUndefined: true, 40 | }); 41 | -------------------------------------------------------------------------------- /src/contexts/FriendContext.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useLocalStorage, useIsClient } from "@uidotdev/usehooks"; 4 | import { createContext, useContext } from "react"; 5 | import type { Preference } from "~/lib/definitions"; 6 | 7 | export type Friend = { 8 | id: string; 9 | state: Preference[]; 10 | name: string; 11 | link: string; 12 | active: boolean; 13 | }; 14 | 15 | interface FriendProviderProps { 16 | children: React.ReactNode; 17 | } 18 | 19 | interface FriendContext { 20 | friendData: Friend[]; 21 | setFriendData: React.Dispatch>; 22 | } 23 | 24 | const FriendContext = createContext({} as FriendContext); 25 | export function useFriend() { 26 | return useContext(FriendContext); 27 | } 28 | 29 | function FriendClient({ children }: FriendProviderProps) { 30 | const [friendData, setFriendData] = useLocalStorage("friend", []); 31 | 32 | return ( 33 | 34 | {children} 35 | 36 | ); 37 | } 38 | 39 | type ClientOnlyProps = { 40 | children: React.ReactNode; 41 | fallback?: React.ReactNode; 42 | }; 43 | 44 | export const FriendProvider: React.FC = ({ 45 | children, 46 | fallback, 47 | }) => { 48 | const isClient = useIsClient(); 49 | 50 | // Render children if on client side, otherwise return null 51 | return isClient ? {children} : fallback ?? null; 52 | }; 53 | -------------------------------------------------------------------------------- /src/components/Dialog/DialogLayout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 3 | import type { DialogProps as DialogPrimitiveProps } from "@radix-ui/react-dialog"; 4 | import React from "react"; 5 | 6 | const SheetTrigger = DialogPrimitive.Trigger; 7 | const SheetPortal = DialogPrimitive.Portal; 8 | const SheetClose = DialogPrimitive.Close; 9 | const SheetOverlay = DialogPrimitive.Overlay; 10 | const SheetTitle = DialogPrimitive.Title; 11 | const SheetDescription = DialogPrimitive.Description; 12 | const SheetContent = DialogPrimitive.Content; 13 | 14 | interface SheetProps extends DialogPrimitiveProps { 15 | closeWidth: number; 16 | } 17 | const Sheet = ({ 18 | children, 19 | onOpenChange, 20 | closeWidth, 21 | ...props 22 | }: SheetProps) => { 23 | React.useEffect(() => { 24 | const handleResize = () => { 25 | if (window.innerWidth >= closeWidth) { 26 | onOpenChange?.(false); 27 | } 28 | }; 29 | 30 | window.addEventListener("resize", handleResize); 31 | 32 | return () => { 33 | window.removeEventListener("resize", handleResize); 34 | }; 35 | }, [onOpenChange, closeWidth]); 36 | 37 | return ( 38 | 39 | {children} 40 | 41 | ); 42 | }; 43 | 44 | export { 45 | Sheet, 46 | SheetPortal, 47 | SheetOverlay, 48 | SheetClose, 49 | SheetTrigger, 50 | SheetContent, 51 | SheetTitle, 52 | SheetDescription, 53 | }; 54 | -------------------------------------------------------------------------------- /src/components/Event/BlockedEventList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { usePreview } from "~/contexts/PreviewContext"; 3 | import { getDayEnum, getRowIndex } from "~/lib/functions"; 4 | import { Duration, DateTime } from "luxon"; 5 | 6 | export default function BlockedEventList() { 7 | const { blockedEvents } = usePreview(); 8 | return blockedEvents.map((blocked) => { 9 | let endTime = DateTime.fromFormat(blocked.start, "HH:mm"); 10 | const duration = Duration.fromObject({ minutes: blocked.duration }); 11 | endTime = endTime.plus(duration); 12 | const col = getDayEnum(blocked.day) + 2; 13 | const row = getRowIndex(blocked.start); 14 | const rowSpan = blocked.duration / 30; 15 | 16 | return ( 17 |
26 |

27 | {blocked.name} 28 |

29 |

30 | {blocked.start} - {endTime.toFormat("HH:mm")} 31 |

32 |
33 | ); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/app/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Link from "next/link"; 3 | import { HiOutlineExclamationCircle } from "react-icons/hi2"; 4 | import { buttonVariants } from "~/components"; 5 | import { usePathname } from "next/navigation"; 6 | 7 | export default function Error({ 8 | error, 9 | }: { 10 | error: Error & { digest?: string }; 11 | }) { 12 | const pathname = usePathname(); 13 | return ( 14 |
15 |
16 | 20 |
21 |

Uh oh, something went wrong

22 |

{error.message}

23 |
24 |

25 | Please report this issue,{" "} 26 | 30 | by creating an issue on Github 31 | 32 |

33 |

(Please include the url in the issue)

34 |
35 | window.location.reload()} 39 | > 40 | Try again 41 | 42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { type Config } from "tailwindcss"; 2 | import { fontFamily } from "tailwindcss/defaultTheme"; 3 | import plugin from "tailwindcss/plugin"; 4 | 5 | export default { 6 | content: ["./src/**/*.{tsx,ts}"], 7 | theme: { 8 | extend: { 9 | keyframes: { 10 | "accordion-down": { 11 | from: { height: "0" }, 12 | to: { height: "var(--radix-accordion-content-height)" }, 13 | }, 14 | "accordion-up": { 15 | from: { height: "var(--radix-accordion-content-height)" }, 16 | to: { height: "0" }, 17 | }, 18 | }, 19 | animation: { 20 | "accordion-down": "accordion-down 0.2s ease-out", 21 | "accordion-up": "accordion-up 0.2s ease-out", 22 | }, 23 | gridTemplateRows: { 24 | "48": "repeat(48, minmax(0, 1fr))", 25 | }, 26 | fontSize: { 27 | md: [ 28 | "1.125rem", 29 | { 30 | lineHeight: "1.625", 31 | }, 32 | ], 33 | }, 34 | fontFamily: { 35 | sans: ["var(--font-sans)", ...fontFamily.sans], 36 | }, 37 | }, 38 | }, 39 | darkMode: "class", 40 | plugins: [ 41 | plugin(function ({ addUtilities }) { 42 | addUtilities({ 43 | ".scrollbar-hide": { 44 | /* IE and Edge */ 45 | "-ms-overflow-style": "none", 46 | 47 | /* Firefox */ 48 | "scrollbar-width": "none", 49 | 50 | /* Safari and Chrome */ 51 | "&::-webkit-scrollbar": { 52 | display: "none", 53 | }, 54 | }, 55 | }); 56 | }), 57 | ], 58 | } satisfies Config; 59 | -------------------------------------------------------------------------------- /src/app/classes/[class]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { HiOutlinePlusCircle } from "react-icons/hi"; 2 | import { RetainLink, Tooltip, buttonVariants } from "~/components"; 3 | import ClassList from "../(ClassesComponents)/LinkedClassList"; 4 | import MobileClassList from "../(ClassesComponents)/LinkedMobileClassList"; 5 | 6 | import type { Metadata } from "next"; 7 | 8 | export const metadata: Metadata = { 9 | title: "Edit Class", 10 | }; 11 | 12 | export default function ClassLayout({ 13 | children, 14 | }: { 15 | children: React.ReactNode; 16 | }) { 17 | return ( 18 |
19 |
20 |
21 | 22 | 23 |

24 | Classes 25 |

26 |
27 |
28 | 29 | 33 | 34 | Add 35 | 36 | 37 |
38 |
39 |
40 |
41 | 42 |
43 | {children} 44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/app/classes/(ClassesComponents)/LinkedMobileClassList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState } from "react"; 3 | import { HiBars3 } from "react-icons/hi2"; 4 | import { HiOutlineX } from "react-icons/hi"; 5 | import { 6 | Button, 7 | Sheet, 8 | SheetTrigger, 9 | SheetPortal, 10 | SheetOverlay, 11 | SheetContent, 12 | SheetClose, 13 | SheetTitle, 14 | } from "~/components"; 15 | import ClassList from "./LinkedClassList"; 16 | 17 | export default function MobileClassList() { 18 | const [open, setOpen] = useState(false); 19 | 20 | return ( 21 | 22 | 23 | 29 | 30 | 31 | 32 | 33 | 34 | Classes 35 | 36 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/components/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Badge } from "./Badge/Badge"; 2 | export { default as ClassCard } from "./ClassCard/ClassCard"; 3 | export { default as ClassCardClient } from "./ClassCard/ClassCardClient"; 4 | export { default as CalendarHeader } from "./Calendar/CalendarHeader"; 5 | export { default as TimeSlot } from "./Calendar/CalendarTimeSlot"; 6 | export { default as AddFriend } from "./Calendar/AddFriendModal"; 7 | export { default as AllocatedPopover } from "./Popovers/AllocatedPopover"; 8 | export { default as FriendPopover } from "./Popovers/FriendPopover"; 9 | export { default as Share } from "./Button/ShareButton"; 10 | export { default as Button } from "./Button/Button"; 11 | export { buttonVariants } from "./Button/Button"; 12 | export { default as Tooltip } from "./Tooltip/Tooltip"; 13 | export { default as ClearPreferences } from "./Button/ClearPreferenceButton"; 14 | export { default as Event } from "./Event/EventBody"; 15 | export { default as EventList } from "./Event/EventList"; 16 | export { default as BlockedEventList } from "./Event/BlockedEventList"; 17 | export { default as PreviewEventClient } from "./Event/PreviewEventClient"; 18 | export { default as DragOverlay } from "./DragOverlay/DragOverlay"; 19 | export { default as RetainLink } from "./Link/RetainParamsLink"; 20 | export { default as ThemeSelector } from "./ThemeSelectors/ThemeSelector"; 21 | export { 22 | Sheet, 23 | SheetPortal, 24 | SheetOverlay, 25 | SheetClose, 26 | SheetTrigger, 27 | SheetContent, 28 | SheetTitle, 29 | SheetDescription, 30 | } from "./Dialog/DialogLayout"; 31 | export { default as BlockedDialog } from "./Dialog/BlockedTimeDialog"; 32 | export { Tabs, TabsList, TabsTrigger, TabsContent } from "./Tabs/Tabs"; 33 | -------------------------------------------------------------------------------- /src/app/(Home)/DraggableMobileClassList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { HiBars3 } from "react-icons/hi2"; 3 | import { HiOutlineX } from "react-icons/hi"; 4 | import { 5 | Button, 6 | Sheet, 7 | SheetTrigger, 8 | SheetPortal, 9 | SheetContent, 10 | SheetClose, 11 | SheetTitle, 12 | } from "~/components"; 13 | import ClassList from "./DraggableClassList"; 14 | import { useDnD } from "~/contexts/DndProvider"; 15 | 16 | export default function MobileClassList() { 17 | const { 18 | hidden, 19 | openMobileClassListSheet: open, 20 | setMobileClassListSheetOpen: setOpen, 21 | } = useDnD(); 22 | 23 | return ( 24 | 25 | 26 | 32 | 33 | 34 | 52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/ClassCard/ClassCardClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { colourVariants } from "~/lib/definitions"; 3 | import type { Course } from "~/lib/definitions"; 4 | import { useDraggable } from "@dnd-kit/core"; 5 | import { usePreview } from "~/contexts/PreviewContext"; 6 | import { useMemo } from "react"; 7 | import { cn } from "~/lib/utils"; 8 | export default function ClassCardClient({ 9 | children, 10 | course, 11 | id, 12 | }: { 13 | children: React.ReactNode; 14 | course: Course; 15 | id: string; 16 | }) { 17 | const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ 18 | id: id, 19 | data: { 20 | course: course, 21 | }, 22 | attributes: { 23 | role: "div", 24 | tabIndex: 0, 25 | }, 26 | }); 27 | const { events } = usePreview(); 28 | const isAllocated = useMemo( 29 | () => events.find((item) => item.id === course.id), 30 | [events, course], 31 | ); 32 | return ( 33 |
52 | {children} 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/app/classes/(ClassesComponents)/LinkedClassList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { ClassCard } from "~/components"; 3 | import { colourVariants } from "~/lib/definitions"; 4 | import { RetainLink } from "~/components"; 5 | import { usePreview } from "~/contexts/PreviewContext"; 6 | import { HiChevronRight } from "react-icons/hi2"; 7 | import { usePathname } from "next/navigation"; 8 | import { cn } from "~/lib/utils"; 9 | 10 | export default function ClassList() { 11 | const { courseData } = usePreview(); 12 | const segement = usePathname(); 13 | 14 | return ( 15 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/app/classes/[class]/DeleteButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Button } from "~/components"; 3 | import { HiTrash } from "react-icons/hi2"; 4 | import { useUrlState } from "~/hooks/useUrlState"; 5 | import toast from "react-hot-toast"; 6 | import { usePreview } from "~/contexts/PreviewContext"; 7 | import type { Preference } from "~/lib/definitions"; 8 | 9 | interface deleteButtonProps { 10 | id: string; 11 | } 12 | 13 | export default function DeleteButton({ id }: deleteButtonProps) { 14 | const { courseData } = usePreview(); 15 | const { replaceMultiple, decode } = useUrlState(); 16 | 17 | const handleDelete = () => { 18 | const index = courseData.findIndex((item) => item.id === id); 19 | let events: Preference[] = []; 20 | const parsedPrefs: Preference[] = decode("pref") as Preference[]; 21 | if (parsedPrefs) { 22 | events = parsedPrefs; 23 | } 24 | const eventIndex = events.findIndex((item) => item.id === id); 25 | 26 | const classes = index !== -1 ? courseData.toSpliced(index, 1) : courseData; 27 | const newEvents = 28 | eventIndex !== -1 ? events.toSpliced(eventIndex, 1) : events; 29 | const newIndex = index >= courseData.length - 1 ? 0 : index + 1; 30 | 31 | replaceMultiple( 32 | [ 33 | { element: classes, prefName: "state" }, 34 | { element: newEvents, prefName: "pref" }, 35 | ], 36 | classes.length === 0 37 | ? "/classes/add" 38 | : `/classes/${courseData[newIndex]?.id}`, 39 | ); 40 | toast.success("Class deleted successfully"); 41 | }; 42 | 43 | return ( 44 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timetable", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "lint": "next lint", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "@dnd-kit/core": "^6.1.0", 13 | "@dnd-kit/modifiers": "^7.0.0", 14 | "@dnd-kit/sortable": "^10.0.0", 15 | "@hookform/resolvers": "^3.3.2", 16 | "@radix-ui/react-accordion": "^1.1.2", 17 | "@radix-ui/react-dialog": "^1.0.5", 18 | "@radix-ui/react-tabs": "^1.0.4", 19 | "@t3-oss/env-nextjs": "^0.7.0", 20 | "@uidotdev/usehooks": "^2.4.1", 21 | "@vercel/analytics": "^1.1.1", 22 | "clsx": "^2.0.0", 23 | "jsoncrush": "^1.1.8", 24 | "luxon": "^3.4.4", 25 | "nanoid": "^5.0.4", 26 | "next": "^14.0.4", 27 | "next-themes": "^0.2.1", 28 | "react": "^18.2.0", 29 | "react-dom": "^18.2.0", 30 | "react-hook-form": "^7.48.2", 31 | "react-hot-toast": "^2.4.1", 32 | "react-icons": "^4.11.0", 33 | "react-tiny-popover": "^8.0.4", 34 | "tailwind-merge": "^2.1.0", 35 | "zod": "^3.22.4" 36 | }, 37 | "devDependencies": { 38 | "@types/eslint": "^8.44.2", 39 | "@types/luxon": "^3.3.7", 40 | "@types/node": "^18.16.0", 41 | "@types/react": "^18.2.20", 42 | "@types/react-dom": "^18.2.7", 43 | "@typescript-eslint/eslint-plugin": "^6.3.0", 44 | "@typescript-eslint/parser": "^6.3.0", 45 | "autoprefixer": "^10.4.14", 46 | "eslint": "^8.47.0", 47 | "eslint-config-next": "^13.5.4", 48 | "postcss": "^8.4.27", 49 | "prettier": "^3.0.0", 50 | "prettier-plugin-tailwindcss": "^0.5.1", 51 | "tailwindcss": "^3.4.1", 52 | "typescript": "^5.1.6" 53 | }, 54 | "ct3aMetadata": { 55 | "initVersion": "7.22.0" 56 | }, 57 | "packageManager": "npm@9.6.7" 58 | } 59 | -------------------------------------------------------------------------------- /src/components/ClassCard/ClassCard.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from "~/components"; 2 | import { CourseType } from "~/lib/definitions"; 3 | import type { Course } from "~/lib/definitions"; 4 | 5 | // https://tailwindcss.com/docs/content-configuration#dynamic-class-names 6 | export default function ClassCard({ course }: { course: Course }) { 7 | let cardColour; 8 | switch (course.type) { 9 | case CourseType.Lecture: 10 | cardColour = 11 | "bg-green-200 text-green-800 dark:bg-green-800 dark:text-green-200"; 12 | break; 13 | case CourseType.Tutorial: 14 | cardColour = 15 | "bg-blue-200 text-blue-800 dark:bg-blue-800 dark:text-blue-200"; 16 | break; 17 | case CourseType.Practical: 18 | cardColour = 19 | "bg-rose-200 text-rose-800 dark:bg-rose-800 dark:text-rose-200"; 20 | break; 21 | case CourseType.Workshop: 22 | cardColour = 23 | "bg-amber-200 text-amber-800 dark:bg-amber-800 dark:text-amber-200"; 24 | break; 25 | default: 26 | cardColour = 27 | "bg-neutral-200 text-neutral-800 dark:bg-neutral-600 dark:text-neutral-200"; 28 | break; 29 | } 30 | return ( 31 | <> 32 |

{course.title}

33 |
34 |

35 | {course.courseCode} 36 |

37 |

38 | {course.options.length} -  39 | {course.options.length === 1 ? "Option" : "Options"} 40 |

41 |
42 | {CourseType[course.type]} 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/app/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/app/(Home)/Calendar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | CalendarHeader, 4 | TimeSlot, 5 | PreviewEventClient, 6 | EventList, 7 | BlockedEventList, 8 | } from "~/components"; 9 | import CalendarToolbar from "../../components/Calendar/CalendarToolbar"; 10 | import CalendarLayout from "../../components/Calendar/CalendarLayout"; 11 | import { FriendProvider } from "~/contexts/FriendContext"; 12 | 13 | export default function Calendar() { 14 | return ( 15 | 16 |
17 | 18 | {Array.from({ length: 38 }, (_, index) => { 19 | const num = 2 + index; 20 | return ( 21 | 22 |
26 | {index % 2 !== 1 && `${index / 2 + 5}:00`} 27 |
28 | 29 | 30 | 31 | 32 | 33 |
34 | ); 35 | })} 36 | }> 37 | 38 | 39 | 40 | 41 |
42 | 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "~/styles/globals.css"; 2 | 3 | import { Inter } from "next/font/google"; 4 | import Navbar from "./(Navbar)/navbar"; 5 | import { Toaster } from "react-hot-toast"; 6 | import ThemeProvider from "~/contexts/ThemeProvider"; 7 | import Script from "next/script"; 8 | import { Analytics } from "@vercel/analytics/react"; 9 | import type { Metadata } from "next"; 10 | 11 | const inter = Inter({ 12 | subsets: ["latin"], 13 | variable: "--font-sans", 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: { 18 | default: "PlanMyTimetable", 19 | template: "%s | PlanMyTimetable", 20 | }, 21 | description: "A simple webapp to plan your timetable interactively", 22 | keywords: [ 23 | "RMIT", 24 | "Allocate+", 25 | "Timetable", 26 | "Melbourne Uni", 27 | "Uni Melb", 28 | "Monash", 29 | "Drag", 30 | "Drop", 31 | ], 32 | }; 33 | 34 | export default function RootLayout({ 35 | children, 36 | }: { 37 | children: React.ReactNode; 38 | }) { 39 | return ( 40 | 41 | 44 | 45 | 46 |
47 | {children} 48 |
49 | 55 |
56 | 57 | 58 |