diff --git a/database/indexes/courses.sql b/database/indexes/courses.sql
index 336a5ed5..55ae0cbc 100644
--- a/database/indexes/courses.sql
+++ b/database/indexes/courses.sql
@@ -6,6 +6,7 @@ create index courses_teacher_en ON courses USING pgroonga(teacher_en);
create index courses_department ON courses USING pgroonga(department);
create index courses_course ON courses USING pgroonga(course);
create index courses_raw_id ON courses USING pgroonga(raw_id);
+create index courses_keywords ON course_syllabus USING pgroonga(keywords);
CREATE OR REPLACE FUNCTION search_courses(keyword text)
RETURNS SETOF courses AS
@@ -26,4 +27,27 @@ BEGIN
END IF;
END
$func$
+LANGUAGE plpgsql;
+
+DROP FUNCTION search_courses_with_syllabus;
+
+CREATE OR REPLACE FUNCTION search_courses_with_syllabus(keyword text)
+RETURNS SETOF courses_with_syllabus AS
+$func$
+BEGIN
+ IF trim(keyword) = '' THEN
+ -- Keyword is blank, return all courses
+ RETURN QUERY SELECT * FROM courses_with_syllabus;
+ ELSE
+ -- Keyword is not blank, perform the search
+ RETURN QUERY SELECT * FROM courses_with_syllabus
+ WHERE name_zh &@~ keyword
+ OR teacher_zh &@~ keyword
+ OR venues &@~ keyword
+ OR name_en &@~ keyword
+ OR teacher_en &@~ keyword
+ OR raw_id &@ keyword;
+ END IF;
+END
+$func$
LANGUAGE plpgsql;
\ No newline at end of file
diff --git a/database/views/courses_with_syllabus.sql b/database/views/courses_with_syllabus.sql
new file mode 100644
index 00000000..861e4264
--- /dev/null
+++ b/database/views/courses_with_syllabus.sql
@@ -0,0 +1,7 @@
+CREATE OR REPLACE VIEW courses_with_syllabus AS
+SELECT
+ c.*,
+ cs.brief,
+ cs.keywords
+FROM courses c
+LEFT JOIN course_syllabus cs ON c.raw_id = cs.raw_id;
diff --git a/src/app/[lang]/courses/[courseId]/DownloadSyllabus.tsx b/src/app/[lang]/courses/[courseId]/DownloadSyllabus.tsx
new file mode 100644
index 00000000..4ce15365
--- /dev/null
+++ b/src/app/[lang]/courses/[courseId]/DownloadSyllabus.tsx
@@ -0,0 +1,22 @@
+'use client';
+import supabase from "@/config/supabase";
+import { RawCourseID } from "@/types/courses";
+import { Button } from "@mui/joy";
+import { DownloadCloud } from "react-feather";
+
+const DownloadSyllabus = ({ courseId }: { courseId: RawCourseID }) => {
+ const handleDownload = async () => {
+
+ const fileURL = `${process.env.NEXT_PUBLIC_SUPABASE_URL}storage/v1/object/public/syllabus/${encodeURIComponent(courseId)}.pdf`;
+ const link = document.createElement('a')
+ link.href = fileURL
+ link.setAttribute('download', courseId+'.pdf')
+ document.body.appendChild(link)
+ link.click()
+ link.remove()
+ }
+
+ return } onClick={handleDownload}>Download PDF
+}
+
+export default DownloadSyllabus;
\ No newline at end of file
diff --git a/src/app/[lang]/courses/[courseId]/page.tsx b/src/app/[lang]/courses/[courseId]/page.tsx
index 8991d8f2..c2562a94 100644
--- a/src/app/[lang]/courses/[courseId]/page.tsx
+++ b/src/app/[lang]/courses/[courseId]/page.tsx
@@ -1,11 +1,11 @@
import Fade from "@/components/Animation/Fade";
import { getDictionary } from "@/dictionaries/dictionaries";
-import {getCourse, getCoursePTTReview} from '@/lib/course';
+import {getCourse, getCoursePTTReview, getCourseWithSyllabus} from '@/lib/course';
import { LangProps } from "@/types/pages";
import {Accordion, AccordionDetails, AccordionGroup, AccordionSummary, Alert, Chip, Divider, Button} from '@mui/joy';
import { format } from "date-fns";
import { ResolvingMetadata } from "next";
-import {AlertTriangle, Minus, Plus} from 'react-feather';
+import {AlertTriangle, DownloadCloud, Minus, Plus} from 'react-feather';
import { redirect } from 'next/navigation'
import CourseTagList from "@/components/Courses/CourseTagsList";
import {useSettings} from '@/hooks/contexts/settings';
@@ -14,6 +14,7 @@ import SelectCourseButton from '@/components/Courses/SelectCourseButton';
import { createTimetableFromCourses } from "@/helpers/timetable";
import Timetable from "@/components/Timetable/Timetable";
import { MinimalCourse } from "@/types/courses";
+import DownloadSyllabus from "./DownloadSyllabus";
type PageProps = {
params: { courseId? : string }
@@ -29,7 +30,7 @@ export async function generateMetadata({ params }: PageProps, parent: ResolvingM
const CourseDetailPage = async ({ params }: PageProps & LangProps) => {
const courseId = decodeURI(params.courseId as string);
- const course = await getCourse(courseId);
+ const course = await getCourseWithSyllabus(courseId);
const reviews = await getCoursePTTReview(courseId);
const dict = await getDictionary(params.lang);
@@ -71,9 +72,16 @@ const CourseDetailPage = async ({ params }: PageProps & LangProps) => {
+
+
Brief
+
{course.course_syllabus.brief}
+
{dict.course.details.description}
-
{course?.note}
+
+ {course.course_syllabus.content ?? <>
+
+ >}
{course?.prerequisites &&
儅修
@@ -101,9 +109,13 @@ const CourseDetailPage = async ({ params }: PageProps & LangProps) => {
}
+
{dict.course.details.restrictions}
-
{course.restrictions}
+
{course.restrictions ?? "無"}
{dict.course.details.compulsory}
diff --git a/src/app/[lang]/courses/page.tsx b/src/app/[lang]/courses/page.tsx
index 142fdc51..010cc484 100644
--- a/src/app/[lang]/courses/page.tsx
+++ b/src/app/[lang]/courses/page.tsx
@@ -1,7 +1,7 @@
'use client';;
import CourseListItem from "@/components/Courses/CourseListItem";
import InputControl from "@/components/FormComponents/InputControl";
-import supabase, { CourseDefinition } from "@/config/supabase";
+import supabase, {CourseDefinition, CourseSyllabusView} from '@/config/supabase';
import { Button, CircularProgress, Divider, Drawer, IconButton, LinearProgress, Stack } from "@mui/joy";
import { NextPage } from "next";
import { useEffect, useState, Fragment, useRef, use, useMemo } from "react";
@@ -56,7 +56,7 @@ const emptyFilters: RefineControlFormTypes = {
const CoursePage: NextPage = () => {
const dict = useDictionary();
- const [courses, setCourses] = useState
([]);
+ const [courses, setCourses] = useState([]);
const [loading, setLoading] = useState(true);
const [totalCount, setTotalCount] = useState(0);
const [headIndex, setHeadIndex] = useState(0);
@@ -134,7 +134,7 @@ const CoursePage: NextPage = () => {
setLoading(true);
//Query for courses
try {
- let temp = supabase.rpc('search_courses', {
+ let temp = supabase.rpc('search_courses_with_syllabus', {
keyword: filters.textSearch,
}, { count: 'exact' })
if (filters.level.length)
@@ -196,7 +196,7 @@ const CoursePage: NextPage = () => {
if (error) console.error(error);
else {
console.log(courses)
- setCourses(courses as CourseDefinition[]);
+ setCourses(courses as CourseSyllabusView[]);
setHeadIndex(index);
}
}
diff --git a/src/components/Courses/CourseListItem.tsx b/src/components/Courses/CourseListItem.tsx
index 119261bc..3cb14fea 100644
--- a/src/components/Courses/CourseListItem.tsx
+++ b/src/components/Courses/CourseListItem.tsx
@@ -1,5 +1,5 @@
'use client';
-import { CourseDefinition } from '@/config/supabase';
+import {CourseDefinition, CourseSyllabusView} from '@/config/supabase';
import useDictionary from '@/dictionaries/useDictionary';
import { useSettings } from '@/hooks/contexts/settings';
import { Button, Tooltip } from '@mui/joy';
@@ -9,8 +9,7 @@ import Link from 'next/link';
import CourseTagList from './CourseTagsList';
import SelectCourseButton from './SelectCourseButton';
-
-const CourseListItem: FC<{ course: CourseDefinition }> = ({ course }) => {
+const CourseListItem: FC<{ course: CourseSyllabusView }> = ({ course }) => {
const dict = useDictionary();
return
@@ -21,6 +20,7 @@ const CourseListItem: FC<{ course: CourseDefinition }> = ({ course }) => {
{course.name_en} - {(course.teacher_en ?? []).join(',')}
+
{course.brief}
{course.restrictions}
{course.note}
{course.prerequisites &&
@@ -40,8 +40,8 @@ const CourseListItem: FC<{ course: CourseDefinition }> = ({ course }) => {
No Venues
}
-
-
+
+
diff --git a/src/config/supabase.ts b/src/config/supabase.ts
index 86efe8ec..df993178 100644
--- a/src/config/supabase.ts
+++ b/src/config/supabase.ts
@@ -4,6 +4,9 @@ import { createClient } from "@supabase/supabase-js";
const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL ?? "", process.env.NEXT_PUBLIC_SUPABASE_KEY ?? "");
export type CourseDefinition = Database['public']['Tables']['courses']['Row'];
+export type CourseSyllabusDefinition = Database['public']['Tables']['course_syllabus']['Row'];
+export type CourseJoinWithSyllabus = CourseDefinition & { course_syllabus: CourseSyllabusDefinition };
+export type CourseSyllabusView = Database['public']['Views']['courses_with_syllabus']['Row'];
export type AlertDefinition = Database['public']['Tables']['alerts']['Row'];
export type BusScheduleDefinition = Database['public']['Tables']['bus_schedule']['Row'];
export type CdsCourseDefinition = Database['public']['Tables']['cds_courses']['Row'];
diff --git a/src/hooks/useUserTimetable.tsx b/src/hooks/useUserTimetable.tsx
index 74540480..a3329a9f 100644
--- a/src/hooks/useUserTimetable.tsx
+++ b/src/hooks/useUserTimetable.tsx
@@ -10,7 +10,7 @@ const useUserTimetable = (loadCourse = true) => {
const { courses, timetableTheme, setCourses } = useSettings();
const { data: allCourseData = [], error, isLoading } = useSWR(['courses', courses], async ([table, courseCodes]) => {
- const { data = [], error } = await supabase.from('courses').select("*").in('raw_id', courseCodes);
+ const { data = [], error } = await supabase.from('courses_with_syllabus').select("*").in('raw_id', courseCodes);
if(error) throw error;
if(!data) throw new Error('No data');
return data;
diff --git a/src/lib/course.ts b/src/lib/course.ts
index 0e027919..eaac1486 100644
--- a/src/lib/course.ts
+++ b/src/lib/course.ts
@@ -1,5 +1,25 @@
import supabase from '@/config/supabase';
import { parse } from 'node-html-parser';
+import {CourseJoinWithSyllabus} from '@/config/supabase';
+
+
+export const getCourseWithSyllabus = async (courseId: string) => {
+ const { data, error } = await supabase
+ .from('courses')
+ .select(`
+ *,
+ course_syllabus (
+ *
+ )
+ `)
+ .eq('raw_id', courseId);
+ if(error) {
+ console.error(error)
+ return null;
+ }
+ else return data![0] as unknown as CourseJoinWithSyllabus;
+}
+
export const getCourse = async (courseId: string) => {
const { data, error } = await supabase.from('courses').select('*').eq('raw_id', courseId);
@@ -10,6 +30,19 @@ export const getCourse = async (courseId: string) => {
else return data![0];
}
+
+export const getMinimalCourse = async (courseId: string) => {
+ const { data, error } = await supabase
+ .from('courses')
+ .select('raw_id, name_zh, name_en, department, course, class, credits, venues, times, teacher_zh, language')
+ .eq('raw_id', courseId);
+ if(error) {
+ console.error(error)
+ return null;
+ }
+ else return data![0];
+}
+
export const getCoursePTTReview = async (courseId: string) => {
const course = await getCourse(courseId);
//TODO: use better search to find the posts
diff --git a/src/types/supabase.ts b/src/types/supabase.ts
index 273cadc2..3d435fd1 100644
--- a/src/types/supabase.ts
+++ b/src/types/supabase.ts
@@ -604,6 +604,12 @@ export interface Database {
columns: ["raw_id"]
referencedRelation: "courses"
referencedColumns: ["raw_id"]
+ },
+ {
+ foreignKeyName: "course_syllabus_raw_id_fkey"
+ columns: ["raw_id"]
+ referencedRelation: "courses_with_syllabus"
+ referencedColumns: ["raw_id"]
}
]
}
@@ -783,6 +789,51 @@ export interface Database {
}
}
Views: {
+ courses_with_syllabus: {
+ Row: {
+ brief: string | null
+ capacity: number | null
+ class: string | null
+ closed_mark: string | null
+ compulsory_for: string[] | null
+ content: string | null
+ course: string | null
+ credits: number | null
+ cross_discipline: string[] | null
+ department: string | null
+ elective_for: string[] | null
+ first_specialization: string[] | null
+ ge_target: string | null
+ ge_type: string | null
+ has_file: boolean | null
+ keywords: string | null
+ language: string | null
+ name_en: string | null
+ name_zh: string | null
+ no_extra_selection: boolean | null
+ note: string | null
+ prerequisites: string | null
+ raw_1_2_specialization: string | null
+ raw_compulsory_elective: string | null
+ raw_cross_discipline: string | null
+ raw_extra_selection: string | null
+ raw_id: string | null
+ raw_teacher_en: string | null
+ raw_teacher_zh: string | null
+ raw_time: string | null
+ raw_venue: string | null
+ reserve: number | null
+ restrictions: string | null
+ second_specialization: string[] | null
+ semester: string | null
+ tags: string[] | null
+ teacher_en: string[] | null
+ teacher_zh: string[] | null
+ times: string[] | null
+ venues: string[] | null
+ }
+ Relationships: []
+ }
distinct_classes: {
Row: {
class: string | null
@@ -1415,6 +1466,53 @@ export interface Database {
venues: string[]
}[]
}
+ search_courses_with_syllabus: {
+ Args: {
+ keyword: string
+ }
+ Returns: {
+ brief: string | null
+ capacity: number | null
+ class: string | null
+ closed_mark: string | null
+ compulsory_for: string[] | null
+ content: string | null
+ course: string | null
+ credits: number | null
+ cross_discipline: string[] | null
+ department: string | null
+ elective_for: string[] | null
+ first_specialization: string[] | null
+ ge_target: string | null
+ ge_type: string | null
+ has_file: boolean | null
+ keywords: string | null
+ language: string | null
+ name_en: string | null
+ name_zh: string | null
+ no_extra_selection: boolean | null
+ note: string | null
+ prerequisites: string | null
+ raw_1_2_specialization: string | null
+ raw_compulsory_elective: string | null
+ raw_cross_discipline: string | null
+ raw_extra_selection: string | null
+ raw_id: string | null
+ raw_teacher_en: string | null
+ raw_teacher_zh: string | null
+ raw_time: string | null
+ raw_venue: string | null
+ reserve: number | null
+ restrictions: string | null
+ second_specialization: string[] | null
+ semester: string | null
+ tags: string[] | null
+ teacher_en: string[] | null
+ teacher_zh: string[] | null
+ times: string[] | null
+ venues: string[] | null
+ }[]
+ }
set_limit: {
Args: {
"": number