Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@
<link rel="icon" type="image/png" href="/logo_square.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ECE Pathmaker</title>
<meta name="description" content="ECEs, plan your courses with drag-and-drop - no loading and no fuss" />
<link rel="canonical" href="https://ecepathmaker.com/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="ECE Pathmaker" />
<meta property="og:title" content="ECE Pathmaker" />
<meta property="og:description" content="ECEs, plan your courses with drag-and-drop - no loading and no fuss" />
<meta property="og:url" content="https://ecepathmaker.com/" />
<meta property="og:image" content="https://ecepathmaker.com/og/default.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="ECE Pathmaker" />
<meta name="twitter:description" content="ECEs, plan your courses with drag-and-drop - no loading and no fuss" />
<meta name="twitter:image" content="https://ecepathmaker.com/og/default.png" />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#379683" />
</head>
<body>
<div id="root"></div>
Expand Down
9 changes: 8 additions & 1 deletion jest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import "@testing-library/jest-dom";
import type { ReactNode } from "react";

jest.mock('lucide-react');
jest.mock('lucide-react');

// react-helmet-async needs a HelmetProvider in the tree; stub it out for tests
jest.mock('react-helmet-async', () => ({
Helmet: () => null,
HelmetProvider: ({ children }: { children: ReactNode }) => children,
}));
45 changes: 44 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"react": "^18.3.1",
"react-colorful": "^5.6.1",
"react-dom": "^18.3.1",
"react-helmet-async": "^3.0.0",
"react-icons": "^5.4.0",
"react-router-dom": "^7.1.1",
"tailwind-merge": "^2.6.0",
Expand Down Expand Up @@ -62,6 +63,7 @@
"ts-node": "^10.9.2",
"typescript": "~5.6.2",
"typescript-eslint": "^8.18.2",
"vite": "^6.0.5"
"vite": "^6.0.5",
"vite-plugin-sitemap": "^0.8.2"
}
}
Binary file added public/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions public/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "ECE Pathmaker",
"short_name": "Pathmaker",
"description": "ECEs, plan your courses with drag-and-drop - no loading and no fuss",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#379683",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
]
}
Binary file added public/og/default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
User-agent: *
Allow: /

