diff --git a/app/(landing)/page.tsx b/app/(landing)/page.tsx index 2142504..7b4e32e 100644 --- a/app/(landing)/page.tsx +++ b/app/(landing)/page.tsx @@ -11,7 +11,7 @@ import { } from '@heroicons/react/24/solid'; const titlingGothicFB = localFont({ - src: './TitlingGothicFB.woff2', + src: '../fonts/TitlingGothicFB.woff2', display: 'swap', }); diff --git a/app/(main)/compare/page.tsx b/app/(main)/compare/page.tsx index 604fe92..ec0941a 100644 --- a/app/(main)/compare/page.tsx +++ b/app/(main)/compare/page.tsx @@ -9,6 +9,11 @@ import { import fetcher from '@/utils/fetcher'; import roundToTenth from '@/utils/round-to-tenth'; import { EllipsisVerticalIcon, XMarkIcon } from '@heroicons/react/24/solid'; +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Compare', +}; export default async function Page({ searchParams, diff --git a/app/(main)/courses/(page)/page.tsx b/app/(main)/courses/(page)/page.tsx index 1cfb174..8085770 100644 --- a/app/(main)/courses/(page)/page.tsx +++ b/app/(main)/courses/(page)/page.tsx @@ -1,7 +1,12 @@ import { Card } from '@/components/atoms'; import { DepartmentsResponse } from '@/types/core/departments'; +import { Metadata } from 'next'; import Link from 'next/link'; +export const metadata: Metadata = { + title: 'Courses', +}; + export default async function Page() { const data: DepartmentsResponse = await fetch( process.env.BASE_API_URL + '/core/departments', diff --git a/app/(main)/courses/[id]/layout.tsx b/app/(main)/courses/[id]/layout.tsx index 1180f1c..9d2c036 100644 --- a/app/(main)/courses/[id]/layout.tsx +++ b/app/(main)/courses/[id]/layout.tsx @@ -1,6 +1,17 @@ import { BreadcrumbMenu } from '@/components/atoms'; +import { Metadata } from 'next'; import Link from 'next/link'; +export async function generateMetadata({ + params, +}: { + params: { id: string }; +}): Promise { + return { + title: `${params.id}`, + }; +} + export default function Layout({ statistics, schedules, diff --git a/app/(main)/courses/search/page.tsx b/app/(main)/courses/search/page.tsx index 0e8a573..f585902 100644 --- a/app/(main)/courses/search/page.tsx +++ b/app/(main)/courses/search/page.tsx @@ -3,8 +3,13 @@ import { FilterGroup, PaginationBar } from '@/components/molecules'; import { CoursesSearchResponse } from '@/types'; import fetcher from '@/utils/fetcher'; import { EllipsisVerticalIcon, XMarkIcon } from '@heroicons/react/24/solid'; +import { Metadata } from 'next'; import Link from 'next/link'; +export const metadata: Metadata = { + title: 'Courses Search', +}; + export default async function Page({ searchParams, }: { diff --git a/app/(main)/courses/sitemap.ts b/app/(main)/courses/sitemap.ts new file mode 100644 index 0000000..9ead9d1 --- /dev/null +++ b/app/(main)/courses/sitemap.ts @@ -0,0 +1,35 @@ +import { CoursesSearchResponse } from '@/types'; +import fetcher from '@/utils/fetcher'; +import { MetadataRoute } from 'next'; + +const sitemapLinksLimit = 50000; + +export async function generateSitemaps() { + const { total_results } = (await fetcher( + process.env.BASE_API_URL + `/core/courses/search?`, + )) as CoursesSearchResponse; + const numberOfSitemaps = Math.ceil(total_results / sitemapLinksLimit); + return Array.from({ length: numberOfSitemaps }, (_, i) => ({ + id: i, + })); +} + +export default async function sitemap({ + id, +}: { + id: number; +}): Promise { + const requestParams = new URLSearchParams(); + requestParams.append('limit', sitemapLinksLimit.toString()); + requestParams.append('page', (id + 1).toString()); + const { items } = (await fetcher( + process.env.BASE_API_URL + + `/core/courses/search?${requestParams.toString()}`, + )) as CoursesSearchResponse; + return items.map((course) => ({ + url: `${process.env.NEXT_PUBLIC_BASE_URL}/courses/${course.department}-${course.course_number}`, + lastModified: new Date().toISOString(), + changeFrequency: 'monthly', + priority: 0.6, + })); +} diff --git a/app/(main)/professors/(page)/page.tsx b/app/(main)/professors/(page)/page.tsx index 10b791f..2a62735 100644 --- a/app/(main)/professors/(page)/page.tsx +++ b/app/(main)/professors/(page)/page.tsx @@ -1,5 +1,10 @@ import { LastNameDisplay } from '@/app/(main)/professors/(page)/lastname'; import SWRConfigProvider from '@/wrappers/swr-config'; +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Professors', +}; export default function Page() { const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; diff --git a/app/(main)/professors/[id]/Inter_24pt-Bold.ttf b/app/(main)/professors/[id]/Inter_24pt-Bold.ttf new file mode 100644 index 0000000..46b3583 Binary files /dev/null and b/app/(main)/professors/[id]/Inter_24pt-Bold.ttf differ diff --git a/app/(main)/professors/[id]/Inter_24pt-ExtraBold.ttf b/app/(main)/professors/[id]/Inter_24pt-ExtraBold.ttf new file mode 100644 index 0000000..b775c08 Binary files /dev/null and b/app/(main)/professors/[id]/Inter_24pt-ExtraBold.ttf differ diff --git a/app/(main)/professors/[id]/Inter_24pt-Italic.ttf b/app/(main)/professors/[id]/Inter_24pt-Italic.ttf new file mode 100644 index 0000000..1048b07 Binary files /dev/null and b/app/(main)/professors/[id]/Inter_24pt-Italic.ttf differ diff --git a/app/(main)/professors/[id]/layout.tsx b/app/(main)/professors/[id]/layout.tsx index a1daacb..6cc2034 100644 --- a/app/(main)/professors/[id]/layout.tsx +++ b/app/(main)/professors/[id]/layout.tsx @@ -1,6 +1,18 @@ import { BreadcrumbMenu } from '@/components/atoms'; +import { formatName } from '@/utils/format-name'; +import { Metadata } from 'next'; import Link from 'next/link'; +export async function generateMetadata({ + params, +}: { + params: { id: string }; +}): Promise { + return { + title: `${formatName(params.id)}`, + }; +} + export default function Layout({ statistics, schedules, diff --git a/app/(main)/professors/[id]/logo.svg b/app/(main)/professors/[id]/logo.svg new file mode 100644 index 0000000..c9eddf6 --- /dev/null +++ b/app/(main)/professors/[id]/logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/(main)/professors/[id]/opengraph-image.tsx b/app/(main)/professors/[id]/opengraph-image.tsx new file mode 100644 index 0000000..aab089f --- /dev/null +++ b/app/(main)/professors/[id]/opengraph-image.tsx @@ -0,0 +1,131 @@ +import { + ProfessorsIDReviewStatsResponse, + ProfessorsIDSummaryResponse, +} from '@/types'; +import fetcher from '@/utils/fetcher'; +import roundToTenth from '@/utils/round-to-tenth'; +import { ImageResponse } from 'next/og'; + +export const runtime = 'edge'; + +export const alt = 'Professor Page'; +export const size = { + width: 1200, + height: 630, +}; + +export const contentType = 'image/png'; + +export default async function Image({ params }: { params: { id: string } }) { + const { name } = (await fetcher( + process.env.BASE_API_URL + `/core/professors/${params.id}/summary`, + )) as ProfessorsIDSummaryResponse; + const { avg_rating, total_reviews } = (await fetcher( + process.env.BASE_API_URL + `/core/professors/${params.id}/reviews-stats`, + )) as ProfessorsIDReviewStatsResponse; + const review = roundToTenth(avg_rating ?? 0); + + const interItalic = fetch( + new URL('./Inter_24pt-Italic.ttf', import.meta.url), + ).then((res) => res.arrayBuffer()); + const interBold = fetch( + new URL('./Inter_24pt-Bold.ttf', import.meta.url), + ).then((res) => res.arrayBuffer()); + const interExtraBold = fetch( + new URL('./Inter_24pt-ExtraBold.ttf', import.meta.url), + ).then((res) => res.arrayBuffer()); + + return new ImageResponse( + ( +
+

+ {total_reviews} Reviews +

+
+ + + + + +

+ {name} +

+ {review ? ( +

+ {review} + + /5 + +

+ ) : ( +

+ No reviews. Be the first to write one! +

+ )} +
+

+ {total_reviews} Reviews +

+
+ ), + { + ...size, + fonts: [ + { + name: 'Inter-Italic', + data: await interItalic, + style: 'italic', + weight: 400, + }, + { + name: 'Inter-Bold', + data: await interBold, + style: 'normal', + weight: 700, + }, + { + name: 'Inter-ExtraBold', + data: await interExtraBold, + style: 'normal', + weight: 800, + }, + ], + }, + ); +} diff --git a/app/(main)/professors/review/page.tsx b/app/(main)/professors/review/page.tsx index 0fc6929..ae36303 100644 --- a/app/(main)/professors/review/page.tsx +++ b/app/(main)/professors/review/page.tsx @@ -2,9 +2,21 @@ import { Btn, Card, Select, Tag, Textarea } from '@/components/atoms'; import { FilterGroup, SearchBar } from '@/components/molecules'; import { CoursesSearchResponse } from '@/types'; import fetcher from '@/utils/fetcher'; +import { formatName } from '@/utils/format-name'; import { getServerSession } from '@/utils/get-server-session'; +import { Metadata } from 'next'; import { redirect } from 'next/navigation'; +export async function generateMetadata({ + searchParams, +}: { + searchParams: { professor_id: string }; +}): Promise { + return { + title: `Review ${formatName(searchParams.professor_id)}`, + }; +} + export default async function Page({ searchParams, }: { diff --git a/app/(main)/professors/search/page.tsx b/app/(main)/professors/search/page.tsx index ea773b6..4e7261a 100644 --- a/app/(main)/professors/search/page.tsx +++ b/app/(main)/professors/search/page.tsx @@ -3,8 +3,13 @@ import { FilterGroup, PaginationBar } from '@/components/molecules'; import { ProfessorsSearchResponse } from '@/types'; import fetcher from '@/utils/fetcher'; import { EllipsisVerticalIcon, XMarkIcon } from '@heroicons/react/24/solid'; +import { Metadata } from 'next'; import Link from 'next/link'; +export const metadata: Metadata = { + title: 'Professors Search', +}; + export default async function Page({ searchParams, }: { diff --git a/app/(main)/professors/sitemap.ts b/app/(main)/professors/sitemap.ts new file mode 100644 index 0000000..b3c33c4 --- /dev/null +++ b/app/(main)/professors/sitemap.ts @@ -0,0 +1,35 @@ +import { ProfessorsSearchResponse } from '@/types'; +import fetcher from '@/utils/fetcher'; +import { MetadataRoute } from 'next'; + +const sitemapLinksLimit = 50000; + +export async function generateSitemaps() { + const { total_results } = (await fetcher( + process.env.BASE_API_URL + `/core/professors/search?`, + )) as ProfessorsSearchResponse; + const numberOfSitemaps = Math.ceil(total_results / sitemapLinksLimit); + return Array.from({ length: numberOfSitemaps }, (_, i) => ({ + id: i, + })); +} + +export default async function sitemap({ + id, +}: { + id: number; +}): Promise { + const requestParams = new URLSearchParams(); + requestParams.append('limit', sitemapLinksLimit.toString()); + requestParams.append('page', (id + 1).toString()); + const { items } = (await fetcher( + process.env.BASE_API_URL + + `/core/professors/search?${requestParams.toString()}`, + )) as ProfessorsSearchResponse; + return items.map((professor) => ({ + url: `${process.env.NEXT_PUBLIC_BASE_URL}/professors/${professor.id}`, + lastModified: new Date().toISOString(), + changeFrequency: 'monthly', + priority: 0.6, + })); +} diff --git a/app/(main)/profile/page.tsx b/app/(main)/profile/page.tsx index c3cc9dd..783b9ed 100644 --- a/app/(main)/profile/page.tsx +++ b/app/(main)/profile/page.tsx @@ -2,8 +2,13 @@ import { ParamSelect } from '@/components/molecules'; import { Review } from '@/components/organisms'; import { UsersProfileResponse } from '@/types'; import fetcher from '@/utils/fetcher'; +import { Metadata } from 'next'; import { cookies } from 'next/headers'; +export const metadata: Metadata = { + title: 'My Profile', +}; + export default async function Page({ searchParams, }: { diff --git a/app/(main)/schedules/search/page.tsx b/app/(main)/schedules/search/page.tsx index 91536a7..88503e8 100644 --- a/app/(main)/schedules/search/page.tsx +++ b/app/(main)/schedules/search/page.tsx @@ -4,6 +4,11 @@ import { Schedule } from '@/components/organisms'; import { SchedulesSearchResponse } from '@/types'; import fetcher from '@/utils/fetcher'; import { EllipsisVerticalIcon, XMarkIcon } from '@heroicons/react/24/solid'; +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Schedules Search', +}; export default async function Page({ searchParams, diff --git a/app/apple-touch-icon.png b/app/apple-touch-icon.png new file mode 100644 index 0000000..574ed5a Binary files /dev/null and b/app/apple-touch-icon.png differ diff --git a/app/favicon-48x48.png b/app/favicon-48x48.png new file mode 100644 index 0000000..a22adf1 Binary files /dev/null and b/app/favicon-48x48.png differ diff --git a/app/favicon.ico b/app/favicon.ico index 18cf73b..d3d58d2 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/app/favicon.svg b/app/favicon.svg new file mode 100644 index 0000000..ec0699c --- /dev/null +++ b/app/favicon.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/(landing)/TitlingGothicFB.woff2 b/app/fonts/TitlingGothicFB.woff2 similarity index 100% rename from app/(landing)/TitlingGothicFB.woff2 rename to app/fonts/TitlingGothicFB.woff2 diff --git a/app/layout.tsx b/app/layout.tsx index d134643..817ccb1 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,13 +3,14 @@ import { Inter } from 'next/font/google'; import './globals.css'; import { Footer } from '@/components/organisms'; import { cookies } from 'next/headers'; +import { GoogleAnalytics, GoogleTagManager } from '@next/third-parties/google'; const inter = Inter({ subsets: ['latin'] }); export const metadata: Metadata = { title: { - template: '%s | Course Scheduler', - default: 'SJSU Course Scheduler', + template: '%s | Lenses', + default: 'Lenses', }, description: 'Find the best professors and courses at SJSU.', authors: [ @@ -48,6 +49,8 @@ export default function RootLayout({ {children}