From 9c416c40a4217e92fef72025a7a6bcba31f35072 Mon Sep 17 00:00:00 2001 From: ImJustChew Date: Sun, 29 Oct 2023 21:40:56 +0800 Subject: [PATCH] Added Theme Selection for Timetable --- src/app/[lang]/settings/page.tsx | 37 +++++- .../[lang]/timetable/calendar.ics/route.tsx | 3 +- src/app/[lang]/timetable/view/page.tsx | 6 +- src/components/Alerts/ThemeChangableAlert.tsx | 37 ++++++ .../Timetable/TimetableCourseList.tsx | 6 +- src/dictionaries/en.json | 10 ++ src/dictionaries/zh.json | 10 ++ src/helpers/timetable.ts | 114 +++++++++++++++++- src/hooks/contexts/settings.tsx | 10 +- src/hooks/useUserTimetable.tsx | 6 +- 10 files changed, 225 insertions(+), 14 deletions(-) create mode 100644 src/components/Alerts/ThemeChangableAlert.tsx diff --git a/src/app/[lang]/settings/page.tsx b/src/app/[lang]/settings/page.tsx index 90fd5278..ef40555f 100644 --- a/src/app/[lang]/settings/page.tsx +++ b/src/app/[lang]/settings/page.tsx @@ -1,8 +1,34 @@ 'use client'; import useDictionary from "@/dictionaries/useDictionary"; +import { timetableColors } from "@/helpers/timetable"; import { useSettings } from "@/hooks/contexts/settings"; import { Divider, Option, Select, Switch } from "@mui/joy"; -import { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; + +const TimetableThemePreview = ({ theme, onClick = () => {}, selected = false }: { theme: string, selected?: boolean, onClick?: () => void}) => { + return
+
+ {timetableColors[theme].map((color, index) => ( +
+ ))} +
+ {theme} +
+} + +const TimetableThemeList = () => { + const { timetableTheme, setTimetableTheme } = useSettings(); + return
+ { + Object.keys(timetableColors).map((theme, index) => ( + setTimetableTheme(theme)} selected={timetableTheme == theme} /> + )) + } +
+ +} const SettingsPage = () => { @@ -44,6 +70,15 @@ const SettingsPage = () => {
+ +
+
+

{dict.settings.theme.title}

+

{dict.settings.theme.description}

+
+ {/* TODO: Timetable Preview */} + +
) }; diff --git a/src/app/[lang]/timetable/calendar.ics/route.tsx b/src/app/[lang]/timetable/calendar.ics/route.tsx index d85bd457..20f005db 100644 --- a/src/app/[lang]/timetable/calendar.ics/route.tsx +++ b/src/app/[lang]/timetable/calendar.ics/route.tsx @@ -9,12 +9,13 @@ import { zonedTimeToUtc } from 'date-fns-tz' export async function GET(request: Request) { const { searchParams } = new URL(request.url) const courses_ids = searchParams.get('semester_1121')?.split(',')!; + const theme = searchParams.get('theme') || 'tsinghuarian'; try { let { data = [], error } = await supabase.from('courses').select("*").in('raw_id', courses_ids); if (error) throw error; else { - const timetableData = createTimetableFromCourses(data!); + const timetableData = createTimetableFromCourses(data!, theme); const icss = ics.createEvents(timetableData.map(course => { const start = zonedTimeToUtc(parse( scheduleTimeSlots[course.startTime]!.start, diff --git a/src/app/[lang]/timetable/view/page.tsx b/src/app/[lang]/timetable/view/page.tsx index b57fd247..232ca01e 100644 --- a/src/app/[lang]/timetable/view/page.tsx +++ b/src/app/[lang]/timetable/view/page.tsx @@ -6,11 +6,13 @@ import { useSearchParams } from 'next/navigation' import supabase from "@/config/supabase"; import useSWR from "swr"; import { createTimetableFromCourses, timetableColors } from "@/helpers/timetable"; +import { useSettings } from "@/hooks/contexts/settings"; const ViewTimetablePage: NextPage = () => { const router = useRouter(); const searchParams = useSearchParams(); const courseCodes = searchParams.get('semester_1121')?.split(','); + const { timetableTheme } = useSettings(); if(!courseCodes) router.back(); @@ -22,7 +24,7 @@ const ViewTimetablePage: NextPage = () => { return data; }) - const timetableData = courses? createTimetableFromCourses(courses) : []; + const timetableData = courses? createTimetableFromCourses(courses, timetableTheme) : []; return (
@@ -30,7 +32,7 @@ const ViewTimetablePage: NextPage = () => {
{courses && courses.map((course, index) => (
-
+
{course.name_zh} {course.name_en} diff --git a/src/components/Alerts/ThemeChangableAlert.tsx b/src/components/Alerts/ThemeChangableAlert.tsx new file mode 100644 index 00000000..78a0d210 --- /dev/null +++ b/src/components/Alerts/ThemeChangableAlert.tsx @@ -0,0 +1,37 @@ +import useDictionary from "@/dictionaries/useDictionary"; +import { Alert, Button, IconButton } from "@mui/joy" +import Link from "next/link"; +import React from "react"; +import { Info, X } from "react-feather" +import { useLocalStorage } from "usehooks-ts" + +const ThemeChangableAlert = () => { + const [open, setOpen] = useLocalStorage('theme_changable_alert', true); + const dict = useDictionary(); + + if(!open) return <>; + + return + } + endDecorator={ + + + + + setOpen(false)}> + + + + } + > + {dict.alerts.TimetableCourseList.text} + +} + +export default ThemeChangableAlert \ No newline at end of file diff --git a/src/components/Timetable/TimetableCourseList.tsx b/src/components/Timetable/TimetableCourseList.tsx index 5107c8d2..2685dff2 100644 --- a/src/components/Timetable/TimetableCourseList.tsx +++ b/src/components/Timetable/TimetableCourseList.tsx @@ -1,4 +1,4 @@ -import { Button, ButtonGroup, DialogContent, DialogTitle, IconButton, Input, ModalClose, ModalDialog } from '@mui/joy'; +import { Alert, Button, ButtonGroup, DialogContent, DialogTitle, IconButton, Input, ModalClose, ModalDialog } from '@mui/joy'; import { Calendar, Download, EyeOff, Image, Mail, Search, Share, Trash } from 'react-feather'; import { QRCodeSVG } from 'qrcode.react'; import { useSettings } from '@/hooks/contexts/settings'; @@ -7,6 +7,7 @@ import { useRouter } from 'next/navigation'; import { useModal } from '@/hooks/contexts/useModal'; import CourseSearchbar from './CourseSearchbar'; import { timetableColors } from '@/helpers/timetable'; +import ThemeChangableAlert from '../Alerts/ThemeChangableAlert'; const TimetableCourseList = () => { const { courses, setCourses } = useSettings(); @@ -54,7 +55,7 @@ const TimetableCourseList = () => {
`} + href={`mailto:?subject=Here is My Timetable&body=My Timetable can be found on NTHUMODS at ${shareLink}`} target='_blank' variant="outlined" startDecorator={} @@ -138,6 +139,7 @@ const TimetableCourseList = () => {
)} +
diff --git a/src/dictionaries/en.json b/src/dictionaries/en.json index e8ed4aab..cf88620e 100644 --- a/src/dictionaries/en.json +++ b/src/dictionaries/en.json @@ -68,9 +68,19 @@ "language": { "title": "Language", "description": "Select your preferred language." + }, + "theme": { + "title": "Theme", + "description": "Personalize your timetable to make it yours." } }, "venues": { "placeholder": "Select a Venue from the list" + }, + "alerts": { + "TimetableCourseList": { + "action": "Go Settings", + "text": "You can change your theme in the settings page!" + } } } \ No newline at end of file diff --git a/src/dictionaries/zh.json b/src/dictionaries/zh.json index 5e79a34f..afa37170 100644 --- a/src/dictionaries/zh.json +++ b/src/dictionaries/zh.json @@ -68,9 +68,19 @@ "language": { "title": "語言 Language", "description": "選擇您想要的語言。" + }, + "theme": { + "title": "時間表顔色", + "description": "客制化你的時間表的顔色,讓你的時間表更有個性!" } }, "venues": { "placeholder": "Select a Venue from the list" + }, + "alerts": { + "TimetableCourseList": { + "action": "Go Settings", + "text": "You can change your theme in the settings page!" + } } } \ No newline at end of file diff --git a/src/helpers/timetable.ts b/src/helpers/timetable.ts index 95083d50..276fdd23 100644 --- a/src/helpers/timetable.ts +++ b/src/helpers/timetable.ts @@ -2,7 +2,115 @@ import { CourseDefinition } from "@/config/supabase"; import { scheduleTimeSlots } from "@/const/timetable"; import { CourseTimeslotData } from "@/types/timetable"; -export const timetableColors = { +export const timetableColors: { [theme: string]: string[] } = { + 'harmonyBlossom': [ + "#855EBE", // Orchid Whisper + "#D65DB1", // Pink Dahlia + "#FF6F91", // Blossom Pink + "#FF9573", // Coral Charm + "#FFC85B", // Sunlit Petal + "#A9BC5D", // Meadow Glow + "#5DA671", // Fresh Fern + "#22907B", // Turquoise Dream + "#1C6873", // Deep Lagoon + "#2F4959", // Midnight Sapphire + ], + 'autumnSunset': [ + "#844C2A", // Maple Brown + "#D65631", // Fiery Pumpkin + "#FF7F3D", // Sunset Blaze + "#FFA354", // Harvest Gold + "#FFCE6C", // Amber Glow + "#A8B964", // Olive Grove + "#5C9053", // Forest Green + "#207A69", // Teal Twilight + "#1C6472", // Twilight Blue + "#2F4A59", // Dusk Navy + ], + 'oceanBreeze': [ + "#2A758C", // Deep Sea Blue + "#316ED6", // Azure Waters + "#3D88FF", // Oceanic Blue + "#54A0FF", // Calm Surf + "#6CBEFF", // Gentle Breeze + "#64B9A8", // Coastal Green + "#53A061", // Seafoam Delight + "#427F27", // Emerald Shore + "#355A22", // Seaside Moss + "#2A472F", // Midnight Tide + ], + 'springMeadow': [ + "#688456", // Fresh Grass + "#6ED663", // Zesty Lime + "#80FF70", // Spring Green + "#A2FF8E", // Vibrant Meadow + "#C9FFAB", // Lush Pasture + "#B3C35E", // Sunny Field + "#94A72F", // Meadowland + "#7B8E1E", // Golden Sun + "#68721A", // Honeydew Gold + "#4A5A2F", // Mossy Path + ], + 'sunsetWarmth': [ + "#8C5F2A", // Amber Haze + "#D68431", // Tangerine Sunset + "#FF9B3D", // Warm Embrace + "#FFB854", // Sunlit Copper + "#FFD06C", // Golden Radiance + "#BCAB60", // Amber Fields + "#7E762F", // Sunflower Gold + "#55782D", // Olive Harvest + "#48671A", // Burnished Bronze + "#3C4D2F", // Rustic Ember + ], + 'roseGarden': [ + "#8C5E61", // Dusty Rose + "#D65D75", // Rose Petal + "#FF6F8F", // Blush Pink + "#FF969D", // Rosy Cheeks + "#FFC76C", // Sun-kissed Rose + "#B99D61", // Antique Rose + "#7E625E", // Mauve Dream + "#8E4C75", // Orchid Mist + "#7A3673", // Violet Haze + "#5D2D59", // Midnight Rose + ], + 'sapphireTwilight': [ + "#59618C", // Twilight Blue + "#734DD6", // Sapphire Gem + "#9181FF", // Evening Sky + "#A89EFF", // Starry Horizon + "#C6C5FF", // Lavender Dusk + "#B5B4A8", // Misty Lake + "#7F8F61", // Enchanted Forest + "#4E7627", // Emerald Moon + "#2F712A", // Pine Grove + "#292F47", // Midnight Sapphire + ], + 'goldenHarbor': [ + "#8C752A", // Sunlit Sand + "#D68F31", // Golden Shores + "#FFA63D", // Amber Harbor + "#FFBC54", // Glistening Gold + "#FFD36C", // Radiant Bay + "#B8A461", // Coastal Pebble + "#8E842F", // Lighthouse Glow + "#6D7427", // Warm Marina + "#6F591A", // Sunlit Deck + "#4F432F", // Nautical Bronze + ], + 'plumElegance': [ + "#8C5E81", // Plum Charm + "#D65D96", // Lavender Mist + "#FF6FAA", // Radiant Orchid + "#FF96B9", // Velvet Blossom + "#FFC76C", // Golden Plum + "#B88761", // Peach Blossom + "#8E5C6E", // Mauve Serenity + "#753B8E", // Twilight Rose + "#6B1A87", // Royal Amethyst + "#4F2D4A", // Midnight Plum + ], 'pastelColors': [ "#ffffed", // lavender "#fff5ee", // seashell @@ -30,7 +138,7 @@ export const timetableColors = { } -export const createTimetableFromCourses = (data: CourseDefinition[]) => { +export const createTimetableFromCourses = (data: CourseDefinition[], theme = 'tsinghuarian') => { const newTimetableData: CourseTimeslotData[] = []; data!.forEach(course => { //get unique days first @@ -46,7 +154,7 @@ export const createTimetableFromCourses = (data: CourseDefinition[]) => { const startTime = Math.min(...times); const endTime = Math.max(...times); //get the color - const color = timetableColors['tsinghuarian'][data!.indexOf(course)]; + const color = timetableColors[theme][data!.indexOf(course)]; //push to scheduleData newTimetableData.push({ course: course, diff --git a/src/hooks/contexts/settings.tsx b/src/hooks/contexts/settings.tsx index 3d3ff7e9..80fa0c42 100644 --- a/src/hooks/contexts/settings.tsx +++ b/src/hooks/contexts/settings.tsx @@ -4,15 +4,18 @@ import { FC, PropsWithChildren, createContext, useContext, useEffect, useMemo, u import { useLocalStorage } from 'usehooks-ts'; import { useCookies } from 'react-cookie'; import { Language, SettingsType } from "@/types/settings"; +import type { timetableColors } from "@/helpers/timetable"; const settingsContext = createContext>({ language: "zh", darkMode: false, courses: [], + timetableTheme: "tsinghuarian", setSettings: () => {}, setCourses: () => {}, setLanguage: () => {}, - setDarkMode: () => {} + setDarkMode: () => {}, + setTimetableTheme: () => {} }); const useSettingsProvider = () => { @@ -21,6 +24,7 @@ const useSettingsProvider = () => { const pathname = usePathname(); const [cookies, setCookie, removeCookie] = useCookies(['theme']); const [courses, setCourses] = useLocalStorage("semester_1121", []); + const [timetableTheme, setTimetableTheme] = useLocalStorage("timetable_theme", "tsinghuarian"); const setSettings = (settings: SettingsType) => { setCourses(settings.courses); @@ -61,10 +65,12 @@ const useSettingsProvider = () => { language, darkMode, courses, + timetableTheme, setSettings, setCourses, setLanguage, - setDarkMode + setDarkMode, + setTimetableTheme }; } diff --git a/src/hooks/useUserTimetable.tsx b/src/hooks/useUserTimetable.tsx index 8190cde6..839bfb8e 100644 --- a/src/hooks/useUserTimetable.tsx +++ b/src/hooks/useUserTimetable.tsx @@ -6,7 +6,7 @@ import { createTimetableFromCourses } from "@/helpers/timetable"; import { CourseTimeslotData } from "@/types/timetable"; const useUserTimetable = (loadCourse = true) => { - const { courses, setCourses } = useSettings(); + const { courses, timetableTheme, setCourses } = useSettings(); const [localCourseCache, setLocalCourseCache] = useLocalStorage("cached_courses", []); const [timetableData, setTimetableData] = useState([]); @@ -16,7 +16,7 @@ const useUserTimetable = (loadCourse = true) => { if(!loadCourse) return; console.info('Loading from Cache') setAllCourseData(localCourseCache!); - setTimetableData(createTimetableFromCourses(localCourseCache)); + setTimetableData(createTimetableFromCourses(localCourseCache, timetableTheme)); }, []); useEffect(() => { @@ -28,7 +28,7 @@ const useUserTimetable = (loadCourse = true) => { if(error) console.error(error); else { setAllCourseData(data!); - setTimetableData(createTimetableFromCourses(data!)); + setTimetableData(createTimetableFromCourses(data!, timetableTheme)); setLocalCourseCache(data!); } } catch(e) {