Sitemap: https://ecepathmaker.com/sitemap.xml
2 changes: 1 addition & 1 deletion src/components/forms/CourseForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ const CourseForm: FC<CourseFormProps> = ({
};

const [justSubmitted, setJustSubmitted] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout>();
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();

useEffect(() => {
if (justSubmitted) {
Expand Down
22 changes: 22 additions & 0 deletions src/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,27 @@ import { emptyGrid } from "../../utils/emptyGrid";
import mockCourses from "../../data/mockCourses";
import { saveCourses, saveCoursesOnGrid, saveCoursesUsed, saveDependencies, saveLayouts } from "../../firebase/firestore";
import { auth } from "../../firebase/firebase";
import Seo from "../seo/Seo";
import { SITE_DESCRIPTION, SITE_NAME, SITE_URL } from "../../lib/site";

const siteJsonLd: Record<string, unknown>[] = [
{
"@context": "https://schema.org",
"@type": "WebApplication",
name: SITE_NAME,
url: SITE_URL,
description: SITE_DESCRIPTION,
applicationCategory: "EducationalApplication",
operatingSystem: "Web",
offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
},
{
"@context": "https://schema.org",
"@type": "WebSite",
name: SITE_NAME,
url: SITE_URL,
},
];

interface LayoutProps {
children: ReactNode;
Expand Down Expand Up @@ -121,6 +142,7 @@ const Layout: FC<LayoutProps> = ({ children }) => {
setSavedLayouts,
}}
>
<Seo jsonLd={siteJsonLd} />
<div className="min-h-screen flex flex-col w-max md:w-full dark:bg-gray-700">
<div className="h-24" />
<Navbar />
Expand Down
63 changes: 63 additions & 0 deletions src/components/seo/Seo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Helmet } from "react-helmet-async";
import {
DEFAULT_OG_IMAGE,
SITE_DESCRIPTION,
SITE_NAME,
SITE_URL,
} from "../../lib/site";

interface SeoProps {
title?: string;
description?: string;
path?: string;
image?: string;
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
noindex?: boolean;
}

const Seo = ({
title,
description,
path,
image,
jsonLd,
noindex,
}: SeoProps) => {
const fullTitle = title ? `${title} - ${SITE_NAME}` : SITE_NAME;
const metaDescription = description ?? SITE_DESCRIPTION;
const canonical = `${SITE_URL}${path ?? "/"}`;
const ogImage = image ?? DEFAULT_OG_IMAGE;
const absoluteImage = ogImage.startsWith("http")
? ogImage
: `${SITE_URL}${ogImage}`;

return (
<Helmet>
<title>{fullTitle}</title>
<meta name="description" content={metaDescription} />
{noindex && <meta name="robots" content="noindex" />}

<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={metaDescription} />
<meta property="og:type" content="website" />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={absoluteImage} />
<meta property="og:site_name" content={SITE_NAME} />

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={metaDescription} />
<meta name="twitter:image" content={absoluteImage} />

<link rel="canonical" href={canonical} />

{jsonLd && (
<script type="application/ld+json">
{JSON.stringify(jsonLd).replace(/</g, "\\u003c")}
</script>
)}
</Helmet>
);
};

export default Seo;
5 changes: 5 additions & 0 deletions src/lib/site.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const SITE_URL = "https://ecepathmaker.com";
export const SITE_NAME = "ECE Pathmaker";
export const SITE_DESCRIPTION =
"ECEs, plan your courses with drag-and-drop - no loading and no fuss";
export const DEFAULT_OG_IMAGE = "/og/default.png";
9 changes: 6 additions & 3 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { AuthProvider } from './contexts/AuthContext.tsx';
import { HelmetProvider } from 'react-helmet-async';

const storedTheme = localStorage.getItem('theme');
if (storedTheme === 'dark') {
Expand All @@ -13,8 +14,10 @@ if (storedTheme === 'dark') {

createRoot(document.getElementById('root')!).render(
<StrictMode>
<AuthProvider>
<App />
</AuthProvider>
<HelmetProvider>
<AuthProvider>
<App />
</AuthProvider>
</HelmetProvider>
</StrictMode>,
)
22 changes: 22 additions & 0 deletions src/pages/Courses.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ import { createPortal } from "react-dom";
import { FilterState } from "../types/types";
import { mockCourses } from "../utils/dataImports";
import { CourseCard, Filter } from "../utils/componentImports";
import Seo from "../components/seo/Seo";
import { SITE_URL } from "../lib/site";

const coursesJsonLd: Record<string, unknown> = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "Home", item: SITE_URL },
{
"@type": "ListItem",
position: 2,
name: "Courses",
item: `${SITE_URL}/courses`,
},
],
};

const Courses = () => {
const sensors = useSensors(
Expand Down Expand Up @@ -99,6 +115,12 @@ const Courses = () => {

return (
<div>
<Seo
title="Courses"
path="/courses"
description="Browse and filter UofT ECE courses by stream, term, and category"
jsonLd={coursesJsonLd}
/>
<h2 className="mb-8 text-2xl font-medium dark:text-gray-50">
🗂️ Explore and rearrange courses (all the courses on Magellan and more)!
</h2>
Expand Down
17 changes: 17 additions & 0 deletions src/pages/FAQ.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { FC, useEffect, useState } from 'react';
import { siteFaqData, courseFaqData } from "../data/faqData";
import { FAQCard } from "../utils/componentImports";
import Seo from "../components/seo/Seo";
import { SITE_URL } from "../lib/site";

const faqJsonLd: Record<string, unknown> = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "Home", item: SITE_URL },
{ "@type": "ListItem", position: 2, name: "FAQ", item: `${SITE_URL}/faq` },
],
};

const FAQ: FC = () => {
const [showButton, setShowButton] = useState(false);
Expand Down Expand Up @@ -29,6 +40,12 @@ const FAQ: FC = () => {

return (
<div className="dark:text-gray-50">
<Seo
title="FAQ"
path="/faq"
description="FAQs about course selection (I asked the registrar) and how to use ECE Pathmaker"
jsonLd={faqJsonLd}
/>
<div className="bg-neutral2 dark:bg-slate-600 p-8 rounded-md text-center shadow-sm">
<h2 className="text-2xl font-bold dark:text-gray-50">FAQs</h2>
<ul className="space-y-2 mt-4">
Expand Down
5 changes: 5 additions & 0 deletions src/pages/Maker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
LoadLayout,
SaveLayout,
} from "../utils/componentImports";
import Seo from "../components/seo/Seo";

const Maker = () => {

Expand All @@ -28,6 +29,10 @@ const Maker = () => {

return (
<div>
<Seo
path="/"
description="UofT Magellan move over, plan your upper-year ECE courses on a drag-and-drop grid that tracks requirements without wait"
/>
<CourseGrid
setCustomInfo={setCustomInfo}
setPreqString={setPreqString}
Expand Down
2 changes: 2 additions & 0 deletions src/pages/NotFound.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { chillguy } from '../utils/assetImports';
import Seo from '../components/seo/Seo';

const NotFound = () => {
return (
<div className="text-xl text-center">
<Seo title="Page not found" path="/404" noindex />
<h1 className="text-4xl font-bold">404 Not Found</h1>
<p className="my-4">The page you are looking for does not exist, but maybe you'll find it in your heart.</p>
<img src={chillguy} alt="Chill Guy" className="w-1/2 mx-auto mt-6" />
Expand Down
Loading
Loading