├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── ---01-bug-report.yml ├── PULL_REQUEST_TEMPLATE.md └── DISCUSSION_TEMPLATE │ └── ideas.yml ├── public ├── robots.txt └── firebase-messaging-sw.js ├── .gitignore ├── src ├── index.html ├── workbox-sw.js ├── components │ ├── FAB.jsx │ ├── FAB.module.scss │ ├── Button.jsx │ ├── Banner.jsx │ ├── Banner.module.scss │ ├── Checkbox.jsx │ ├── AssignmentForm.module.scss │ ├── Modal.jsx │ ├── Button.module.scss │ ├── Modal.module.scss │ ├── Checkbox.module.scss │ └── AssignmentForm.jsx ├── routes │ ├── api │ │ ├── verify-password │ │ │ └── +get.js │ │ ├── subscribe │ │ │ └── +post.js │ │ └── assignments │ │ │ ├── [id] │ │ │ ├── +delete.js │ │ │ └── +patch.js │ │ │ └── +post.js │ ├── styles.module.scss │ └── +pages.jsx ├── index.css ├── util │ ├── pendingAssignments.js │ ├── finishedAssignments.js │ ├── dateUtils.js │ └── firebase.js └── scss │ └── colors.scss ├── tsconfig.json ├── README.md ├── LICENSE ├── package.json └── CONTRIBUTING.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: geeekyboy -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | lib 5 | .cache 6 | .env 7 | .env.local 8 | .idea 9 | .parcel-cache 10 | .vscode/settings.json 11 | .netlify 12 | /patches 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 💡 Feature Request 4 | url: https://github.com/GeeekyBoy/csed-2024-assignments/discussions/categories/ideas 5 | about: Have an idea for a new feature? Let us know! 6 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": false, 5 | "jsx": "preserve", 6 | "baseUrl": ".", 7 | "types": ["@mango-js/types"], 8 | "outDir": "dist", 9 | }, 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /src/workbox-sw.js: -------------------------------------------------------------------------------- 1 | import { clientsClaim } from "workbox-core"; 2 | import { registerRoute } from "workbox-routing"; 3 | import { NetworkFirst } from "workbox-strategies"; 4 | 5 | self.skipWaiting(); 6 | clientsClaim(); 7 | 8 | registerRoute( 9 | ({ request }) => request.destination !== "", 10 | new NetworkFirst({ 11 | cacheName: "offlineCache", 12 | }) 13 | ); 14 | -------------------------------------------------------------------------------- /src/components/FAB.jsx: -------------------------------------------------------------------------------- 1 | import * as styles from "./FAB.module.scss"; 2 | 3 | function FAB({ children, label, onClick }) { 4 | const handleClick = (e) => { 5 | if (onClick) { 6 | onClick(e); 7 | } 8 | } 9 | return ( 10 | 13 | ); 14 | } 15 | 16 | export default FAB; 17 | -------------------------------------------------------------------------------- /src/routes/api/verify-password/+get.js: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | 3 | dotenv.config() 4 | 5 | export default async ({ headers }) => { 6 | const token = headers.authorization?.split(" ")[1]; 7 | if (token !== process.env["ADMIN_TOKEN"]) { 8 | return { 9 | statusCode: 401, 10 | data: { 11 | message: "Unauthorized", 12 | }, 13 | }; 14 | } 15 | return { 16 | data: { 17 | message: "Password verified", 18 | }, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | font-size: 19.2px; 4 | font-family: Arial, Helvetica, sans-serif; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | overflow: hidden; 10 | width: 100%; 11 | height: 100%; 12 | } 13 | 14 | *::-webkit-scrollbar { 15 | width: 8px; 16 | 17 | } 18 | 19 | *::-webkit-scrollbar-track { 20 | background: transparent; 21 | } 22 | 23 | *::-webkit-scrollbar-thumb { 24 | background-color: #dadce0; 25 | border-radius: 10px; 26 | } 27 | 28 | *::-webkit-scrollbar-thumb:hover { 29 | background-color: #80868b; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/FAB.module.scss: -------------------------------------------------------------------------------- 1 | @import "../scss/colors"; 2 | 3 | .root { 4 | position: fixed; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | bottom: 18px; 9 | right: 18px; 10 | border-radius: 100%; 11 | font-size: 24px; 12 | color: $color-fill-color-text-on-accent-primary; 13 | background-color: $color-fill-color-accent-primary; 14 | outline: none; 15 | border: none; 16 | width: 56px; 17 | height: 56px; 18 | box-shadow: $effect-style-shadow-flyout; 19 | cursor: pointer; 20 | -webkit-tap-highlight-color: transparent; 21 | } 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Motivation 2 | 3 | This section should describe the motivation behind the changes, and reference the related GitHub issues. E.g. "Fixes #123". 4 | 5 | ## Description 6 | 7 | This section should describe the changes proposed in the pull request. Explain the rationale for the changes, and comment on your code where necessary. 8 | 9 | ## Testing 10 | 11 | This section should describe the testing that has been done on the changes. Include instructions for reproducing the tests/verification, and list any relevant details for your test configuration. If there are no tests to be run, please add a quick note explaining why. 12 | -------------------------------------------------------------------------------- /src/components/Button.jsx: -------------------------------------------------------------------------------- 1 | import * as styles from "./Button.module.scss"; 2 | 3 | function Button({ 4 | children, 5 | label, 6 | fullWidth = false, 7 | secondary = false, 8 | disabled = false, 9 | onClick, 10 | }) { 11 | const handleClick = (e) => { 12 | if (!disabled && onClick) { 13 | onClick(e); 14 | } 15 | } 16 | return ( 17 | 27 | ); 28 | } 29 | 30 | export default Button; 31 | -------------------------------------------------------------------------------- /src/util/pendingAssignments.js: -------------------------------------------------------------------------------- 1 | export const load = () => { 2 | return (localStorage.getItem("pending") || "").split(","); 3 | }; 4 | 5 | export const toggle = (id) => { 6 | const pending = load(); 7 | const index = pending.indexOf(id); 8 | if (index == -1) { 9 | pending.push(id); 10 | } else { 11 | pending.splice(index, 1); 12 | } 13 | localStorage.setItem("pending", pending.join(",")); 14 | return pending; 15 | }; 16 | 17 | export const sync = (assignments) => { 18 | const ids = assignments.map((assignment) => assignment[0]); 19 | const pending = load().filter((id) => ids.includes(id)); 20 | localStorage.setItem("pending", pending.join(",")); 21 | return pending; 22 | }; 23 | -------------------------------------------------------------------------------- /src/util/finishedAssignments.js: -------------------------------------------------------------------------------- 1 | export const load = () => { 2 | return (localStorage.getItem("finished") || "").split(","); 3 | }; 4 | 5 | export const toggle = (id) => { 6 | const finished = load(); 7 | const index = finished.indexOf(id); 8 | if (index == -1) { 9 | finished.push(id); 10 | } else { 11 | finished.splice(index, 1); 12 | } 13 | localStorage.setItem("finished", finished.join(",")); 14 | return finished; 15 | }; 16 | 17 | export const sync = (assignments) => { 18 | const ids = assignments.map((assignment) => assignment[0]); 19 | const finished = load().filter((id) => ids.includes(id)); 20 | localStorage.setItem("finished", finished.join(",")); 21 | return finished; 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/Banner.jsx: -------------------------------------------------------------------------------- 1 | import * as styles from "./Banner.module.scss"; 2 | 3 | function Banner({ children, variant = "attention", onClick }) { 4 | const handleClick = () => { 5 | if (onClick) { 6 | onClick(); 7 | } 8 | }; 9 | return ( 10 |
19 | {children} 20 |
21 | ); 22 | } 23 | 24 | export default Banner; 25 | -------------------------------------------------------------------------------- /src/routes/api/subscribe/+post.js: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | import { getApps, initializeApp, cert } from "firebase-admin/app"; 3 | import { getMessaging } from "firebase-admin/messaging"; 4 | 5 | dotenv.config() 6 | 7 | const app = getApps()[0] || initializeApp({ 8 | credential: cert({ 9 | projectId: process.env["FIREBASE_ADMIN_PROJECT_ID"], 10 | clientEmail: process.env["FIREBASE_ADMIN_CLIENT_EMAIL"], 11 | privateKey: process.env["FIREBASE_ADMIN_PRIVATE_KEY"].replace(/\\n/g, "\n"), 12 | }), 13 | databaseURL: `https://${process.env["FIREBASE_ADMIN_PROJECT_ID"]}.firebaseio.com`, 14 | }); 15 | 16 | const messaging = getMessaging(app); 17 | 18 | export default async ({ body }) => { 19 | const token = body["token"]; 20 | await messaging.subscribeToTopic(token, "assignments"); 21 | return { 22 | data: { 23 | message: "Subscribed", 24 | }, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /public/firebase-messaging-sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener("notificationclick", function (event) { 2 | event.stopImmediatePropagation(); 3 | event.notification.close(); 4 | clients.openWindow("https://csed2024assignments.geeekyboy.com/"); 5 | }); 6 | 7 | importScripts("https://www.gstatic.com/firebasejs/9.18.0/firebase-app-compat.js"); 8 | importScripts("https://www.gstatic.com/firebasejs/9.18.0/firebase-messaging-compat.js"); 9 | 10 | firebase.initializeApp({ 11 | apiKey: "AIzaSyBRTHLN9HHQjevO4yHIqQeokv3VCzaZHXw", 12 | authDomain: "csed-assignments-4deb5.firebaseapp.com", 13 | databaseURL: "https://csed-assignments-4deb5-default-rtdb.firebaseio.com", 14 | projectId: "csed-assignments-4deb5", 15 | storageBucket: "csed-assignments-4deb5.appspot.com", 16 | messagingSenderId: "435235328157", 17 | appId: "1:435235328157:web:51647b0916d880972b8474", 18 | measurementId: "G-3HB4Z9SEGM", 19 | }); 20 | 21 | const messaging = firebase.messaging(); 22 | -------------------------------------------------------------------------------- /src/components/Banner.module.scss: -------------------------------------------------------------------------------- 1 | @import "../scss/colors"; 2 | 3 | .root { 4 | width: calc(100% - 40px); 5 | margin-top: -10px; 6 | padding: 8px 20px; 7 | background-color: $color-fill-color-accent-primary; 8 | color: $color-fill-color-text-on-accent-primary; 9 | font-size: 0.875rem; 10 | text-align: center; 11 | &.clickable { 12 | cursor: pointer; 13 | } 14 | &.attention { 15 | background-color: $color-fill-color-accent-primary; 16 | color: $color-fill-color-text-on-accent-primary; 17 | } 18 | &.success { 19 | background-color: $color-fill-color-system-success-background; 20 | color: $color-fill-color-system-success; 21 | } 22 | &.caution { 23 | background-color: $color-fill-color-system-caution-background; 24 | color: $color-fill-color-system-caution; 25 | } 26 | &.critical { 27 | background-color: $color-fill-color-system-critical-background; 28 | color: $color-fill-color-system-critical; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Checkbox.jsx: -------------------------------------------------------------------------------- 1 | import CheckmarkIcon from "jsx:@fluentui/svg-icons/icons/checkmark_12_regular.svg"; 2 | import * as styles from "./Checkbox.module.scss"; 3 | 4 | function Checkbox({ 5 | name, 6 | label, 7 | $value, 8 | readOnly, 9 | disabled, 10 | }) { 11 | const handleToggle = () => { 12 | if (disabled || readOnly) return; 13 | $value = !$value; 14 | } 15 | return ( 16 |
21 | 29 | {label && ( 30 | 33 | )} 34 |
35 | ); 36 | } 37 | 38 | export default Checkbox; 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSED 2024 Assignments 2 | 3 |
4 | 5 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/GeeekyBoy/csed-2024-assignments/blob/main/LICENSE) 6 | [![GitHub stars](https://img.shields.io/github/stars/GeeekyBoy/csed-2024-assignments.svg?style=social&label=Star)](https://github.com/GeeekyBoy/csed-2024-assignments) 7 | 8 |
9 | 10 | ## Overview 11 | 12 | CSED 2024 Assignments is a simple assignments tracker for CSED 2024 students. Through this app, students can mark their assignments as pending or done. They can also be notified when any assignment is added or updated. 13 | 14 | ## Contributing 15 | 16 | Read our [contributing guide](CONTRIBUTING.md) to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to CSED 2024 Assignments. 17 | 18 | ## License 19 | 20 | CSED 2024 Assignments is licensed under the [MIT License](LICENSE). 21 | 22 | ![View count](https://hits-app.vercel.app/hits?url=https://github.com/GeeekyBoy/csed-2024-assignments&bgRight=000&bgLeft=000) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present GeeekyBoy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csed-2024-assignments", 3 | "description": "All the assignments for CSED 2024 in one place.", 4 | "keywords": [ 5 | "csed", 6 | "assignments", 7 | "mango" 8 | ], 9 | "license": "MIT", 10 | "scripts": { 11 | "start": "mango-scripts start", 12 | "build": "mango-scripts build", 13 | "serve": "mango-scripts serve" 14 | }, 15 | "devDependencies": { 16 | "@mango-js/scripts": "^1.0.0-alpha.29", 17 | "@mango-js/types": "^1.0.0-alpha.29", 18 | "process": "^0.11.10" 19 | }, 20 | "dependencies": { 21 | "firebase": "^10.7.2", 22 | "firebase-admin": "^12.0.0", 23 | "@fluentui/svg-icons": "^1.1.225", 24 | "canvas-confetti": "^1.9.2", 25 | "workbox-core": "^7.0.0", 26 | "workbox-routing": "^7.0.0", 27 | "workbox-strategies": "^7.0.0", 28 | "dotenv": "^16.3.2" 29 | }, 30 | "alias": { 31 | "process": "process/browser.js" 32 | }, 33 | "config": { 34 | "publicUrl": "/", 35 | "browsers": "defaults", 36 | "cdn": "self", 37 | "devServer": { 38 | "port": 4000 39 | }, 40 | "prodServer": { 41 | "port": 3000 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/routes/api/assignments/[id]/+delete.js: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import { getApps, initializeApp, cert } from "firebase-admin/app"; 3 | import { getFirestore } from "firebase-admin/firestore"; 4 | 5 | dotenv.config() 6 | 7 | const app = getApps()[0] || initializeApp({ 8 | credential: cert({ 9 | projectId: process.env["FIREBASE_ADMIN_PROJECT_ID"], 10 | clientEmail: process.env["FIREBASE_ADMIN_CLIENT_EMAIL"], 11 | privateKey: process.env["FIREBASE_ADMIN_PRIVATE_KEY"].replace(/\\n/g, "\n"), 12 | }), 13 | databaseURL: `https://${process.env["FIREBASE_ADMIN_PROJECT_ID"]}.firebaseio.com`, 14 | }); 15 | 16 | const db = getFirestore(app); 17 | 18 | export default async ({ body, route, headers }) => { 19 | const token = headers.authorization?.split(" ")[1]; 20 | if (token !== process.env["ADMIN_TOKEN"]) { 21 | return { 22 | statusCode: 401, 23 | data: { 24 | message: "Unauthorized", 25 | }, 26 | }; 27 | } 28 | const assignmentId = route.params.id; 29 | await db.doc(`assignments/${assignmentId}`).delete(); 30 | return { 31 | data: { 32 | message: "Assignment updated", 33 | }, 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/AssignmentForm.module.scss: -------------------------------------------------------------------------------- 1 | @import "../scss/colors"; 2 | 3 | .modalForm { 4 | position: relative; 5 | display: grid; 6 | grid-template-columns: repeat(2, 1fr); 7 | grid-template-rows: repeat(3, auto); 8 | grid-gap: 10px; 9 | label { 10 | display: block; 11 | font-size: 0.875rem; 12 | margin-bottom: 5px; 13 | } 14 | input { 15 | width: 100%; 16 | height: 32px; 17 | padding: 6px 10px; 18 | border: 1px solid transparent; 19 | border-radius: 4px; 20 | border-color: $color-stroke-color-card-stroke-default; 21 | border-bottom-color: $color-stroke-color-control-strong-stroke-default; 22 | background-color: $color-fill-color-control-default; 23 | outline: none; 24 | box-sizing: border-box; 25 | -moz-box-sizing: border-box; 26 | -webkit-box-sizing: border-box; 27 | margin-bottom: 10px; 28 | &:disabled { 29 | opacity: 0.6; 30 | cursor: not-allowed; 31 | } 32 | &:hover { 33 | background-color: $color-fill-color-control-secondary; 34 | } 35 | &:focus { 36 | border-radius: 4px 4px 0 0; 37 | background-color: $color-fill-color-control-input-active; 38 | border-bottom-color: $color-fill-color-accent-primary; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Modal.jsx: -------------------------------------------------------------------------------- 1 | import Button from "./Button"; 2 | import * as styles from "./Modal.module.scss"; 3 | 4 | function Modal({ 5 | title, 6 | children, 7 | submitLabel, 8 | cancelLabel, 9 | onSubmit, 10 | onCancel, 11 | disabled, 12 | $active, 13 | }) { 14 | const handleCancel = () => { 15 | if (onCancel) { 16 | onCancel(); 17 | } 18 | $active = false; 19 | } 20 | const handleSubmit = () => { 21 | if (onSubmit) { 22 | onSubmit(); 23 | } 24 | } 25 | return ( 26 |
27 |
28 |
29 |
30 |
{title}
31 |
{children}
32 |
33 |
34 | 37 | 40 |
41 |
42 |
43 | ); 44 | } 45 | 46 | export default Modal; 47 | -------------------------------------------------------------------------------- /src/routes/api/assignments/+post.js: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import { getApps, initializeApp, cert } from "firebase-admin/app"; 3 | import { getFirestore } from "firebase-admin/firestore"; 4 | import { getMessaging } from "firebase-admin/messaging"; 5 | 6 | dotenv.config() 7 | 8 | const app = getApps()[0] || initializeApp({ 9 | credential: cert({ 10 | projectId: process.env["FIREBASE_ADMIN_PROJECT_ID"], 11 | clientEmail: process.env["FIREBASE_ADMIN_CLIENT_EMAIL"], 12 | privateKey: process.env["FIREBASE_ADMIN_PRIVATE_KEY"].replace(/\\n/g, "\n"), 13 | }), 14 | databaseURL: `https://${process.env["FIREBASE_ADMIN_PROJECT_ID"]}.firebaseio.com`, 15 | }); 16 | 17 | const db = getFirestore(app); 18 | const messaging = getMessaging(app); 19 | 20 | export default async ({ body, headers }) => { 21 | const token = headers.authorization?.split(" ")[1]; 22 | if (token !== process.env["ADMIN_TOKEN"]) { 23 | return { 24 | statusCode: 401, 25 | data: { 26 | message: "Unauthorized", 27 | }, 28 | }; 29 | } 30 | await db.collection("assignments").add(body); 31 | await messaging.sendToTopic("assignments", { 32 | notification: { 33 | title: "New Assignment", 34 | body: `${body["subject"]} - ${body["assignment"]}`, 35 | }, 36 | }); 37 | return { 38 | data: { 39 | message: "Assignment added", 40 | }, 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /src/routes/api/assignments/[id]/+patch.js: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import { getApps, initializeApp, cert } from "firebase-admin/app"; 3 | import { getFirestore } from "firebase-admin/firestore"; 4 | import { getMessaging } from "firebase-admin/messaging"; 5 | 6 | dotenv.config() 7 | 8 | const app = getApps()[0] || initializeApp({ 9 | credential: cert({ 10 | projectId: process.env["FIREBASE_ADMIN_PROJECT_ID"], 11 | clientEmail: process.env["FIREBASE_ADMIN_CLIENT_EMAIL"], 12 | privateKey: process.env["FIREBASE_ADMIN_PRIVATE_KEY"].replace(/\\n/g, "\n"), 13 | }), 14 | databaseURL: `https://${process.env["FIREBASE_ADMIN_PROJECT_ID"]}.firebaseio.com`, 15 | }); 16 | 17 | const db = getFirestore(app); 18 | const messaging = getMessaging(app); 19 | 20 | export default async ({ body, route, headers }) => { 21 | const token = headers.authorization?.split(" ")[1]; 22 | if (token !== process.env["ADMIN_TOKEN"]) { 23 | return { 24 | statusCode: 401, 25 | data: { 26 | message: "Unauthorized", 27 | }, 28 | }; 29 | } 30 | const assignmentId = route.params.id; 31 | await db.doc(`assignments/${assignmentId}`).update(body); 32 | await messaging.sendToTopic("assignments", { 33 | notification: { 34 | title: "Assignment Updated", 35 | body: `${body["subject"]} - ${body["assignment"]}`, 36 | }, 37 | }); 38 | return { 39 | data: { 40 | message: "Assignment updated", 41 | }, 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/util/dateUtils.js: -------------------------------------------------------------------------------- 1 | export const timestampToDate = (timestamp) => { 2 | const date = timestamp ? new Date(timestamp) : new Date(); 3 | let month = date.getMonth() + 1; 4 | let dayOfMonth = date.getDate(); 5 | const year = date.getFullYear(); 6 | month = month < 10 ? "0" + month : month; 7 | dayOfMonth = dayOfMonth < 10 ? "0" + dayOfMonth : dayOfMonth; 8 | return year + "-" + month + "-" + dayOfMonth; 9 | } 10 | 11 | export const timestampToTime = (timestamp) => { 12 | const date = timestamp ? new Date(timestamp) : new Date(); 13 | let hour = date.getHours(); 14 | let minute = date.getMinutes(); 15 | hour = hour < 10 ? "0" + hour : hour; 16 | minute = minute < 10 ? "0" + minute : minute; 17 | return hour + ":" + minute; 18 | } 19 | 20 | export const dateToString = (date, showTime, beforeSection) => { 21 | const day = date.getDay(); 22 | const month = date.getMonth() + 1; 23 | const dayOfMonth = date.getDate(); 24 | let hour = date.getHours(); 25 | let minute = date.getMinutes(); 26 | const ampm = hour >= 12 ? "PM" : "AM"; 27 | hour = hour % 12; 28 | hour = hour ? hour : 12; 29 | minute = minute < 10 ? "0" + minute : minute; 30 | const strTime = hour + ":" + minute + " " + ampm; 31 | const days = [ 32 | "Sunday", 33 | "Monday", 34 | "Tuesday", 35 | "Wednesday", 36 | "Thursday", 37 | "Friday", 38 | "Saturday", 39 | ]; 40 | return ( 41 | days[day] + 42 | " " + 43 | dayOfMonth + 44 | "/" + 45 | month + 46 | (showTime ? " @ " + strTime : "") + 47 | (beforeSection ? " before section" : "") 48 | ); 49 | } -------------------------------------------------------------------------------- /.github/DISCUSSION_TEMPLATE/ideas.yml: -------------------------------------------------------------------------------- 1 | title: "Sample title" 2 | labels: ["enhancement"] 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thanks for taking the time to propose a new feature! Please make sure to include as much information as possible. 8 | - type: checkboxes 9 | attributes: 10 | label: Is this feature proposed before? 11 | description: Please search to see if your idea is already proposed. 12 | options: 13 | - label: I have searched the existing proposals and have not found a similar one 14 | required: true 15 | - type: textarea 16 | attributes: 17 | label: Motivation 18 | description: Describe the motivation behind this idea, and why you think it would be a good addition. Feel free to support your argument with real-world examples. This section should be high-level and not include technical details or implementation ideas. 19 | validations: 20 | required: true 21 | - type: textarea 22 | attributes: 23 | label: Proposed Solution 24 | description: Describe the solution you'd like. Try to be as specific as possible. Feel free to include code snippets, diagrams, or photos to help explain your idea. 25 | validations: 26 | required: true 27 | - type: textarea 28 | attributes: 29 | label: Alternative Solutions 30 | description: Describe any alternative solutions you've considered. This is the place to discuss trade-offs you might have thought of. 31 | validations: 32 | required: true 33 | - type: textarea 34 | attributes: 35 | label: Competitors 36 | description: What other products have this feature? What do you like about how they implement it? What do you dislike about how they implement it? 37 | validations: 38 | required: false 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---01-bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "Sample title" 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! Please make sure to include as much information as possible. 10 | - type: checkboxes 11 | attributes: 12 | label: Is there an existing issue for this? 13 | description: Please search to see if an issue already exists for the bug you encountered. 14 | options: 15 | - label: I have searched the existing issues and have not found a similar one 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: Current Behavior 20 | description: A concise description of what you're experiencing. 21 | validations: 22 | required: true 23 | - type: textarea 24 | attributes: 25 | label: Expected Behavior 26 | description: A concise description of what you expected to happen. 27 | validations: 28 | required: true 29 | - type: textarea 30 | attributes: 31 | label: Steps To Reproduce 32 | description: Steps to reproduce the behavior. 33 | placeholder: | 34 | 1. In this environment... 35 | 2. Do '...' 36 | 3. See error... 37 | validations: 38 | required: true 39 | - type: textarea 40 | attributes: 41 | label: Environment 42 | description: | 43 | examples: 44 | - **OS**: Ubuntu 20.04 45 | - **Browser**: Chrome 90 46 | value: | 47 | - **OS**: 48 | - **Browser**: 49 | validations: 50 | required: true 51 | - type: checkboxes 52 | id: willing-to-pr 53 | attributes: 54 | label: I would be interested in opening a PR for this issue 55 | description: | 56 | If you have the time and interest, we always appreciate pull requests! 57 | options: 58 | - label: I would be interested in opening a PR for this issue 59 | required: false 60 | -------------------------------------------------------------------------------- /src/components/Button.module.scss: -------------------------------------------------------------------------------- 1 | @import "../scss/colors"; 2 | 3 | .root { 4 | min-width: 86px; 5 | padding: 7px 14px; 6 | border: 1px solid transparent; 7 | border-radius: 4px; 8 | border-color: $color-stroke-color-control-stroke-default; 9 | border-bottom-color: $color-stroke-color-control-stroke-secondary; 10 | outline: none; 11 | background-color: $color-fill-color-accent-primary; 12 | color: $color-fill-color-text-on-accent-primary; 13 | font-size: 0.875rem; 14 | cursor: pointer; 15 | transition: background-color 0.3s, color 0.2s; 16 | -webkit-tap-highlight-color: transparent; 17 | &.fullWidth { 18 | width: 100%; 19 | } 20 | &.secondary { 21 | background-color: $color-fill-color-control-default; 22 | color: $color-fill-color-text-primary; 23 | } 24 | &:hover { 25 | background-color: $color-fill-color-accent-secondary; 26 | &.secondary { 27 | background-color: $color-fill-color-control-secondary; 28 | } 29 | } 30 | &:active { 31 | border-color: $color-stroke-color-control-stroke-on-accent-default; 32 | border-bottom-color: $color-stroke-color-control-stroke-on-accent-default; 33 | background-color: $color-fill-color-accent-tertiary; 34 | color: $color-fill-color-text-on-accent-secondary; 35 | &.secondary { 36 | border-color: $color-stroke-color-control-stroke-default; 37 | border-bottom-color: $color-stroke-color-control-stroke-default; 38 | color: $color-fill-color-text-secondary; 39 | } 40 | } 41 | &:disabled { 42 | border-color: transparent !important; 43 | border-bottom-color: transparent !important; 44 | background-color: $color-fill-color-accent-disabled !important; 45 | color: $color-fill-color-text-on-accent-disabled !important; 46 | cursor: not-allowed; 47 | &.secondary { 48 | border-color: $color-stroke-color-control-stroke-default !important; 49 | border-bottom-color: $color-stroke-color-control-stroke-default !important; 50 | background-color: $color-fill-color-control-disabled !important; 51 | color: $color-fill-color-text-disabled !important; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/components/Modal.module.scss: -------------------------------------------------------------------------------- 1 | @import "../scss/colors"; 2 | 3 | .root { 4 | display: none; 5 | position: absolute; 6 | z-index: 10; 7 | 8 | &.active { 9 | display: block; 10 | } 11 | .modalOverlay { 12 | position: fixed; 13 | top: 0; 14 | left: 0; 15 | width: 100%; 16 | height: 100%; 17 | background-color: #000000; 18 | opacity: 0.5; 19 | } 20 | .modalContent { 21 | position: fixed; 22 | margin: 0 auto; 23 | width: 100%; 24 | bottom: 0; 25 | background-color: #ffffff; 26 | color: #000000; 27 | line-height: 1.3; 28 | overflow: hidden; 29 | border-radius: 8px; 30 | background-color: $color-background-fill-color-solid-background-base; 31 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); 32 | 33 | @media (min-width: 768px) { 34 | position: relative; 35 | max-width: 600px; 36 | } 37 | 38 | .modalHeader { 39 | position: relative; 40 | text-align: center; 41 | margin-bottom: 15px; 42 | color: $color-fill-color-text-primary; 43 | font-weight: 600; 44 | .modalClose { 45 | position: absolute; 46 | top: 0; 47 | right: 0; 48 | padding: 1rem; 49 | cursor: pointer; 50 | } 51 | } 52 | .modalHeaderFooter { 53 | padding: 20px; 54 | overflow: hidden; 55 | background-color: $color-background-fill-color-layer-alt; 56 | box-sizing: border-box; 57 | -moz-box-sizing: border-box; 58 | -webkit-box-sizing: border-box; 59 | } 60 | .modalFooter { 61 | display: flex; 62 | flex-direction: row; 63 | align-items: center; 64 | justify-content: flex-end; 65 | width: 100%; 66 | padding: 15px 20px; 67 | gap: 10px; 68 | box-sizing: border-box; 69 | -moz-box-sizing: border-box; 70 | -webkit-box-sizing: border-box; 71 | } 72 | } 73 | @media (min-width: 768px) { 74 | position: fixed; 75 | width: 100%; 76 | height: 100%; 77 | top: 0; 78 | left: 0; 79 | align-items: center; 80 | justify-content: center; 81 | &.active { 82 | display: flex; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/components/Checkbox.module.scss: -------------------------------------------------------------------------------- 1 | @import "../scss/colors"; 2 | 3 | .CheckboxShell { 4 | display: flex; 5 | position: relative; 6 | flex-direction: row; 7 | align-items: flex-start; 8 | justify-content: center; 9 | width: fit-content; 10 | padding: 6px 4px; 11 | background-color: transparent; 12 | gap: 8px; 13 | & > label { 14 | width: max-content; 15 | margin-bottom: 0; 16 | color: $color-fill-color-text-primary; 17 | font-size: 0.875rem; 18 | cursor: pointer; 19 | } 20 | &.disabled { 21 | & > label { 22 | color: $color-fill-color-text-disabled; 23 | } 24 | .CheckToggle { 25 | border-color: $color-stroke-color-control-strong-stroke-disabled; 26 | background-color: $color-fill-color-control-alt-disabled; 27 | &.checked { 28 | border-color: transparent; 29 | background-color: $color-fill-color-accent-disabled; 30 | color: $color-fill-color-text-on-accent-disabled; 31 | } 32 | } 33 | & * { 34 | cursor: not-allowed; 35 | } 36 | } 37 | &.readOnly * { 38 | cursor: default; 39 | } 40 | } 41 | 42 | .CheckToggle { 43 | display: flex; 44 | align-items: center; 45 | justify-content: center; 46 | width: 20px; 47 | height: 20px; 48 | padding: 0; 49 | border: 1px solid; 50 | border-radius: 3px; 51 | border-color: $color-stroke-color-control-strong-stroke-default; 52 | outline: none; 53 | background-color: transparent; 54 | background-color: $color-fill-color-control-alt-secondary; 55 | cursor: pointer; 56 | transition: border-color 0.2s, background-color 0.2s; 57 | &:hover { 58 | background-color: $color-fill-color-control-alt-tertiary; 59 | } 60 | &:active { 61 | border-color: $color-stroke-color-control-strong-stroke-disabled; 62 | background-color: $color-fill-color-control-alt-quarternary; 63 | } 64 | &.checked { 65 | border-color: transparent; 66 | background-color: $color-fill-color-accent-primary; 67 | color: $color-fill-color-text-on-accent-primary; 68 | &:hover { 69 | background-color: $color-fill-color-accent-secondary; 70 | } 71 | &:active { 72 | background-color: $color-fill-color-accent-tertiary; 73 | color: $color-fill-color-text-on-accent-secondary; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/util/firebase.js: -------------------------------------------------------------------------------- 1 | // @mango 2 | import { initializeApp } from "firebase/app"; 3 | import { getAnalytics } from "firebase/analytics"; 4 | import { getMessaging, onMessage, getToken, isSupported } from "firebase/messaging"; 5 | import { initializeFirestore, enableMultiTabIndexedDbPersistence } from "firebase/firestore"; 6 | 7 | const firebaseConfig = { 8 | apiKey: process.env["FIREBASE_CLIENT_API_KEY"], 9 | authDomain: process.env["FIREBASE_CLIENT_AUTH_DOMAIN"], 10 | databaseURL: process.env["FIREBASE_CLIENT_DATABASE_URL"], 11 | projectId: process.env["FIREBASE_CLIENT_PROJECT_ID"], 12 | storageBucket: process.env["FIREBASE_CLIENT_STORAGE_BUCKET"], 13 | messagingSenderId: process.env["FIREBASE_CLIENT_MESSAGING_SENDER_ID"], 14 | appId: process.env["FIREBASE_CLIENT_APP_ID"], 15 | measurementId: process.env["FIREBASE_CLIENT_MEASUREMENT_ID"], 16 | }; 17 | 18 | const app = initializeApp(firebaseConfig); 19 | getAnalytics(app); 20 | 21 | const db = initializeFirestore(app, { 22 | cacheSizeBytes: 5e6, 23 | }); 24 | 25 | enableMultiTabIndexedDbPersistence(db).catch((err) => { 26 | if (err.code === "failed-precondition") { 27 | console.error( 28 | "Multiple tabs open, persistence can only be enabled in one tab at a a time." 29 | ); 30 | } else if (err.code === "unimplemented") { 31 | console.error( 32 | "The current browser does not support all of the features required to enable persistence" 33 | ); 34 | } 35 | }); 36 | 37 | let $notificationsState = 38 | (typeof navigator !== 'undefined') && 39 | (navigator.cookieEnabled) && 40 | ('serviceWorker' in navigator) && 41 | ('Notification' in window) && 42 | ('PushManager' in window) 43 | ? Notification.permission 44 | : 'unsupported'; 45 | 46 | isSupported().then((supported) => { 47 | if (!supported) { 48 | $notificationsState = 'unsupported'; 49 | } 50 | }); 51 | 52 | const enableNotifications = async () => { 53 | if (await isSupported()) { 54 | Notification.requestPermission().then((permission) => { 55 | $notificationsState = permission; 56 | if (permission === 'granted') { 57 | const messaging = getMessaging(app); 58 | getToken(messaging, { 59 | vapidKey: process.env["FIREBASE_CLIENT_VAPID_KEY"], 60 | }).then(async (currentToken) => { 61 | if (currentToken) { 62 | try { 63 | await fetch("/api/subscribe", { 64 | method: "POST", 65 | body: JSON.stringify({ 66 | token: currentToken, 67 | }), 68 | headers: { 69 | "Content-Type": "application/json", 70 | }, 71 | }); 72 | onMessage( 73 | messaging, 74 | (payload) => { 75 | console.log("payload", payload); 76 | }, 77 | (error) => { 78 | console.error("Error while subscribing to notifications", error); 79 | } 80 | ); 81 | } catch { 82 | console.error(`Can't subscribe to notifications`); 83 | } 84 | } 85 | }); 86 | } 87 | }); 88 | } 89 | } 90 | 91 | if (('Notification' in window) && Notification.permission === "granted") { 92 | enableNotifications(); 93 | } 94 | 95 | export { 96 | $notificationsState, 97 | enableNotifications, 98 | } 99 | 100 | -------------------------------------------------------------------------------- /src/routes/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import "../scss/colors"; 2 | 3 | @keyframes pulse { 4 | 0% { 5 | transform: scale(0.95); 6 | opacity: 0.5; 7 | } 8 | 70% { 9 | transform: scale(1); 10 | opacity: 1; 11 | } 12 | 100% { 13 | transform: scale(0.95); 14 | opacity: 0.5; 15 | } 16 | } 17 | 18 | .root { 19 | position: fixed; 20 | width: 100%; 21 | height: 100%; 22 | background-color: #f9fbfd; 23 | background-position: center; 24 | color: #000000; 25 | line-height: 1.3; 26 | overflow: auto; 27 | 28 | table { 29 | display: table; 30 | width: 100%; 31 | max-width: 600px; 32 | border-collapse: collapse; 33 | border-spacing: 0; 34 | margin: 0; 35 | padding: 0; 36 | border: 0; 37 | font-size: 100%; 38 | font: inherit; 39 | background: transparent; 40 | border-collapse: collapse; 41 | border-spacing: 0; 42 | caption-side: top; 43 | cursor: auto; 44 | direction: ltr; 45 | empty-cells: show; 46 | font-family: inherit; 47 | font-size: inherit; 48 | font-style: inherit; 49 | font-variant: inherit; 50 | font-weight: inherit; 51 | letter-spacing: inherit; 52 | line-height: inherit; 53 | list-style: none; 54 | text-align: left; 55 | text-indent: 0; 56 | text-transform: none; 57 | visibility: visible; 58 | white-space: normal; 59 | word-spacing: normal; 60 | th { 61 | background-color: #999999; 62 | border: 1px solid #000000; 63 | padding: 0.3rem 0.5rem; 64 | font-size: 1.2rem; 65 | } 66 | td { 67 | position: relative; 68 | border: 1px solid #000000; 69 | padding: 0.3rem 0.5rem; 70 | font-family: 'Times New Roman', Times, serif; 71 | &.pending { 72 | color: $color-fill-color-system-caution; 73 | animation: pulse 2s infinite; 74 | } 75 | &.done { 76 | text-decoration: line-through; 77 | color: $color-fill-color-system-success; 78 | } 79 | } 80 | } 81 | 82 | a { 83 | color: #000000; 84 | } 85 | 86 | footer { 87 | font-size: 0.8rem; 88 | } 89 | } 90 | 91 | .filters { 92 | display: flex; 93 | flex-direction: row; 94 | justify-content: space-between; 95 | align-items: center; 96 | width: 100%; 97 | max-width: 600px; 98 | margin-bottom: 12px; 99 | } 100 | 101 | .StatusToggle { 102 | position: absolute; 103 | top: 50%; 104 | margin-top: -10px; 105 | left: -28px; 106 | display: flex; 107 | align-items: center; 108 | justify-content: center; 109 | width: 20px; 110 | min-width: 20px; 111 | height: 20px; 112 | min-height: 20px; 113 | padding: 2.5px; 114 | border: 1px solid; 115 | border-radius: 100%; 116 | border-color: $color-stroke-color-control-strong-stroke-default; 117 | outline: none; 118 | background-color: $color-fill-color-control-alt-secondary; 119 | cursor: pointer; 120 | transition: border-color 0.2s; 121 | &.done { 122 | border-color: $color-fill-color-accent-primary; 123 | background-color: $color-fill-color-accent-primary; 124 | color: $color-fill-color-text-on-accent-primary; 125 | & > svg * { 126 | stroke-width: 64px; 127 | } 128 | } 129 | } 130 | 131 | .DeleteButton { 132 | position: absolute; 133 | top: 50%; 134 | margin-top: -10px; 135 | left: -28px; 136 | display: flex; 137 | align-items: center; 138 | justify-content: center; 139 | width: 20px; 140 | min-width: 20px; 141 | height: 20px; 142 | min-height: 20px; 143 | padding: 2.5px; 144 | border: none; 145 | outline: none; 146 | background-color: transparent; 147 | cursor: pointer; 148 | transition: border-color 0.2s; 149 | } 150 | 151 | .viewport { 152 | background-color: #ffffff; 153 | color: #000000; 154 | width: 100%; 155 | min-height: 100vh; 156 | max-width: 800px; 157 | border-width: 1px; 158 | border-style: solid; 159 | padding: 50px 30px; 160 | margin-bottom: 12px; 161 | box-sizing: border-box; 162 | -moz-box-sizing: border-box; 163 | -webkit-box-sizing: border-box; 164 | border-color: $color-stroke-color-card-stroke-default; 165 | 166 | a { 167 | color: #1155cc; 168 | text-decoration: underline; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/components/AssignmentForm.jsx: -------------------------------------------------------------------------------- 1 | import Modal from "./Modal"; 2 | import Checkbox from "./Checkbox"; 3 | import * as dateUtils from "../util/dateUtils"; 4 | import * as styles from "./AssignmentForm.module.scss"; 5 | 6 | function AssignmentForm({ $active, currData = [] }) { 7 | let $isBusy = false; 8 | let $id, 9 | $subject, 10 | $assignment, 11 | $dueDate, 12 | $dueTime, 13 | $link, 14 | $showTime, 15 | $beforeSection; 16 | $createIEffect(() => { 17 | $id = currData[0] !== undefined ? currData[0] : null; 18 | $subject = currData[1] || ""; 19 | $assignment = currData[2] || ""; 20 | $dueDate = dateUtils.timestampToDate(currData[3]); 21 | $dueTime = dateUtils.timestampToTime(currData[3]); 22 | $link = currData[4] || ""; 23 | $showTime = currData[5] || false; 24 | $beforeSection = currData[6] || false; 25 | }, [currData]); 26 | async function handleSubmit() { 27 | $isBusy = true; 28 | if (!localStorage.getItem("token")) { 29 | localStorage.setItem("token", prompt("Please enter your password")); 30 | } 31 | const id = $id; 32 | const subject = $subject; 33 | const assignment = $assignment; 34 | const dueDate = new Date($dueDate + " " + $dueTime).getTime(); 35 | const link = $link; 36 | const showTime = $showTime; 37 | const beforeSection = $beforeSection; 38 | if ($id !== null) { 39 | const res = await fetch(`/api/assignments/${id}`, { 40 | method: "PATCH", 41 | headers: { 42 | "Content-Type": "application/json", 43 | "Authorization": "Bearer " + localStorage.getItem("token"), 44 | }, 45 | body: JSON.stringify({ 46 | subject, 47 | assignment, 48 | dueDate, 49 | link, 50 | showTime, 51 | beforeSection, 52 | }) 53 | }); 54 | if (res.status === 401) { 55 | localStorage.removeItem("token"); 56 | alert("Invalid password"); 57 | } else { 58 | $active = false; 59 | } 60 | } else { 61 | const res = await fetch("/api/assignments", { 62 | method: "POST", 63 | headers: { 64 | "Content-Type": "application/json", 65 | "Authorization": "Bearer " + localStorage.getItem("token"), 66 | }, 67 | body: JSON.stringify({ 68 | subject, 69 | assignment, 70 | dueDate, 71 | link, 72 | showTime, 73 | beforeSection, 74 | }) 75 | }); 76 | if (res.status === 401) { 77 | localStorage.removeItem("token"); 78 | alert("Invalid password"); 79 | } else { 80 | $active = false; 81 | } 82 | } 83 | $isBusy = false; 84 | } 85 | return ( 86 | 94 |
e.preventDefault()}> 95 |
96 | 97 | 104 |
105 |
106 | 107 | 114 |
115 |
116 | 117 | 123 |
124 |
125 | 126 | 132 |
133 |
134 | 135 | 141 |
142 | 143 | 148 | 149 |
150 | ); 151 | } 152 | 153 | export default AssignmentForm; 154 | -------------------------------------------------------------------------------- /src/scss/colors.scss: -------------------------------------------------------------------------------- 1 | $color-fill-color-text-primary: rgba(0,0,0,0.896); 2 | $color-fill-color-text-secondary: rgba(0,0,0,0.606); 3 | $color-fill-color-text-tertiary: rgba(0,0,0,0.446); 4 | $color-fill-color-text-disabled: rgba(0,0,0,0.361); 5 | $color-fill-color-accent-text-disabled: rgba(0,0,0,0.361); 6 | $color-fill-color-text-on-accent-primary: rgba(255,255,255,1); 7 | $color-fill-color-text-on-accent-secondary: rgba(255,255,255,0.700); 8 | $color-fill-color-text-on-accent-disabled: rgba(255,255,255,1); 9 | $color-fill-color-text-on-accent-selected-text: rgba(255,255,255,1); 10 | $color-fill-color-control-transparent: rgba(255,255,255,0.000); 11 | $color-fill-color-control-default: rgba(255,255,255,0.700); 12 | $color-fill-color-control-secondary: rgba(249,249,249,0.500); 13 | $color-fill-color-control-tertiary: rgba(249,249,249,0.300); 14 | $color-fill-color-control-input-active: rgba(255,255,255,1); 15 | $color-fill-color-control-disabled: rgba(249,249,249,0.300); 16 | $color-fill-color-control-strong-default: rgba(0,0,0,0.446); 17 | $color-fill-color-control-strong-disabled: rgba(0,0,0,0.317); 18 | $color-fill-color-subtle-transparent: rgba(255,255,255,0.000); 19 | $color-fill-color-subtle-secondary: rgba(0,0,0,0.037); 20 | $color-fill-color-subtle-tertiary: rgba(0,0,0,0.024); 21 | $color-fill-color-subtle-disabled: rgba(0,0,0,0.000); 22 | $color-fill-color-control-solid-default: rgba(255,255,255,1); 23 | $color-fill-color-control-alt-transparent: rgba(255,255,255,0.000); 24 | $color-fill-color-control-alt-secondary: rgba(0,0,0,0.024); 25 | $color-fill-color-control-alt-tertiary: rgba(0,0,0,0.058); 26 | $color-fill-color-control-alt-quarternary: rgba(0,0,0,0.092); 27 | $color-fill-color-control-alt-disabled: rgba(255,255,255,0.000); 28 | $color-fill-color-accent-primary: rgba(0, 103, 192, 1); 29 | $color-fill-color-accent-secondary: rgba(0, 103, 192, 0.90); 30 | $color-fill-color-accent-tertiary: rgba(0, 103, 192, 0.80); 31 | $color-fill-color-accent-disabled: rgba(0,0,0,0.217); 32 | $color-fill-color-system-critical: rgba(196,43,28,1); 33 | $color-fill-color-system-success: rgba(15,123,15,1); 34 | $color-fill-color-system-attention: rgba(0,95,183,1); 35 | $color-fill-color-system-caution: rgba(157,93,0,1); 36 | $color-fill-color-system-attention-background: rgba(246,246,246,0.500); 37 | $color-fill-color-system-success-background: rgba(223,246,221,1); 38 | $color-fill-color-system-caution-background: rgba(255,244,206,1); 39 | $color-fill-color-system-critical-background: rgba(253,231,233,1); 40 | $color-fill-color-system-neutral: rgba(0,0,0,0.446); 41 | $color-fill-color-system-neutral-background: rgba(0,0,0,0.024); 42 | $color-fill-color-system-solid-neutral: rgba(138,138,138,1); 43 | $color-fill-color-system-solid-attention-background: rgba(247,247,247,1); 44 | $color-fill-color-system-solid-neutral-background: rgba(243,243,243,1); 45 | $color-fill-color-control-on-image-default: rgba(255,255,255,0.790); 46 | $color-fill-color-control-on-image-secondary: rgba(243,243,243,1); 47 | $color-fill-color-control-on-image-tertiary: rgba(235,235,235,1); 48 | $color-fill-color-control-on-image-disabled: rgba(255,255,255,0.000); 49 | $color-stroke-color-control-stroke-default: rgba(0,0,0,0.058); 50 | $color-stroke-color-control-stroke-secondary: rgba(0,0,0,0.162); 51 | $color-stroke-color-control-stroke-on-accent-default: rgba(255,255,255,0.080); 52 | $color-stroke-color-control-stroke-on-accent-secondary: rgba(0,0,0,0.400); 53 | $color-stroke-color-control-stroke-on-accent-tertiary: rgba(0,0,0,0.217); 54 | $color-stroke-color-control-stroke-on-accent-disabled: rgba(0,0,0,0.058); 55 | $color-stroke-color-control-stroke-for-strong-fill-when-on-image: rgba(255,255,255,0.350); 56 | $color-stroke-color-control-strong-stroke-default: rgba(0,0,0,0.446); 57 | $color-stroke-color-control-strong-stroke-disabled: rgba(0,0,0,0.217); 58 | $color-stroke-color-card-stroke-default: rgba(0,0,0,0.15); 59 | $color-stroke-color-card-stroke-default-solid: rgba(235,235,235,1); 60 | $color-stroke-color-divider-stroke-default: rgba(0,0,0,0.080); 61 | $color-stroke-color-surface-stroke-default: rgba(117,117,117,0.400); 62 | $color-stroke-color-surface-stroke-flyout: rgba(0,0,0,0.058); 63 | $color-stroke-color-focus-stroke-outer: rgba(0,0,0,0.896); 64 | $color-stroke-color-focus-stroke-inner: rgba(255,255,255,1); 65 | $color-background-fill-color-card-background-default: rgba(255,255,255,0.700); 66 | $color-background-fill-color-card-background-secondary: rgba(246,246,246,0.500); 67 | $color-background-fill-color-smoke-default: rgba(0,0,0,0.300); 68 | $color-background-fill-color-layer-default: rgba(255,255,255,0.500); 69 | $color-background-fill-color-layer-alt: rgba(255,255,255,1); 70 | $color-background-fill-color-solid-background-base: rgba(243,243,243,1); 71 | $color-background-fill-color-solid-background-secondary: rgba(238,238,238,1); 72 | $color-background-fill-color-solid-background-tertiary: rgba(249,249,249,1); 73 | $color-background-fill-color-solid-background-quarternary: rgba(255,255,255,1); 74 | $effect-style-shadow-card-rest: 0.00px 2.00px 4.00px rgba(0,0,0,0.040); 75 | $effect-style-shadow-card-hover: 0.00px 2.00px 4.00px rgba(0,0,0,0.100); 76 | $effect-style-shadow-tooltip: 0.00px 4.00px 8.00px rgba(0,0,0,0.140); 77 | $effect-style-shadow-flyout: 0.00px 8.00px 16.00px rgba(0,0,0,0.140); 78 | $effect-style-shadow-dialog: 0.00px 2.00px 21.00px rgba(0,0,0,0.147); 79 | $effect-style-shell-shadows-inactive-window: 0.00px 2.00px 10.67px rgba(0,0,0,0.147); 80 | $effect-style-shell-shadows-active-window: 0.00px 2.00px 21.00px rgba(0,0,0,0.220); -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor Handbook 2 | 3 | Thanks for your interest in contributing to CSED 2024 Assignments! We're excited to have you on board. In this document, we'll outline the guidelines for contributing to CSED 2024 Assignments. These guidelines are designed to make it as easy as possible to get involved. 4 | 5 | ## Table of Contents 6 | 7 | - [Reporting Bugs](#reporting-bugs) 8 | - [Feature Requests](#feature-requests) 9 | - [Code Contribution Guidelines](#code-contribution-guidelines) 10 | - [Development Setup](#development-setup) 11 | - [Commit Message Style](#commit-message-style) 12 | - [Pull Requests Rules](#pull-requests-rules) 13 | 14 | ## Reporting Bugs 15 | 16 | This section guides you through submitting a bug report for CSED 2024 Assignments. Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports. 17 | 18 | ### Before Submitting A Bug Report 19 | 20 | - **Check the [issue tracker](https://github.com/GeeekyBoy/csed-2024-assignments/issues?q=is%3Aissue)**. Someone might have already reported the same problem. If it's already reported **and the issue is still open**, add a comment to the existing issue instead of opening a new one. 21 | - **Perform a cursory search on the internet**. This is a good way to see if the problem is a known issue with a known solution. 22 | 23 | ### How Do I Submit A (Good) Bug Report? 24 | 25 | To make sure your bug report gets the attention it deserves, please follow these guidelines: 26 | 27 | - **Use a clear and descriptive title** for the issue to identify the problem. 28 | - **Describe the exact steps which reproduce the problem** in as many details as possible. 29 | - **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). 30 | - **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. 31 | - **Explain which behavior you expected to see instead and why.** 32 | 33 | ## Feature Requests 34 | 35 | This section guides you through submitting a feature request for CSED 2024 Assignments. Following these guidelines helps maintainers and the community understand your suggestion and why it would be useful. 36 | 37 | ### Before Submitting An Enhancement Suggestion 38 | 39 | - **Have a look at the [proposed features](https://github.com/GeeekyBoy/csed-2024-assignments/discussions/categories/ideas)**. Someone might have already suggested the same thing. 40 | 41 | ### How Do I Submit A (Good) Feature Request? 42 | 43 | To make sure your feature request gets the attention it deserves, please follow these guidelines: 44 | 45 | - **Use a clear and descriptive title** for the discussion to identify the suggestion. 46 | - **Explain why this feature would be useful** to most CSED 2024 students. 47 | - **Describe the solution you'd like**. Be specific and include details of your proposed implementation. 48 | - **List some other projects where this feature exists**. This is a good way to get a feel for how it might work. This is not required, but it can help to get a better picture of what you're suggesting. 49 | - **Describe alternatives you've considered**. This is important so that others can understand the trade-offs you might have thought of. 50 | - **Include UML diagrams** if you think they would help to understand your suggestion. 51 | 52 | ## Code Contribution Guidelines 53 | 54 | ### Development Setup 55 | 56 | Before you start working on CSED 2024 Assignments, you'll need to setup your development environment. Follow the steps below to get started: 57 | 58 | 1. Fork the CSED 2024 Assignments repository on GitHub. 59 | 2. Clone your fork locally: 60 | ```bash 61 | git clone 62 | cd csed-2024-assignments 63 | ``` 64 | 3. Install the dependencies: 65 | ```bash 66 | npm install 67 | ``` 68 | 4. Run CSED 2024 Assignments website locally to test your changes: 69 | ```bash 70 | npm run start 71 | ``` 72 | 73 | No need to work on a separate branch. Once you're done, create a pull request to the `main` branch. It's advisable to pull the latest changes from the `main` branch periodically to avoid merge conflicts. 74 | 75 | ### Commit Message Style 76 | 77 | CSED 2024 Assignments commit message guidelines are derived from the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. Before proceeding, make sure you understand the specification. We add a few extra rules on top of the specification: 78 | 79 | #### Format 80 | 81 | The commit message must be in the following format. Please note that each line cannot be longer than 72 characters. 82 | 83 | ``` 84 | [(optional scope)]: 85 | 86 | [optional body] 87 | 88 | [optional footer(s)] 89 | ``` 90 | 91 | #### Type & Emoji 92 | 93 | | Type | Description | Emoji | 94 | | ---- | ----------- | ----- | 95 | | `build` | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm). This includes changes to the build scripts and package.json files. | 👷 | 96 | | `ci` | Changes to our CI configuration files and scripts (examples: CircleCi, SauceLabs) | 👷 | 97 | | `docs` | Documentation only changes. This includes changes to the documentation, website, README, etc. | 📝 | 98 | | `feat` | A new feature or enhancement that is not a bug fix. | ✨ | 99 | | `fix` | A bug fix. | 🐛 | 100 | | `perf` | A code change that improves performance | ⚡️ | 101 | | `refactor` | A code change that neither fixes a bug nor adds a feature | ♻️ | 102 | | `test` | Adding missing tests or correcting existing tests | 🧪 | 103 | | `revert` | Reverting a previous commit. | ⏪ | 104 | | `chore` | Changes that don't fit any of the above categories | [choose](https://gitmoji.dev/) | 105 | 106 | #### Scope 107 | 108 | Scope is optional. It can be anything specifying the place of the commit change. A common use case is when committing a change in the README file where the commit message would be `docs(readme): 📝`. 109 | 110 | #### Subject 111 | 112 | - **Use the imperative, present tense**: "change" not "changed" nor "changes". 113 | - **Capitalize the first letter**. 114 | - **Don't end the subject with a period or any other punctuation**. 115 | - **Initials must be uppercased**: `IE rejects CSS classes starting with _` not `ie rejects css classes starting with _`. 116 | 117 | #### Body 118 | 119 | - **Use the imperative, present tense**: "change" not "changed" nor "changes". 120 | - **Capitalize the first letter of each sentence**. 121 | 122 | ### Pull Requests Rules 123 | 124 | Here's some rules to follow when submitting a pull request. These rules are strictly enforced to ensure a high quality of the codebase. 125 | 126 | - **One pull request per feature**. If you want to add multiple features, create multiple pull requests. 127 | - **One pull request per bug fix**. If you want to fix multiple bugs, create multiple pull requests. 128 | - **Don't commit changes to files that are irrelevant to your feature or bug fix**. This will make it easier for the reviewer to focus on the changes that matter. 129 | -------------------------------------------------------------------------------- /src/routes/+pages.jsx: -------------------------------------------------------------------------------- 1 | import { enableNotifications, $notificationsState } from "../util/firebase"; 2 | import confetti from 'canvas-confetti'; 3 | import AssignmentForm from "../components/AssignmentForm"; 4 | import Banner from "../components/Banner"; 5 | import FAB from "../components/FAB"; 6 | import Checkbox from "../components/Checkbox"; 7 | import CheckmarkIcon from "jsx:@fluentui/svg-icons/icons/checkmark_24_regular.svg"; 8 | import DeleteIcon from "jsx:@fluentui/svg-icons/icons/delete_24_regular.svg"; 9 | import AddIcon from "jsx:@fluentui/svg-icons/icons/add_24_regular.svg"; 10 | import EditIcon from "jsx:@fluentui/svg-icons/icons/edit_24_regular.svg"; 11 | import { getFirestore, onSnapshot, collection } from "firebase/firestore"; 12 | import * as dateUtils from "../util/dateUtils.js"; 13 | import * as pendingAssignments from "../util/pendingAssignments.js"; 14 | import * as finishedAssignments from "../util/finishedAssignments.js"; 15 | import * as styles from "./styles.module.scss"; 16 | 17 | if ("serviceWorker" in navigator) { 18 | window.addEventListener("load", () => { 19 | navigator.serviceWorker.register(new URL("../workbox-sw.js", import.meta.url), { type: "module" }); 20 | }); 21 | } 22 | 23 | function App() { 24 | let $isModalActive = false; 25 | let $activeAssignment = []; 26 | let $pending = pendingAssignments.load(); 27 | let $finished = finishedAssignments.load(); 28 | let $isEditMode = false; 29 | let $showTodo = true; 30 | let $showPending = true; 31 | let $showFinished = true; 32 | let $loading = true; 33 | let $data = []; 34 | const teamsCodes = [ 35 | ["Data-Intensive", "g9kqa14"], 36 | ["Security", "64daw8i"], 37 | ["Multimedia", "hfe12ez"], 38 | ]; 39 | const driveLink = "https://drive.google.com/drive/folders/1QrC56oFyDboBWRCpjoFI7PedFU3ffilf"; 40 | const db = getFirestore(); 41 | onSnapshot(collection(db, "assignments"), (snapshot) => { 42 | $data = snapshot.docs.map((item) => { 43 | const data = item.data(); 44 | return [ 45 | item.id, 46 | data.subject, 47 | data.assignment, 48 | data.dueDate, 49 | data.link, 50 | data.showTime, 51 | data.beforeSection 52 | ]; 53 | }).sort((a, b) => a[3] - b[3]); 54 | $loading = false; 55 | }); 56 | const handleFabClick = async () => { 57 | if (!$isEditMode) { 58 | if (!localStorage.getItem("token")) { 59 | localStorage.setItem("token", prompt("Please enter your password")); 60 | } 61 | const res = await fetch(`/api/verify-password`, { 62 | method: "GET", 63 | headers: { 64 | "Authorization": "Bearer " + localStorage.getItem("token"), 65 | } 66 | }); 67 | if (res.status === 401) { 68 | localStorage.removeItem("token"); 69 | alert("Invalid password"); 70 | } else { 71 | $isEditMode = true; 72 | } 73 | } else { 74 | $activeAssignment = []; 75 | $isModalActive = true; 76 | } 77 | } 78 | const handleExitEditMode = () => { 79 | $isEditMode = false; 80 | } 81 | const handleChangeStatus = (id) => { 82 | if ($pending.includes(id)) { 83 | $pending = pendingAssignments.toggle(id); 84 | $finished = finishedAssignments.toggle(id); 85 | } else if ($finished.includes(id)) { 86 | $finished = finishedAssignments.toggle(id); 87 | } else { 88 | $pending = pendingAssignments.toggle(id); 89 | } 90 | } 91 | const handleDelete = async (id, subject, assignment) => { 92 | if (!confirm(`Are you sure you want to delete the assignment "${subject} - ${assignment}"?`)) return; 93 | if (!localStorage.getItem("token")) { 94 | localStorage.setItem("token", prompt("Please enter your password")); 95 | } 96 | const res = await fetch(`/api/assignments/${id}`, { 97 | method: "DELETE", 98 | headers: { 99 | "Authorization": "Bearer " + localStorage.getItem("token"), 100 | } 101 | }); 102 | if (res.status === 401) { 103 | localStorage.removeItem("token"); 104 | alert("Invalid password"); 105 | } 106 | } 107 | const handleEditAssignment = (assignment) => { 108 | if ($isEditMode) { 109 | $activeAssignment = assignment; 110 | $isModalActive = true; 111 | } 112 | } 113 | confetti({ 114 | particleCount: 400, 115 | spread: 100, 116 | resize: true, 117 | origin: { y: 0.6 } 118 | }); 119 | return ( 120 |
121 | 122 | CSED 2024 Assignments 123 | 127 | 128 |
129 |
130 |

131 | CSED 2024 Assignments 132 | 133 | GitHub stars 139 | 140 |

141 | 142 | A service powered by 143 | Mango! 144 | 145 |
146 |
147 |
148 | {$isEditMode ? ( 149 | 157 | Tap to exit edit mode! 158 | 159 | ) : $notificationsState === "default" ? ( 160 | 168 | Tap to enable notifications to get notified when an assignment is due! 169 | 170 | ) : $notificationsState === "denied" ? ( 171 | 178 | Notifications are disabled. Please enable them in your browser settings. 179 | 180 | ) : null} 181 | {$loading ? ( 182 |

Loading...

183 | ) : ( 184 |
185 |
186 | 187 | 188 | 189 |
190 | 191 | 192 | 195 | 198 | 199 | {$data.map((item) => ( 200 | (($showTodo && !$pending.includes(item[0]) && !$finished.includes(item[0])) || 201 | ($showPending && $pending.includes(item[0])) || 202 | ($showFinished && $finished.includes(item[0]))) && ( 203 | handleEditAssignment(item)}> 204 | 243 | 249 | 250 | ) 251 | ))} 252 |
193 |
Assignment
194 |
196 |
Date
197 |
208 | {item[1]} → {item[4] ? ( 209 | e.stopPropagation()}> 210 | {item[2]} 211 | 212 | ) : ( 213 | item[2] 214 | )} 215 | {$isEditMode ? ( 216 | 226 | ) : ( 227 | 241 | )} 242 | 247 | {dateUtils.dateToString(new Date(item[3]), item[5], item[6])} 248 |
253 |
254 |
255 |
256 |

257 | Teams Codes : 258 |

259 |
    260 | {teamsCodes.map((item) => ( 261 |
  • 262 | {item[0]} → {item[1]} 263 |
  • 264 | ))} 265 |
266 |
267 |

268 | Drive : 269 |

270 | 271 | CSED 2024 272 | 273 |
274 |
275 | )} 276 |
277 |
278 |

279 | Made with ❤️ by GeeekyBoy in Egypt 280 |
281 | Copyright © {new Date().getFullYear()} GeeekyBoy 282 |

283 |
284 |
285 | 286 | {$isEditMode ? ( 287 | 288 | ) : ( 289 | 290 | )} 291 | 292 | 293 |
294 | ); 295 | } 296 | 297 | export default App; 298 | --------------------------------------------------------------------------------