├── .env.example
├── .eslintignore
├── .eslintrc
├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── create-poc-for-new-component---item.md
│ ├── design-new-component-or-item.md
│ ├── feature_request.md
│ ├── implement-new-component---item.md
│ ├── new-dashboard-page.md
│ └── task-template.md
├── .gitignore
├── .storybook
├── iesd.js
├── main.js
├── manager.js
├── preview-head.html
└── webpack.config.js
├── .stylelintignore
├── .stylelintrc.json
├── LICENSE.md
├── README.md
├── email
├── CSS.ts
├── partials
│ └── Footer.tsx
└── templates
│ ├── GenericEmailTemplate.tsx
│ └── main
│ ├── confirmationEmail.tsx
│ └── resetPasswordEmail.tsx
├── index.d.ts
├── jest.config.js
├── jest.setup.js
├── jsconfig.json
├── lib
├── auth.ts
├── db.ts
└── redis.ts
├── models
├── course.ts
├── courseMeta.ts
├── lesson.ts
├── lessonMeta.ts
├── message.ts
├── notification.ts
├── options.ts
├── user.ts
└── userMeta.ts
├── next-env.d.ts
├── next-seo.config.js
├── next.config.js
├── package.json
├── pages
├── _app.tsx
├── _document.tsx
├── api
│ ├── authenticate
│ │ ├── auth.ts
│ │ ├── login.ts
│ │ ├── logout.ts
│ │ ├── reset.ts
│ │ └── signup.ts
│ ├── course-students
│ │ └── index.ts
│ ├── course
│ │ ├── add.ts
│ │ └── addPicture.ts
│ ├── mail.ts
│ ├── messages.ts
│ ├── meta.ts
│ ├── notifications.ts
│ ├── setup.ts
│ ├── user
│ │ ├── personal.ts
│ │ └── upload.ts
│ └── validate
│ │ ├── confirm.ts
│ │ ├── email.ts
│ │ └── user.ts
├── authenticate.tsx
├── bookmarks
│ └── index.tsx
├── community
│ └── index.tsx
├── confirmation.tsx
├── courses
│ ├── add.tsx
│ ├── categories.tsx
│ ├── index.tsx
│ ├── manage.tsx
│ └── tags.tsx
├── dashboard
│ ├── index.tsx
│ ├── students
│ │ └── index.tsx
│ └── updates.tsx
├── index.tsx
├── lessons
│ └── index.tsx
├── logged-out.tsx
├── messages
│ └── index.tsx
├── reset-password.tsx
├── resources
│ └── index.tsx
├── settings
│ ├── account.tsx
│ ├── preferences.tsx
│ └── profile.tsx
└── welcome.tsx
├── public
├── fonts
│ ├── AvenirLTStd-Black.otf
│ ├── AvenirLTStd-Book.otf
│ ├── AvenirLTStd-Heavy.otf
│ ├── AvenirLTStd-Light.otf
│ ├── AvenirLTStd-Medium.otf
│ └── Big-John.otf
├── images
│ ├── authentication-screenshot.png
│ ├── avatars
│ │ ├── david-8-avatar.jpg
│ │ └── placeholder_image.png
│ ├── coursePicture
│ │ ├── upload_15eab51c419a5e174bce3860f42d1ac8
│ │ ├── upload_31cfd508e85dd3586cdc328fd593a261
│ │ ├── upload_35984330de0154260d732bc550a8ecdd
│ │ ├── upload_3d5a1354820f567fbf009de45b4530ed
│ │ ├── upload_606e0dfc76128734e971591e573f8257
│ │ ├── upload_a13f2281b60262fe66fdb226cafbbec7
│ │ ├── upload_a447bbd89145ec5ef69a3f2618eed98f
│ │ ├── upload_a5dec182d7c8841843a7ee894aec0856
│ │ ├── upload_b31a54e4e9ec9b82460eaeed7b96e5d7
│ │ ├── upload_f6c37dc523bce5af630f65380130b7bf
│ │ └── upload_fa51b1b0c5d8c332ed9e4807542f2bfa
│ ├── illustrations
│ │ ├── access-granted.gif
│ │ └── forbidden.gif
│ ├── landing
│ │ ├── landingcover.jpg
│ │ ├── landingcover2.jpg
│ │ └── riverside-sample.jpg
│ ├── logo
│ │ ├── spark-360x360.png
│ │ ├── spark-text-carbon.svg
│ │ ├── spark-text-snow.svg
│ │ └── spark-text-white.svg
│ └── spark.png
└── scripts
│ └── upload.js
├── src
├── components
│ ├── animation
│ │ ├── Loader
│ │ │ ├── Loader.scss
│ │ │ ├── Loader.stories.tsx
│ │ │ └── Loader.tsx
│ │ └── Redirect
│ │ │ ├── Redirect.scss
│ │ │ ├── Redirect.stories.tsx
│ │ │ └── Redirect.tsx
│ ├── authenticate
│ │ ├── LogIn
│ │ │ ├── LogIn.stories.tsx
│ │ │ ├── LogIn.tsx
│ │ │ └── Login.scss
│ │ ├── LogOut
│ │ │ ├── LogOut.scss
│ │ │ ├── LogOut.stories.tsx
│ │ │ └── LogOut.tsx
│ │ ├── Password
│ │ │ ├── Password.scss
│ │ │ ├── Password.stories.tsx
│ │ │ └── Password.tsx
│ │ ├── Reset
│ │ │ ├── Reset.scss
│ │ │ ├── Reset.stories.tsx
│ │ │ └── Reset.tsx
│ │ └── SignUp
│ │ │ ├── SignUp.scss
│ │ │ ├── SignUp.stories.tsx
│ │ │ └── SignUp.tsx
│ ├── course
│ │ ├── Course.scss
│ │ └── Course.tsx
│ ├── courses-navigation
│ │ ├── Navigation.scss
│ │ └── Navigation.tsx
│ ├── courses
│ │ ├── Courses.scss
│ │ ├── Courses.stories.tsx
│ │ └── Courses.tsx
│ ├── dashboard
│ │ ├── home
│ │ │ ├── DashboardHome.scss
│ │ │ ├── DashboardHome.stories.tsx
│ │ │ └── DashboardHome.tsx
│ │ └── panels
│ │ │ └── addCourse
│ │ │ ├── AddCourse.scss
│ │ │ └── AddCourse.tsx
│ ├── global
│ │ ├── Authorized
│ │ │ ├── Authorized.scss
│ │ │ ├── Authorized.stories.tsx
│ │ │ ├── Authorized.tsx
│ │ │ ├── _note_design.md
│ │ │ └── _note_intro.md
│ │ ├── ContentContainer
│ │ │ └── ContentContainer.tsx
│ │ ├── Footer
│ │ │ ├── Footer.scss
│ │ │ ├── Footer.stories.tsx
│ │ │ └── Footer.tsx
│ │ ├── Message
│ │ │ ├── Message.scss
│ │ │ ├── Message.stories.tsx
│ │ │ └── Message.tsx
│ │ ├── Navigation
│ │ │ ├── Navigation.scss
│ │ │ ├── Navigation.stories.tsx
│ │ │ ├── Navigation.tsx
│ │ │ └── User
│ │ │ │ ├── Messages
│ │ │ │ └── Messages.tsx
│ │ │ │ ├── Notifications
│ │ │ │ └── Notifications.tsx
│ │ │ │ └── User.tsx
│ │ ├── Sidebar
│ │ │ ├── Sidebar.scss
│ │ │ ├── Sidebar.stories.tsx
│ │ │ ├── Sidebar.tsx
│ │ │ └── usage.md
│ │ ├── Spinner
│ │ │ ├── Spinner.scss
│ │ │ ├── Spinner.stories.tsx
│ │ │ └── Spinner.tsx
│ │ └── Unauthorized
│ │ │ ├── Unauthorized.scss
│ │ │ ├── Unauthorized.stories.tsx
│ │ │ └── Unauthorized.tsx
│ ├── landing
│ │ ├── CourseProgramCard.tsx
│ │ ├── Landing.scss
│ │ ├── Landing.stories.tsx
│ │ └── Landing.tsx
│ ├── layouts
│ │ ├── DashboardLayout.scss
│ │ └── DashboardLayout.tsx
│ ├── manage-students
│ │ ├── ManageStudents.scss
│ │ ├── ManageStudents.stories.tsx
│ │ └── ManageStudents.tsx
│ ├── panel
│ │ ├── Panel.scss
│ │ └── Panel.tsx
│ ├── user-info-input
│ │ ├── UserInfoInput.scss
│ │ ├── UserInfoInput.stories.tsx
│ │ ├── UserInfoInput.tsx
│ │ ├── phone-number
│ │ │ └── PhoneNumber.tsx
│ │ └── sanitize
│ │ │ └── sanitize.ts
│ └── utility
│ │ └── Notify.ts
├── context.ts
├── pages.ts
└── style
│ ├── _animation.scss
│ ├── _colors.scss
│ ├── _global.scss
│ ├── _typography.scss
│ ├── _variables.scss
│ ├── index.scss
│ └── pages
│ ├── _dashboard.scss
│ ├── _index.scss
│ └── _welcome.scss
├── tsconfig.json
└── yarn.lock
/.env.example:
--------------------------------------------------------------------------------
1 | # To enable your project environment, rename the file to ".env" (remove the ".example" suffix)
2 | # and populate the incomplete values
3 | # Please ask project administrator if you have concerns about values to use
4 |
5 | #environment
6 | HOST=http://localhost:3000/
7 |
8 | #api
9 | SECRET=
10 |
11 | #mysql
12 | DBHOST=
13 | DBNAME=
14 | DBUSER=
15 | DBPASSWORD=
16 |
17 | #redis
18 | REDISPORT=
19 | REDISIP=
20 | REDISPASS=
21 |
22 | #jwt - Secret for signing JWTs
23 | JWTKEY=
24 |
25 | #mail - SMTP configuration for sending emails (registration, verification, etc)
26 | MAILHOST=
27 | MAILPORT=
28 | MAILUSER=
29 | MAILPASS=
30 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | sass/vendors
2 | .next/
3 | .idea/
4 | .vs/
5 | assets/
6 | docs/
7 | node_modules/
8 | node_modules/*
9 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "plugins": [
4 | "@typescript-eslint",
5 | "react",
6 | "react-hooks"
7 | ],
8 | "extends": [
9 | "google",
10 | "plugin:react/recommended",
11 | "plugin:@typescript-eslint/recommended"
12 | ],
13 | "parserOptions": {
14 | "ecmaVersion": 2019,
15 | "sourceType": "module",
16 | "ecmaFeatures": {
17 | "jsx": true
18 | }
19 | },
20 | "env": {
21 | "es6": true,
22 | "browser": true,
23 | "node": true,
24 | "jest": true
25 | },
26 | "settings": {
27 | "react": {
28 | "version": "detect"
29 | }
30 | },
31 | "rules": {
32 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".ts", "tsx"] }],
33 | "@typescript-eslint/explicit-function-return-type": "off",
34 | "@typescript-eslint/explicit-member-accessibility": "off",
35 | "@typescript-eslint/no-explicit-any": "off",
36 | "@typescript-eslint/no-var-requires": "off",
37 | "max-len": "off",
38 | "no-mixed-spaces-and-tabs": 2,
39 | "quotes": 0,
40 | "require-jsdoc": 0,
41 | "one-var": 0,
42 | "react/prop-types": 0,
43 | "react/react-in-jsx-scope": 0,
44 | "strict": 2,
45 | "camelcase": 0,
46 | "linebreak-style": 0
47 | },
48 | "overrides": []
49 | }
50 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/create-poc-for-new-component---item.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Create POC for new component / item
3 | about: New task for creating POC of a design for a component or item
4 | title: Proof of concept for [name of component] component
5 | labels: story, task, testing
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Components that require updating:**
11 | - New component name
12 |
13 | **Tech to be used:**
14 | - UIKit
15 | - HTML/SASS
16 | - Fontawesome Pro
17 |
18 | **What is the current behavior:**
19 | We currently don't have a User information input panel component.
20 |
21 | **Description:**
22 | Create a proof of concept using HTML/CSS. Take the design PDF and convert it to code. You will be using UIKit as the CSS library along with Fontawesome PRO icons. Any comments you need to make, make them in the discussion of the issue or the implementation issue for this component.
23 |
24 | The expected deliverable will be a component that is responsive and mobile ready. Preferably an HTML file. No comments. No inline CSS. No JavaScript unless otherwise informed. Next task will deal with converting this to a component in React.
25 |
26 | **Justification:** Vital component
27 |
28 | **Notes:**
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/design-new-component-or-item.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Design new component or item
3 | about: For design tasks for new components or items
4 | title: Design [component name] component
5 | labels: task
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Components that require updating:**
11 | - [component name]: new component
12 |
13 | **Tech to be used:**
14 | - Design tools
15 |
16 | **What is the new component or item:**
17 | Create a design PDF for developers to create a static copy with code. You will be using UIKit components along with Fontawesome PRO icons. Any comments you need to make, make them in the discussion of the issue or the implementation issue for this component.
18 |
19 | The expected deliverable will be a design documents that shows all the possible states it has. Provide notes as necessary.
20 |
21 | **Justification:** New component
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/implement-new-component---item.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Implement new component / item
3 | about: 'Implementation of [name of component] component. '
4 | title: Implementation of [name of component] component.
5 | labels: task
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Components that require updating:**
11 | - Navigation - new component
12 |
13 | **Tech to be used:**
14 | - Next.js
15 | - React
16 | - Typescript
17 |
18 | **What is the expected deliverable:**
19 | Take the proof of concept (HTML/CSS) and implement it. This means take it from static and add React. Build it to be modular and without dependencies on other components.
20 |
21 | **Justification:** vital component
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/new-dashboard-page.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: New Dashboard Page
3 | about: 'New dashboard page '
4 | title: Dashboard page - [insert title]
5 | labels: task
6 | assignees: ''
7 |
8 | ---
9 |
10 | This is a three part task:
11 | - [ ] Design the new page
12 | - [ ] Create the proof of concept
13 | - [ ] Implement the proof of concept
14 |
15 | ### **Design**
16 | **What is the new component or item:**
17 | Create a design PDF for developers to create a static copy with code. You will be using UIKit components along with Fontawesome PRO icons. Any comments you need to make, make them in the discussion of the issue or the implementation issue for this component.
18 |
19 | The expected deliverable will be a design documents that shows all the possible states it has. Provide notes as necessary.
20 |
21 | ### ** Proof of Concept**
22 | Create a proof of concept using Storybook.js. Take the design PDF and convert it to code. You will be using UIKit as the CSS library along with Fontawesome PRO icons. Do not worry about logic during this step, just build out the component.
23 |
24 | The expected deliverable will be a component that is responsive and mobile-ready. Make sure you address all stories requested (states). The next task will deal with adding any additional logic that might be needing. For example, creating API requests, iterating through data, etc.
25 |
26 | ### **Implement the Proof of Concept**
27 | Take the proof of concept and add missing logic. This means take it from static and make it dynamic if it requires it. Build it to be modular, add comments and make sure component is polished.
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/task-template.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Task template
3 | about: Create a task
4 | title: "[Actionable task title]"
5 | labels: task
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Components that require updating:**
11 | - component
12 |
13 | **Tech to be used:**
14 | - tech
15 |
16 | **What is the current behavior:**
17 |
18 | **What is the new behavior:**
19 |
20 | **Justification:**
21 |
22 | **Notes:**
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vs
2 | *.log
3 | .env
4 | .next
5 | .idea
6 | .vscode
7 | node_modules
8 | yarn-error.log
--------------------------------------------------------------------------------
/.storybook/iesd.js:
--------------------------------------------------------------------------------
1 | import {create} from '@storybook/theming/create';
2 |
3 | export default create({
4 | base: 'dark',
5 | brandTitle: 'IESD',
6 | brandUrl: 'https://iesd.com',
7 | brandImage: 'https://iesd.com/static/logos/iesd-initials-white.svg',
8 | });
9 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | stories: ['../src/components/**/*.stories.tsx'],
3 | addons: [
4 | '@storybook/addon-actions/register',
5 | '@storybook/addon-knobs/register',
6 | '@storybook/addon-notes/register',
7 | '@storybook/addon-storysource',
8 | '@storybook/addon-a11y/register',
9 | '@storybook/addon-viewport/register',
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/.storybook/manager.js:
--------------------------------------------------------------------------------
1 | import {addons} from '@storybook/addons';
2 | import iesd from './iesd';
3 |
4 | addons.setConfig({
5 | theme: iesd,
6 | });
7 |
--------------------------------------------------------------------------------
/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | // .storybook/webpack.config.js
2 |
3 | const path = require("path");
4 |
5 | module.exports = ({config}) => {
6 | config.module.rules.push({
7 | test: /\.(ts|tsx)$/,
8 | loader: require.resolve('babel-loader'),
9 | options: {
10 | presets: [require.resolve('babel-preset-react-app')],
11 | },
12 | });
13 |
14 | config.module.rules.push({
15 | test: /\.(png|woff|woff2|eot|otf|ttf|svg)$/,
16 | loaders: ['file-loader'],
17 | include: path.resolve(__dirname, '../'),
18 | });
19 |
20 | config.module.rules.push({
21 | test: /\.s?css$/,
22 | use: [
23 | 'style-loader',
24 | 'css-loader',
25 | {
26 | loader: 'sass-loader',
27 | options: {
28 | // Prefer `dart-sass`
29 | implementation: require('sass'),
30 | },
31 | },
32 | {
33 | loader: 'sass-resources-loader',
34 | options: {
35 | resources: "src/style/_variables.scss",
36 | },
37 | },
38 | ],
39 | include: [
40 | path.resolve(__dirname, "../"),
41 | ],
42 | });
43 |
44 | config.resolve.extensions.push('.ts', '.tsx');
45 | return config;
46 | };
47 |
--------------------------------------------------------------------------------
/.stylelintignore:
--------------------------------------------------------------------------------
1 | sass/vendors
2 | email
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-standard",
3 | "rules": {
4 | "max-line-length": null,
5 | "no-descending-specificity": null,
6 | "declaration-block-trailing-semicolon": null,
7 | "at-rule-no-unknown": null
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 lloan alas
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 | Spark
9 | Learning Management System
10 |
11 |
12 |
13 | Spark is an open source learning management system, designed and built for Spark, a digital skills initiative led by a southern California nonprofit, Inland Empire Software Development, Inc. We promote free and open source software.
14 |
15 |
16 |
17 | ## 👨💻 **Getting Started**
18 |
19 | To get started, make sure to fill out the `.env` file with the appropriate information. Make sure that all prerequisites are met or application won't operate correctly.
20 |
21 | The application has two parts to it - the application itself and storybook.
22 |
23 | 
24 |
25 | ## ⛑ **Getting Help**
26 |
27 | Any questions or for support, please open up an issue and label it appropriately `support`.
28 |
29 | If you have any questions, please ask on our [Slack](https://ie-sd.slack.com). We welcome everyone to our Slack, so don't be afraid to join! Have fun coding!
30 |
31 | **Join link **
32 |
33 | ## ⟲ **Prerequisites**
34 |
35 | The minimum requirements to run this include:
36 |
37 | - Redis
38 | - MySQL
39 | - Mail SMTP provider
40 | - Yarn
41 |
42 | ## 🌐 **Installing**
43 |
44 | A step by step series of examples that tell you how to get a development env running.
45 |
46 | 1. Update your `.env` file with the required information. Make sure the mysql and redis information is correct. The mail information is used for the mail system, make sure that is correct or you won't receive account creation verification email or password reset emails. You also need to create a JWT key and an API secret key.
47 |
48 | .env example input
49 | ```
50 | JWTKEY=Ds~4lq~}.?*f~Ql$42J%aR7%SoKxaN
51 | SECRET=SklZvh3a0PwQev901A1zT23vlG
52 | ```
53 |
54 | 2. Install required packages
55 |
56 | ```
57 | yarn install
58 | ```
59 |
60 | 3. Start local development environment.
61 | ```
62 | yarn dev
63 | ```
64 |
65 | 4. Start storybook instance
66 | ```
67 | yarn storybook
68 | ```
69 |
70 | ## 💡 **Features**
71 | - Pages for authentication (login, logout, reset), dashboard, welcome, reset password, confirmation, home and logged-out. HTTPOnly token (JWT) used.
72 | - Protected pages which can be turned on and off by page.
73 | - Redirection system based on user authentication state.
74 | - Email system with very simple template system.
75 | - SASS based.
76 | - Typescript based, makes development a breeze.
77 | - UIKit notification system.
78 | - Models directory and option for initial DB setup.
79 | - ESLint and Stylelint for development.
80 | - React Context handles state at the parent level.
81 | - Storybook integration for self documentation and design system generation.
82 |
83 | ## 🔨 **Built With**
84 | * Next.js
85 | * MySQL
86 | * Redis
87 | * Typescript
88 | * SASS
89 | * UIKit - A clean CSS library
90 | * Nodemailer
91 | * Storybook
92 |
93 | For development:
94 |
95 | * ESLint
96 | * Stylelint
97 | * Jest
98 |
99 | ## 🤝 **Contributing**
100 | All contributions are welcome and we invite everyone to open issues for suggestions and/or bugs.
101 |
102 | - If you want to contribute, please open up an issue.
103 | - If you start working on a feature, branch off of the `master` branch as it should be the most up-to-date.
104 |
105 | ## 📓 **Development Team**
106 |
107 | * **👤 Lloan Alas**
108 | * **👤 Jacob Goodwin**
109 | * **👤 Tony Nguyen**
110 | * **👤 Andy Mendez**
111 | * **👤 David Huang**
112 | * **👤 Katherine Orho**
113 | * **👤 Greg Rojas**
114 | * **👤 Raul Jauregui**
115 |
116 | See also the list of [contributors](https://github.com/lloan/next-authenticate/graphs/contributors) who participated in this project.
117 |
118 | ## 🗒 **License**
119 |
120 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
121 |
--------------------------------------------------------------------------------
/email/CSS.ts:
--------------------------------------------------------------------------------
1 | const uikitcss = `
2 | html,
3 | body {
4 | margin: 0 auto !important;
5 | padding: 0 !important;
6 | height: 100% !important;
7 | width: 100% !important;
8 | }
9 | * {
10 | -ms-text-size-adjust: 100%;
11 | -webkit-text-size-adjust: 100%;
12 | }
13 | div[style*="margin: 16px 0"] {
14 | margin: 0 !important;
15 | }
16 | #MessageViewBody, #MessageWebViewDiv{
17 | width: 100% !important;
18 | }
19 | table,
20 | td {
21 | mso-table-lspace: 0pt !important;
22 | mso-table-rspace: 0pt !important;
23 | }
24 | table {
25 | border-spacing: 0 !important;
26 | border-collapse: collapse !important;
27 | table-layout: fixed !important;
28 | margin: 0 auto !important;
29 | }
30 | img {
31 | -ms-interpolation-mode:bicubic;
32 | }
33 | a {
34 | text-decoration: none;
35 | }
36 | a[x-apple-data-detectors],
37 | .unstyle-auto-detected-links a,
38 | .aBn {
39 | border-bottom: 0 !important;
40 | cursor: default !important;
41 | color: inherit !important;
42 | text-decoration: none !important;
43 | font-size: inherit !important;
44 | font-family: inherit !important;
45 | font-weight: inherit !important;
46 | line-height: inherit !important;
47 | }
48 | .a6S {
49 | display: none !important;
50 | opacity: 0.01 !important;
51 | }
52 | .im {
53 | color: inherit !important;
54 | }
55 | img.g-img + div {
56 | display: none !important;
57 | }
58 | @media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
59 | u ~ div .email-container {
60 | min-width: 320px !important;
61 | }
62 | }
63 | @media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
64 | u ~ div .email-container {
65 | min-width: 375px !important;
66 | }
67 | }
68 | @media only screen and (min-device-width: 414px) {
69 | u ~ div .email-container {
70 | min-width: 414px !important;
71 | }
72 | }
73 | `;
74 |
75 | export default uikitcss;
76 |
--------------------------------------------------------------------------------
/email/partials/Footer.tsx:
--------------------------------------------------------------------------------
1 | function Footer() {
2 | return (
3 |
4 |
5 |
6 |
7 |
8 | Inland Empire Software Development, Inc.3499 Tenth St. Riverside, CA, 92501 US (800)437-0267
9 |
10 |
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | export default Footer;
18 |
--------------------------------------------------------------------------------
/email/templates/GenericEmailTemplate.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import mailCSS from "../CSS";
3 | import Footer from "../partials/Footer";
4 |
5 | function GenericEmailTemplate(props: {content: JSX.Element; title: string}) {
6 | const {content, title} = props;
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 | Password Reset
15 |
18 |
19 |
20 |
21 |
22 | {title}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {content}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 | }
47 |
48 |
49 | export default GenericEmailTemplate;
50 |
--------------------------------------------------------------------------------
/email/templates/main/confirmationEmail.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import GenericEmailTemplate from "../GenericEmailTemplate";
3 |
4 |
5 | const ConfirmationEmail = (options: { url: string; username: string; token: string; title: string }) => {
6 | const {url, username, token, title} = options;
7 |
8 | if (!url || !username || !token) return (An error has occurred, please contact the administrator
);
9 |
10 | return (
11 |
13 |
14 |
15 | {title}
16 | Hello {username},
17 | This email was used to sign up for our service.
18 | Please confirm that it was you.
19 | After you confirm, you will be able to sign in to your account.
20 | Confirm
21 |
22 |
23 |
24 |
25 | } />
26 | );
27 | };
28 |
29 | export default ConfirmationEmail;
30 |
--------------------------------------------------------------------------------
/email/templates/main/resetPasswordEmail.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import GenericEmailTemplate from "../GenericEmailTemplate";
3 |
4 | const ResetPasswordEmail = (options: { url: string; username: string; token: string; email: string; title: string }) => {
5 | const {url, username, token, email, title} = options;
6 |
7 | if (!url || !username || !email || !token) return (An error has occurred, please contact the administrator
);
8 |
9 | return (
10 |
12 |
13 |
14 | {title}
15 | Hello {username},
16 | A password reset request was submitted for your account.
17 | To keep your account safe, this email has been sent with a link to reset your password.
18 | If this request was not initiated by you, please contact an administrator.
19 | Reset Password
20 |
21 |
22 |
23 |
24 | }/>
25 | );
26 | };
27 |
28 | export default ResetPasswordEmail;
29 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import {Component} from "react";
2 |
3 | export interface CreateTable {
4 | createTable: (TableName: string, Query: string) => Function;
5 | }
6 | export interface Message {
7 | status: boolean;
8 | message: string;
9 | }
10 |
11 | export interface DBUpdateUser {
12 | [userName?: string]: string;
13 | [password?: string]: string;
14 | [email?: string]: string;
15 | [role?: string]: UserRoles;
16 | }
17 |
18 | export enum UserRoles {
19 | subscriber = 'subsciber'
20 | }
21 |
22 | // Allow arrays to be indexed with strings
23 | export interface ArrayIndexedWithStrings {
24 | [key: string]: any;
25 | }
26 |
27 | export interface Redirects {
28 | [page: string]: {
29 | redirect: string | undefined;
30 | };
31 | }
32 |
33 | export interface Notification extends UIkit.Notify {
34 | notification: Function;
35 | }
36 |
37 | export default class NextAuthenticate extends Component { }
38 |
39 | export interface MyAppContext {
40 | setContextProperty: function | undefined;
41 | user: string | undefined;
42 | sidebarIsOpen: boolean;
43 | notifications: boolean;
44 | activeDashboardPath: string | undefined;
45 | activeDashboardMenus: Map;
46 | access: boolean;
47 | redirect: string | undefined;
48 | userID: string | undefined;
49 | isPublic: boolean;
50 | }
51 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | setupFiles: ['/jest.setup.js'],
4 | testEnvironment: 'node',
5 | };
6 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | const Enzyme = require('enzyme');
2 | const Adapter = require('enzyme-adapter-react-16');
3 |
4 | Enzyme.configure({adapter: new Adapter()});
5 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "target": "ES6",
5 | "lib": ["es6"]
6 | },
7 | "exclude": [
8 | "node_modules",
9 | "**/node_modules/*"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/lib/auth.ts:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const bcrypt = require('bcryptjs');
3 | const auth: any = {}; // TODO: Find a suitable type
4 |
5 | auth.verifyPassword = (password: string, hash: string) => bcrypt.compareSync(password, hash);
6 |
7 | auth.tokenize = (username: string) => jwt.sign({username}, process.env.JWTKEY, {
8 | algorithm: 'HS256',
9 | expiresIn: 300,
10 | });
11 |
12 | export default auth;
13 |
--------------------------------------------------------------------------------
/lib/redis.ts:
--------------------------------------------------------------------------------
1 | const redis = require("redis");
2 | const bluebird = require('bluebird');
3 |
4 | bluebird.promisifyAll(redis.RedisClient.prototype);
5 | bluebird.promisifyAll(redis.Multi.prototype);
6 |
7 | const client = redis.createClient({
8 | port: process.env.REDISPORT,
9 | host: process.env.REDISIP,
10 | password: process.env.REDISPASS,
11 | db: process.env.REDISDB,
12 | });
13 |
14 | client.setToken = (token: string, identifier: {user: string; userID: string}) => {
15 | console.log(JSON.stringify(identifier));
16 | return client.setAsync(String(token), JSON.stringify(identifier));
17 | };
18 |
19 | client.checkToken = (token: string, userToCheck: {user: string; userID: string}) => {
20 | console.log(token);
21 | return client.getAsync(String(token)).then(function(user: null | string) {
22 | const data = user !== null ? JSON.parse(user) : null;
23 | return data === null ? false :
24 | (userToCheck.user === data.user && Number(userToCheck.userID) === Number(data.userID));
25 | });
26 | };
27 |
28 | export default client;
29 |
--------------------------------------------------------------------------------
/models/course.ts:
--------------------------------------------------------------------------------
1 | import {CreateTable} from "..";
2 |
3 | /**
4 | * Creates the course database table. Only one row per course.
5 | * @param {db} db
6 | */
7 | const course = async (db: CreateTable) => {
8 | const query = `
9 | CREATE TABLE ${process.env.DBNAME}.course (
10 | course_id BIGINT(20) NOT NULL AUTO_INCREMENT,
11 | code VARCHAR(45) NULL,
12 | status TINYINT NOT NULL DEFAULT 0,
13 | name VARCHAR(150) NOT NULL,
14 | instructor VARCHAR(10) NOT NULL,
15 | students_enrolled VARCHAR(255) NOT NULL,
16 | PRIMARY KEY (course_id),
17 | UNIQUE INDEX name_UNIQUE (name ASC),
18 | UNIQUE INDEX course_id_UNIQUE (course_id ASC),
19 | UNIQUE INDEX code_UNIQUE (code ASC)
20 | )`;
21 |
22 | return await db.createTable("course", query);
23 | };
24 |
25 | export default course;
26 |
--------------------------------------------------------------------------------
/models/courseMeta.ts:
--------------------------------------------------------------------------------
1 | import {CreateTable} from "..";
2 |
3 | /**
4 | * Creates table for user course meta data.
5 | * Sample meta_keys: description, lessons, resources, students, etc..
6 | * Multiple rows per user. One per meta_key : meta_value pair. // TODO: Create CRUD methods to manage course keys/values
7 | * Visual: https://i.imgur.com/xJoPpH5.png - // TODO: Update this comment after task completed.
8 | * @param {db} db
9 | */
10 | const courseMeta = async (db: CreateTable) => {
11 | const query = `CREATE TABLE ${process.env.DBNAME}.course_meta
12 | (
13 | course_meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
14 | course_id bigint(20) unsigned NOT NULL DEFAULT '0',
15 | meta_key varchar(255) COLLATE utf8mb4_unicode_520_ci DEFAULT NULL,
16 | meta_value longtext COLLATE utf8mb4_unicode_520_ci,
17 | PRIMARY KEY (course_meta_id),
18 | KEY course_id (course_id),
19 | KEY meta_key (meta_key),
20 | UNIQUE INDEX course_meta_id_UNIQUE (course_meta_id)
21 | ) `;
22 |
23 | return await db.createTable("course_meta", query);
24 | };
25 |
26 | export default courseMeta;
27 |
28 |
--------------------------------------------------------------------------------
/models/lesson.ts:
--------------------------------------------------------------------------------
1 | import {CreateTable} from "..";
2 |
3 | /**
4 | * Creates the lesson database. Only one row per user.
5 | * @param {db} db
6 | */
7 | const lesson = async (db: CreateTable) => {
8 | const query = `
9 | CREATE TABLE ${process.env.DBNAME}.lesson (
10 | lesson_id BIGINT(20) NOT NULL AUTO_INCREMENT,
11 | code VARCHAR(45) NULL,
12 | status TINYINT NOT NULL DEFAULT 0,
13 | name VARCHAR(150) NOT NULL,
14 | PRIMARY KEY (lesson_id),
15 | UNIQUE INDEX name_UNIQUE (name ASC),
16 | UNIQUE INDEX lesson_id_UNIQUE (lesson_id ASC),
17 | UNIQUE INDEX code_UNIQUE (code ASC)
18 | )`;
19 |
20 | return await db.createTable("lesson", query);
21 | };
22 |
23 | export default lesson;
24 |
--------------------------------------------------------------------------------
/models/lessonMeta.ts:
--------------------------------------------------------------------------------
1 | import {CreateTable} from "..";
2 |
3 | /**
4 | * Creates table for user lesson meta data.
5 | * Sample meta_keys: description, lessons, resources, students, etc..
6 | * Multiple rows per user. One per meta_key : meta_value pair. // TODO: Create CRUD methods to manage lesson keys/values
7 | * Visual: https://i.imgur.com/xJoPpH5.png - // TODO: Update this comment after task completed.
8 | * @param {db} db
9 | */
10 | const lessonMeta = async (db: CreateTable) => {
11 | const query = `CREATE TABLE ${process.env.DBNAME}.lesson_meta
12 | (
13 | lesson_meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
14 | lesson_id bigint(20) unsigned NOT NULL DEFAULT '0',
15 | meta_key varchar(255) COLLATE utf8mb4_unicode_520_ci DEFAULT NULL,
16 | meta_value longtext COLLATE utf8mb4_unicode_520_ci,
17 | PRIMARY KEY (lesson_meta_id),
18 | KEY lesson_id (lesson_id),
19 | KEY meta_key (meta_key),
20 | UNIQUE INDEX lesson_meta_id_UNIQUE (lesson_meta_id)
21 | ) `;
22 |
23 | return await db.createTable("lesson_meta", query);
24 | };
25 |
26 | export default lessonMeta;
27 |
28 |
--------------------------------------------------------------------------------
/models/message.ts:
--------------------------------------------------------------------------------
1 | import {CreateTable} from "..";
2 |
3 | /**
4 | * Creates the message database table. Only one row per message.
5 | * @param {db} db
6 | */
7 | const message = async (db: CreateTable) => {
8 | const query = `
9 | CREATE TABLE ${process.env.DBNAME}.message (
10 | id INT NOT NULL AUTO_INCREMENT,
11 | timestamp DATETIME NOT NULL DEFAULT NOW(),
12 | sender_id INT NOT NULL,
13 | recipient_id INT NOT NULL,
14 | body VARCHAR(350) NOT NULL,
15 | is_read TINYINT NULL DEFAULT 0,
16 | expiry_date DATETIME NULL COMMENT 'This column is the cut-off date when reminders will no longer be sent to users.',
17 | recipient_group LONGTEXT NULL,
18 | is_reminder TINYINT NULL DEFAULT 0 COMMENT 'This column flags whether or not a reminder is required for the message.',
19 | next_remind_date DATETIME NULL COMMENT 'This column holds the date when the next reminder needs to be sent. The reminder will be sent on the next_remind_date for the users for whom the ‘is_read’ flag is still ZERO. A new value for this column will be calculated every time a reminder is sent.',
20 | reminder_frequency_id INT NULL COMMENT 'This column signifies the frequency of the reminder. Should it be on daily basis or weekly basis?',
21 | ephemeral TINYINT NULL DEFAULT 0 COMMENT 'If set to true, we will delete this message on specific date.',
22 | removal_date DATETIME NULL COMMENT 'Date for ephemeral message to be removed.',
23 | PRIMARY KEY (id),
24 | UNIQUE INDEX id_UNIQUE (id ASC)
25 | )`;
26 |
27 | return await db.createTable("message", query);
28 | };
29 |
30 | export default message;
31 |
--------------------------------------------------------------------------------
/models/notification.ts:
--------------------------------------------------------------------------------
1 | import {CreateTable} from "..";
2 |
3 | /**
4 | * Creates the notification database table. Only one row per notification.
5 | * @param {db} db
6 | */
7 | const notification = async (db: CreateTable) => {
8 | const query = `
9 | CREATE TABLE ${process.env.DBNAME}.notification (
10 | id INT NOT NULL AUTO_INCREMENT,
11 | user_id INT NOT NULL,
12 | title VARCHAR(150) NOT NULL,
13 | message VARCHAR(200) NOT NULL,
14 | is_read TINYINT NULL DEFAULT 0,
15 | type_id INT NOT NULL DEFAULT 0,
16 | PRIMARY KEY (id),
17 | UNIQUE INDEX id_UNIQUE (id ASC))
18 | `;
19 |
20 | return await db.createTable("notification", query);
21 | };
22 |
23 | export default notification;
24 |
--------------------------------------------------------------------------------
/models/options.ts:
--------------------------------------------------------------------------------
1 | import {CreateTable} from "..";
2 |
3 | /**
4 | * Creates table for site options.
5 | * Sample option_key: admin_email, home url, theme settings, logo, etc.
6 | * Multiple rows per user. One per meta_key : meta_value pair. // TODO: Create CRUD methods to manage option_keys/values
7 | * Visual: https://i.imgur.com/xJoPpH5.png - // TODO: Update this comment after task completed.
8 | * @param {db} db
9 | */
10 | const profile = async (db: CreateTable) => {
11 | const query = `CREATE TABLE ${process.env.DBNAME}.options
12 | (
13 | option_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
14 | meta_key varchar(255) COLLATE utf8mb4_unicode_520_ci NOT NULL,
15 | meta_value longtext COLLATE utf8mb4_unicode_520_ci NOT NULL,
16 | autoload tinyint NOT NULL DEFAULT 0,
17 | PRIMARY KEY (option_id),
18 | KEY key (meta_key),
19 | UNIQUE INDEX option_id_UNIQUE (option_id),
20 | UNIQUE INDEX meta_key_UNIQUE (meta_key),
21 | ) `;
22 |
23 | return await db.createTable("user_meta", query);
24 | };
25 |
26 | export default profile;
27 |
28 |
--------------------------------------------------------------------------------
/models/user.ts:
--------------------------------------------------------------------------------
1 | import {CreateTable} from "..";
2 |
3 | /**
4 | * Creates the user database table. Only one row per user.
5 | * @param {db} db
6 | */
7 | const user = async (db: CreateTable) => {
8 | const query = `
9 | CREATE TABLE ${process.env.DBNAME}.user (
10 | id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Unique user ID to identify a single unique user',
11 | username VARCHAR(35) NOT NULL COMMENT 'Username should be unique ',
12 | password VARCHAR(255) NOT NULL,
13 | email VARCHAR(100) NOT NULL,
14 | role VARCHAR(45) NOT NULL,
15 | last_login DATETIME NOT NULL DEFAULT now(),
16 | confirmation VARCHAR(50) NOT NULL,
17 | password_reset TINYINT NOT NULL DEFAULT 0,
18 | password_token VARCHAR(255),
19 | creation_date DATETIME NOT NULL DEFAULT now(),
20 | PRIMARY KEY (id),
21 | UNIQUE INDEX id_UNIQUE (id ASC),
22 | UNIQUE INDEX username_UNIQUE (username ASC),
23 | UNIQUE INDEX email_UNIQUE (email ASC))`;
24 |
25 | return await db.createTable("user", query);
26 | };
27 |
28 | export default user;
29 |
--------------------------------------------------------------------------------
/models/userMeta.ts:
--------------------------------------------------------------------------------
1 | import {CreateTable} from "..";
2 |
3 | /**
4 | * Creates table for user meta data.
5 | * Sample meta_keys: status, courses, quizzes, uploads, personal information, social, etc.
6 | * Multiple rows per user. One per meta_key : meta_value pair. // TODO: Create CRUD methods to manage meta_keys/values
7 | * Visual: https://i.imgur.com/xJoPpH5.png - // TODO: Update this comment after task completed.
8 | * @param {db} db
9 | */
10 | const userMeta = async (db: CreateTable) => {
11 | const query = `CREATE TABLE ${process.env.DBNAME}.user_meta
12 | (
13 | user_meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
14 | user_id bigint(20) unsigned NOT NULL DEFAULT '0',
15 | meta_key varchar(255) COLLATE utf8mb4_unicode_520_ci DEFAULT NULL,
16 | meta_value longtext COLLATE utf8mb4_unicode_520_ci,
17 | PRIMARY KEY (user_meta_id),
18 | KEY user_id (user_id),
19 | KEY meta_key (meta_key),
20 | UNIQUE INDEX user_meta_id_UNIQUE (user_meta_id)
21 | ) `;
22 |
23 | return await db.createTable("user_meta", query);
24 | };
25 |
26 | export default userMeta;
27 |
28 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | // /
2 | // /
3 |
--------------------------------------------------------------------------------
/next-seo.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/camelcase */
2 | export default {
3 | title: "Authentication System",
4 | openGraph: {
5 | type: 'website',
6 | locale: 'en_IE',
7 | url: 'https://www.github.com/lloan/next-authenticate',
8 | site_name: 'Authenticate',
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const {parsed: localEnv} = require("dotenv").config();
2 | const webpack = require("webpack");
3 | const StylelintPlugin = require('stylelint-webpack-plugin');
4 | const withCSS = require("@zeit/next-css");
5 | const withSASS = require("@zeit/next-sass");
6 | const withImages = require("next-images");
7 |
8 | module.exports = withImages(withCSS(withSASS({
9 | webpack(config) {
10 | config.plugins.push(new webpack.EnvironmentPlugin(localEnv));
11 | config.plugins.push(new StylelintPlugin({
12 | fix: true,
13 | }));
14 |
15 | return config;
16 | },
17 | })));
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spark-lms",
3 | "version": "0.0.1",
4 | "private": true,
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/inland-empire-software-development/spark"
8 | },
9 | "scripts": {
10 | "dev": "next dev",
11 | "setup": "node setup.js",
12 | "development": "next start -p 6001",
13 | "staging": "next start -p 6002",
14 | "production": "next start -p 6003",
15 | "build": "next build",
16 | "start": "next start",
17 | "lint": "./node_modules/.bin/eslint './**/*.{js,ts,tsx}' --quiet --fix || true",
18 | "test": "jest || true",
19 | "jest": "jest --watchAll",
20 | "reset": "rm -rf src/ sass/ pages/ .next/ node_modules/ yarn.lock models/ lib/ email/ && git reset --hard && yarn install",
21 | "storybook": "start-storybook -s ./public HOST=http://localhost:3000/ -p 6006 -c .storybook",
22 | "sbdev": "start-storybook -s ./public HOST=http://localhost:6001/ -p 7001 -c .storybook",
23 | "sbstg": "start-storybook -s ./public HOST=http://localhost:6002/ -p 7002 -c .storybook",
24 | "sbprod": "start-storybook -s ./public HOST=http://localhost:6003/ -p 7003 -c .storybook"
25 | },
26 | "dependencies": {
27 | "@types/formidable": "^1.0.31",
28 | "@types/react-image-crop": "^8.1.2",
29 | "@types/uikit": "^2.27.7",
30 | "@zeit/next-css": "^1.0.1",
31 | "@zeit/next-sass": "^1.0.1",
32 | "app-root-path": "^3.0.0",
33 | "babel-jest": "^24.9.0",
34 | "bcryptjs": "^2.4.3",
35 | "bluebird": "^3.7.2",
36 | "browser-image-compression": "^1.0.9",
37 | "dotenv": "^8.2.0",
38 | "formidable": "^1.2.2",
39 | "fs": "^0.0.1-security",
40 | "glob": "^7.1.6",
41 | "isomorphic-unfetch": "^3.0.0",
42 | "js-cookie": "^2.2.1",
43 | "jsonwebtoken": "^8.5.1",
44 | "multer": "^1.4.2",
45 | "mysql": "^2.17.1",
46 | "next": "^9.3.1",
47 | "next-images": "^1.2.0",
48 | "next-seo": "^4.4.0",
49 | "node-fetch": "^2.6.0",
50 | "node-sass": "^4.13.0",
51 | "nodemailer": "^6.4.1",
52 | "path": "^0.12.7",
53 | "react": "16.12.0",
54 | "react-image-crop": "^8.6.2",
55 | "redis": "^2.8.0",
56 | "sass": "^1.25.0",
57 | "sql-string-escape": "^1.1.6",
58 | "typescript": "^3.7.3",
59 | "uikit": "^3.3.6"
60 | },
61 | "devDependencies": {
62 | "@storybook/addon-a11y": "^5.3.7",
63 | "@storybook/addon-actions": "^5.3.6",
64 | "@storybook/addon-backgrounds": "^5.3.7",
65 | "@storybook/addon-centered": "^5.3.7",
66 | "@storybook/addon-knobs": "^5.3.6",
67 | "@storybook/addon-links": "^5.3.6",
68 | "@storybook/addon-notes": "^5.3.6",
69 | "@storybook/addon-storysource": "^5.3.7",
70 | "@storybook/addon-viewport": "^5.3.7",
71 | "@storybook/addons": "^5.3.6",
72 | "@storybook/cli": "^5.3.8",
73 | "@storybook/react": "^5.3.7",
74 | "@types/jest": "^24.9.0",
75 | "@types/marked": "^0.7.2",
76 | "@types/next": "^9.0.0",
77 | "@types/next-seo": "^1.10.0",
78 | "@types/node": "^12.12.17",
79 | "@types/react": "^16.9.16",
80 | "@types/react-dom": "^16.9.4",
81 | "@typescript-eslint/eslint-plugin": "^2.11.0",
82 | "@typescript-eslint/parser": "^2.11.0",
83 | "babel-eslint": "^10.0.3",
84 | "babel-loader": "^8.0.6",
85 | "babel-preset-react-app": "^9.1.0",
86 | "enzyme": "^3.11.0",
87 | "enzyme-adapter-react-16": "^1.15.2",
88 | "eslint": "^6.7.2",
89 | "eslint-config-google": "^0.14.0",
90 | "eslint-config-prettier": "^6.7.0",
91 | "eslint-loader": "^3.0.2",
92 | "eslint-plugin-css-modules": "^2.11.0",
93 | "eslint-plugin-import": "^2.18.2",
94 | "eslint-plugin-jsx-a11y": "^6.2.3",
95 | "eslint-plugin-react": "^7.16.0",
96 | "eslint-plugin-react-hooks": "^2.2.0",
97 | "jest": "^24.9.0",
98 | "marked": "^0.8.0",
99 | "postcss-loader": "^3.0.0",
100 | "react-addons-test-utils": "^15.6.2",
101 | "react-dom": "16.12.0",
102 | "react-test-renderer": "^16.12.0",
103 | "sass-loader": "^8.0.2",
104 | "sass-resources-loader": "^2.0.1",
105 | "style-loader": "^1.1.3",
106 | "stylelint-config-prettier": "^8.0.0",
107 | "stylelint-config-rational-order": "^0.1.2",
108 | "stylelint-config-standard": "^19.0.0",
109 | "stylelint-prettier": "^1.1.1",
110 | "stylelint-scss": "^3.13.0",
111 | "stylelint-webpack-plugin": "^1.1.2",
112 | "ts-jest": "^24.3.0"
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import '../src/style/index.scss';
2 | import App from 'next/app';
3 | import {Context, defaultContext} from '../src/context';
4 | import {redirects, unprotected} from '../src/pages';
5 | import fetch from "isomorphic-unfetch";
6 | import {DefaultSeo} from 'next-seo';
7 | import SEO from '../next-seo.config';
8 | import Unauthorized from "../src/components/global/Unauthorized/Unauthorized";
9 | import Redirect from "../src/components/animation/Redirect/Redirect";
10 | import Loader from "../src/components/animation/Loader/Loader";
11 | import {MyAppContext} from "../";
12 | import Spinner from '../src/components/global/Spinner/Spinner';
13 | export default class MyApp extends App<{}, {}, MyAppContext> {
14 | state: React.ComponentState = {
15 | ...defaultContext,
16 | }
17 |
18 | componentDidMount(): void {
19 | const {pathname} = this.props.router;
20 | const redirect = redirects[pathname] ? redirects[pathname].redirect : undefined;
21 | const isPublic = unprotected.includes(pathname);
22 |
23 | fetch(`${process.env.HOST}api/authenticate/auth`, {
24 | method: 'POST',
25 | })
26 | .then((res) => res.json())
27 | .then((data) => {
28 | // Allows us to set new values for context that is shared throughout the application
29 | const setContextProperty = (value: any) => {
30 | this.setState(value);
31 | };
32 | this.setState({...data, setContextProperty, redirect, isPublic, isAccessFetched: true});
33 | });
34 | };
35 |
36 | redirect(redirect: string) {
37 | setTimeout(() => {
38 | if ((process as any).browser) {
39 | document.location.href = redirect;
40 | }
41 | console.log('redirecting...');
42 | }, 2000);
43 | }
44 |
45 | render() {
46 | const {Component, pageProps} = this.props;
47 | const {access, redirect, isPublic, isAccessFetched} = this.state;
48 |
49 | if (isAccessFetched) {
50 | if (access && redirect) {
51 | // send user to proper page if they're logged in
52 | this.redirect(redirect);
53 | return ;
54 | } else if (access || isPublic) {
55 | return (
56 |
57 |
58 |
59 |
60 |
61 | );
62 | } else if (!access) {
63 | return ;
64 | } else {
65 | this.redirect("/authenticate");
66 | return ;
67 | }
68 | } else {
69 | return ;
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, {Html, Head, Main, NextScript} from 'next/document';
2 | /**
3 | * Shared components amongst all pages - resources needed placed here.
4 | */
5 | class Doc extends Document {
6 | /**
7 | * @return {JSX.Element}
8 | */
9 | render(): JSX.Element {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | export default Doc;
29 |
--------------------------------------------------------------------------------
/pages/api/authenticate/auth.ts:
--------------------------------------------------------------------------------
1 | import client from '../../../lib/redis';
2 | import {NextApiResponse, NextApiRequest} from 'next';
3 |
4 | export default async (req: NextApiRequest, res: NextApiResponse) => {
5 | const token = req.cookies['portal-token'];
6 | const user = req.cookies['portal-user'];
7 | const userID = req.cookies['portal-user-id'];
8 | const access = await client.checkToken(token, {user, userID});
9 |
10 | const data = {
11 | user, // user that will be rendered on the front-end
12 | userID,
13 | access, // is user authenticated / valid?
14 | };
15 |
16 | res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
17 | res.status(200);
18 | res.send(data);
19 | };
20 |
--------------------------------------------------------------------------------
/pages/api/authenticate/login.ts:
--------------------------------------------------------------------------------
1 | import db from '../../../lib/db';
2 | import auth from '../../../lib/auth';
3 | import client from '../../../lib/redis';
4 | import {Message} from '../../..';
5 | import {NextApiResponse, NextApiRequest} from 'next';
6 |
7 | export default async (req: NextApiRequest, res: NextApiResponse) => {
8 | // set headers
9 | res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
10 | res.setHeader("Content-Type", "json/javascript");
11 |
12 | // messages
13 | const invalid = {
14 | status: false,
15 | message: "invalid credentials",
16 | } as Message;
17 | const valid = {
18 | status: true,
19 | message: "access granted",
20 | } as Message;
21 | const unconfirmed = {
22 | status: false,
23 | message: "account has not been activated",
24 | } as Message;
25 |
26 | // Get credentials from JSON body
27 | const {username, password} = req.body;
28 |
29 | const user = await db.getUser(escape(username));
30 |
31 | res.status(200);
32 |
33 | // if user is not found, return early
34 | if (!user) {
35 | res.send(invalid);
36 | res.end();
37 | return false;
38 | }
39 |
40 | // if user found, verify password matches
41 | if (auth.verifyPassword(password, user.password)) {
42 | if (user.confirmation === "active") {
43 | // password matched - create new token
44 | const token = auth.tokenize(password);
45 |
46 | // set token in redis
47 | client.setToken(token, {user: username, userID: user.id});
48 |
49 | // update last login with true flag
50 | const updateLoginResp = await db.updateUser(user.id, {}, true);
51 |
52 | if (!updateLoginResp.status) {
53 | res.send(invalid);
54 | res.end();
55 | }
56 |
57 | // set HttpOnly cookies
58 | res.setHeader('Set-Cookie', [`portal-token=${token}; path=/; HttpOnly`, `portal-user=${username}; path=/; HttpOnly`, `portal-user-id=${user.id}; path=/; HttpOnly`]);
59 | res.send(valid);
60 | res.end();
61 | return true;
62 | } else {
63 | res.send(unconfirmed);
64 | res.end();
65 | return false;
66 | }
67 | } else {
68 | // let user know this was an invalid request
69 | res.send(invalid);
70 | res.end();
71 | return false;
72 | }
73 | };
74 |
--------------------------------------------------------------------------------
/pages/api/authenticate/logout.ts:
--------------------------------------------------------------------------------
1 | import {Message} from '../../..';
2 | import {NextApiRequest, NextApiResponse} from 'next';
3 |
4 | export default async (_req: NextApiRequest, res: NextApiResponse) => {
5 | // set headers
6 | res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
7 | res.setHeader('Set-Cookie', [`portal-token=; Path=/; HttpOnly`, `portal-user=; Path=/; HttpOnly`, `portal-user-id=; Path=/;HttpOnly`]);
8 | res.status(200);
9 |
10 | res.send({
11 | status: true,
12 | message: "You have been logged out.",
13 | } as Message);
14 | };
15 |
--------------------------------------------------------------------------------
/pages/api/authenticate/reset.ts:
--------------------------------------------------------------------------------
1 | import db from '../../../lib/db';
2 | import {Message} from '../../..';
3 | import {NextApiRequest, NextApiResponse} from 'next';
4 |
5 | export default (req: NextApiRequest, res: NextApiResponse): void => {
6 | // set headers
7 | res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
8 | res.setHeader("Content-Type", "json/javascript");
9 |
10 | // Get credentials from JSON body
11 | const {email, token, user, action, password} = req.body;
12 |
13 | function handlePasswordVerification() {
14 | console.log(req.body);
15 | db.verifyPasswordToken(escape(email), escape(token))
16 | .then((data: any) => {
17 | res.send(data);
18 | })
19 | .catch((error: Error) => {
20 | console.log(error);
21 | });
22 | }
23 |
24 | function handlePasswordResetInitiation(message: any) {
25 | const user = message.data.username;
26 |
27 | // will send email to user, starting the process
28 | // will also update two columns for user - password_token, password_reset
29 | if (user !== undefined) {
30 | db.initiatePasswordReset(escape(user), escape(email))
31 | .then((data: Message) => {
32 | res.send(data);
33 | console.log(data);
34 | })
35 | .catch((error: Error) => {
36 | console.log(error);
37 | });
38 | } else {
39 | res.send({
40 | status: false,
41 | message: "Invalid email provided.",
42 | } as Message);
43 | }
44 | }
45 |
46 | function handlePasswordReset() {
47 | if (token !== undefined && user !== undefined) {
48 | console.log(password, escape(password));
49 | db.passwordReset(escape(user), escape(token), escape(password))
50 | .then((data: any) => {
51 | res.send(data);
52 | }).catch((error: Error) => {
53 | console.log(error);
54 | });
55 | }
56 | }
57 |
58 | if (email && action) {
59 | db.getUserByEmail(escape(email))
60 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
61 | .then(async (data: any) => {
62 | switch (action) {
63 | case "verify":
64 | handlePasswordVerification();
65 | break;
66 | case "initiate":
67 | handlePasswordResetInitiation(data);
68 | break;
69 | case "reset":
70 | handlePasswordReset();
71 | break;
72 | default:
73 | res.send({
74 | status: false,
75 | message: "Request denied.",
76 | });
77 | }
78 | });
79 | } else {
80 | res.send({
81 | status: false,
82 | message: "Request denied.",
83 | });
84 | }
85 | };
86 |
--------------------------------------------------------------------------------
/pages/api/authenticate/signup.ts:
--------------------------------------------------------------------------------
1 | import db from '../../../lib/db';
2 | import {Message} from '../../..';
3 | import {NextApiRequest, NextApiResponse} from 'next';
4 |
5 | export default async (req: NextApiRequest, res: NextApiResponse): Promise => {
6 | // Get credentials from JSON body
7 | const {username, email, password, role} = req.body;
8 |
9 | if (username && email && password) {
10 | db.createUser(username, password, email, role)
11 | .then((result: any) => {
12 | res.send(JSON.stringify(result));
13 | res.end();
14 | } )
15 | .catch((error: Error) => {
16 | res.send(JSON.stringify(error));
17 | res.end();
18 | });
19 | } else {
20 | res.send({
21 | status: false,
22 | message: "Error with data provided.",
23 | } as Message);
24 | res.end();
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/pages/api/course-students/index.ts:
--------------------------------------------------------------------------------
1 |
2 | import {NextApiResponse, NextApiRequest} from 'next';
3 | import db from '../../../lib/db';
4 |
5 | interface Student {
6 | id: number;
7 | imageUri: string;
8 | name: string;
9 | email: string;
10 | status: number;
11 | }
12 |
13 | interface Course {
14 | id: string;
15 | code: string;
16 | name: string;
17 | students: Array;
18 | }
19 |
20 | export default async (req: NextApiRequest, res: NextApiResponse) => {
21 | const user = req.cookies['portal-user'];
22 | const userID = req.cookies['portal-user-id'];
23 |
24 | if (!user || !userID) {
25 | res.status(401).send({
26 | message: "User is not authorized",
27 | });
28 | } else {
29 | // TODO - Verifiy user's role is teacher/admin
30 | switch (req.method) {
31 | case "GET":
32 | db.getAllCoursesByInstructor(userID)
33 | .then((result: any) => {
34 | const {courses, users} = result.data;
35 |
36 | const c = courses.map((course: any) => {
37 | return {
38 | id: course.course_id,
39 | code: course.code,
40 | name: course.name,
41 | students: course.students_enrolled.split(',').map((student: any) => Object.assign(users[student], {id: student})),
42 | } as Course;
43 | });
44 | res.status(200).send(c);
45 | }).catch(() => {
46 | res.status(500).send({
47 | message: "Unknown error",
48 | });
49 | });
50 | break;
51 | default:
52 | res.status(404).end();
53 | }
54 | }
55 |
56 | // students for all courses you're an instructor for
57 | };
58 |
--------------------------------------------------------------------------------
/pages/api/course/add.ts:
--------------------------------------------------------------------------------
1 | import db from "../../../lib/db";
2 |
3 | export default async (req: any) => {
4 | const data = JSON.parse(req.body);
5 | const {courseTitle, cohortID, courseStart, courseExpire, courseDescription} = data;
6 |
7 |
8 | const query = `INSERT INTO courses (courseTitle, cohortID, courseStart, courseExpire, courseDescription)
9 | values ("${courseTitle}","${cohortID}","${courseStart}", "${courseExpire}", "${courseDescription}")`;
10 |
11 | console.log(query);
12 |
13 | db.query(query, (error: any, response: any) => {
14 | if (error) throw error;
15 | console.log(response);
16 | });
17 | };
18 |
19 |
20 |
--------------------------------------------------------------------------------
/pages/api/course/addPicture.ts:
--------------------------------------------------------------------------------
1 |
2 | // import formidable from "formidable";
3 |
4 | export const config = {
5 | api: {
6 | bodyParser: false,
7 | },
8 | };
9 |
10 |
11 | // export default async (req: any, res: any) => {
12 | // const form = new formidable.IncomingForm();
13 | // form.uploadDir = './public/images/coursePicture';
14 | // form.maxFileSize = 2;
15 | // form.on('file', (filename, file) => {
16 | // console.log(file.path);
17 | // // form.emit('data', {name: 'file', key: filename, value: file});
18 | // });
19 | // // form.parse(req, function(err, fields, files) {
20 | // // // console.log(files[0]);
21 | // // res.end();
22 | // // });
23 | // };
24 |
25 |
26 |
--------------------------------------------------------------------------------
/pages/api/mail.ts:
--------------------------------------------------------------------------------
1 | import ReactDOMServer from 'react-dom/server';
2 | import confirmationEmail from './../../email/templates/main/confirmationEmail';
3 | import resetPasswordEmail from './../../email/templates/main/resetPasswordEmail';
4 | import {Message} from '../..';
5 | import {NextApiRequest, NextApiResponse} from "next";
6 |
7 | const nodemailer = require('nodemailer');
8 | const transporter = nodemailer.createTransport({
9 | host: process.env.MAILHOST,
10 | port: process.env.MAILPORT,
11 | auth: {
12 | user: process.env.MAILUSER,
13 | pass: process.env.MAILPASS,
14 | },
15 | tls: {
16 | rejectUnauthorized: false,
17 | },
18 | });
19 |
20 | const handleAction = (options: { action: string; username: string; email: string; data?: any }) => {
21 | const {action, username, email, data} = options;
22 |
23 | // handle the action requested
24 | switch (action) {
25 | case "confirm":
26 | return {
27 | subject: "Confirm your email.",
28 | html: ReactDOMServer.renderToStaticMarkup(confirmationEmail( {
29 | username,
30 | url: (process as any).env.HOST,
31 | token: data.token,
32 | title: "Comfirm your email.",
33 | })),
34 | };
35 | case "reset":
36 | return {
37 | subject: "Reset your password.",
38 | html: ReactDOMServer.renderToStaticMarkup(resetPasswordEmail( {
39 | username,
40 | email,
41 | url: (process as any).env.HOST,
42 | token: data.token,
43 | title: "Reset your password.",
44 | })),
45 | };
46 | }
47 | };
48 |
49 | export default async (req: NextApiRequest, res: NextApiResponse): Promise => {
50 | // set headers
51 | res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
52 | res.setHeader("Content-Type", "json/javascript");
53 |
54 | const {action, username, email, data} = req.body;
55 |
56 | if (!action || !username || !email) {
57 | res.send({
58 | status: false,
59 | message: "Invalid input",
60 | } as Message);
61 | return;
62 | }
63 |
64 | const mailOptions= Object.assign({
65 | from: '"Administrator" <' + process.env.MAILUSER + '>',
66 | to: String(email),
67 | }, handleAction({action, username, email, data}));
68 |
69 |
70 | transporter.sendMail(mailOptions, (err: any, info: string) => {
71 | if (err) {
72 | res.send({
73 | status: false,
74 | message: "Email failed to send.",
75 | } as Message);
76 | }
77 |
78 | console.log("Email log: ", info);
79 | res.send({
80 | status: true,
81 | message: "Email successfully sent.",
82 | } as Message);
83 | });
84 | };
85 |
--------------------------------------------------------------------------------
/pages/api/messages.ts:
--------------------------------------------------------------------------------
1 | import db from "../../lib/db";
2 | import {Message} from "../../index";
3 | import {NextApiRequest, NextApiResponse} from "next";
4 |
5 |
6 | // Route to get meta data from the database.
7 | export default async (req: NextApiRequest, res: NextApiResponse) => {
8 | const {user, userID} = req.body; // get key provided by user
9 |
10 | if (user && userID) {
11 | const messages = await db.getMessageCount(req.body);
12 | res.send(JSON.stringify(messages));
13 | } else {
14 | res.send({
15 | status: false,
16 | message: "data passed incorrectly..",
17 | } as Message);
18 | }
19 |
20 | res.end();
21 | };
22 |
--------------------------------------------------------------------------------
/pages/api/meta.ts:
--------------------------------------------------------------------------------
1 | import db from "../../lib/db";
2 | import {Message} from "../../index";
3 | import {NextApiRequest, NextApiResponse} from "next";
4 |
5 |
6 | // Route to get meta data from the database.
7 | export default async (req: NextApiRequest, res: NextApiResponse) => {
8 | const {key} = req.body; // get key provided by user
9 |
10 | // Check if key is a string
11 | if (key && typeof key === "string") {
12 | const meta = await db.getKey(req.body);
13 | res.send(JSON.stringify(meta.meta_value));
14 | // check if key is an array
15 | } else if (key && Array.isArray(key)) {
16 | const meta = await db.getKeys(req.body);
17 | res.send(JSON.stringify(meta));
18 | } else {
19 | res.send({
20 | status: false,
21 | message: "key(s) passed incorrectly..",
22 | } as Message);
23 | }
24 |
25 | res.end();
26 | };
27 |
--------------------------------------------------------------------------------
/pages/api/notifications.ts:
--------------------------------------------------------------------------------
1 | import db from "../../lib/db";
2 | import {Message} from "../../index";
3 | import {NextApiRequest, NextApiResponse} from "next";
4 |
5 |
6 | // Route to get meta data from the database.
7 | export default async (req: NextApiRequest, res: NextApiResponse) => {
8 | const {user, userID} = req.body; // get key provided by user
9 |
10 | if (user && userID) {
11 | const notifications = await db.getNotificationCount(req.body);
12 | res.send(JSON.stringify(notifications));
13 | } else {
14 | res.send({
15 | status: false,
16 | message: "data passed incorrectly..",
17 | } as Message);
18 | }
19 |
20 | res.end();
21 | };
22 |
--------------------------------------------------------------------------------
/pages/api/setup.ts:
--------------------------------------------------------------------------------
1 | import db from "../../lib/db";
2 | import {Message} from "../../index";
3 | import {error} from "next/dist/build/output/log";
4 | import {NextApiRequest, NextApiResponse} from "next";
5 |
6 | const root = require("app-root-path");
7 | const fs = require("fs");
8 |
9 | interface StoreModel {
10 | [property: string]: {};
11 | }
12 |
13 | type Messages = StoreModel;
14 |
15 | // Setup database tables if they don't exist.
16 | export default (req: NextApiRequest, res: NextApiResponse) => {
17 | const {secret} = req.query; // secret key from user requesting setup
18 |
19 | // check if secret given matches one in this environment
20 | if (secret !== undefined && process.env.SECRET === secret) {
21 | // Promise based
22 | new Promise((resolve, reject) => {
23 | const models: Messages = {}; // temporary store for all models
24 |
25 | // Iterate through file directory and find models
26 | fs.readdirSync(root + "/models") // directory to read
27 | .forEach((file: string) => { // iterate through each file in that directory
28 | const fileName = file.replace(".ts", ""); // remove .js
29 | const model = require("./../../models/" + fileName); // import the model
30 | models[fileName] = model.default; // add model to temp. store for later use
31 | });
32 |
33 | // if models found, resolve, reject if nothing found
34 | models.length === 0 ? reject(error) : resolve(models);
35 | })
36 | .then(async (data: any) => {
37 | const messages: Messages = {}; // temporary message store
38 |
39 | // iterate through all models found
40 | for (const model in data) {
41 | // model should have a main function with same name to trigger its creation
42 | if (data.hasOwnProperty(model)) {
43 | messages[model] = await data[model](db) as Message; // save result to the message store
44 | }
45 | }
46 |
47 | return await messages;
48 | })
49 | .then((data) => {
50 | console.log('All models processed.');
51 | res.send(data); // send all messages as an object in response
52 | })
53 | .catch((error) => {
54 | res.send(error); // send error found in response
55 | });
56 | } else {
57 | // secret did not match, let user know they're not authorized to run setup
58 | res.send({
59 | status: false,
60 | message: "not authorized...",
61 | } as Message);
62 | }
63 | };
64 |
--------------------------------------------------------------------------------
/pages/api/user/upload.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable indent */
2 | /* eslint-disable comma-dangle */
3 | // =======================================================================
4 | // Known Issues/Todos
5 | // -
6 | // =======================================================================
7 |
8 | // import db from '../../../lib/db';
9 | // import auth from '../../../lib/auth';
10 | import {createWriteStream} from 'fs';
11 | import {Message} from '../../..';
12 | import {NextApiResponse, NextApiRequest} from 'next';
13 |
14 | export const config = {
15 | api: {
16 | bodyParser: false
17 | }
18 | };
19 |
20 | export default async (req: NextApiRequest, res: NextApiResponse) => {
21 | // // set headers
22 | // res.setHeader(
23 | // 'Cache-Control',
24 | // 'no-store, no-cache, must-revalidate, proxy-revalidate'
25 | // );
26 |
27 | let status = false;
28 | let message = 'Error: Image failed to upload';
29 |
30 | if (req.pipe(createWriteStream("./public/images/avatars/" + req.headers['user-identification']))) {
31 | status = true;
32 | message = 'Image uploaded';
33 | console.log('Will be sending this image to user ID: ' + req.headers['user-identification']);
34 | }
35 | res.statusCode = 200;
36 |
37 | const statusMessage = {
38 | status: status,
39 | message: message
40 | } as Message;
41 |
42 | // // example message - whatever you want to return, use this format.
43 | // // showing return example
44 | res.send(statusMessage);
45 | res.end();
46 | };
47 |
48 |
--------------------------------------------------------------------------------
/pages/api/validate/confirm.ts:
--------------------------------------------------------------------------------
1 | import db from '../../../lib/db';
2 | import {Message} from '../../..';
3 | import {NextApiResponse, NextApiRequest} from 'next';
4 |
5 | export default async (req: NextApiRequest, res: NextApiResponse): Promise => {
6 | const {user = false, token = false} = req.body;
7 |
8 | if (user && token) {
9 | db.confirmEmail(user, token)
10 | .then((result: any) => {
11 | if (result && result === 'active') {
12 | res.send({
13 | status: true,
14 | message: "Account already activate.",
15 | } as Message);
16 | } else if (result && result.serverStatus === 2) {
17 | res.send({
18 | status: true,
19 | message: "Account has been activated.",
20 | } as Message);
21 | }
22 | }).catch((error: Error) => {
23 | res.send({
24 | status: false,
25 | message: error.message,
26 | } as Message);
27 | });
28 | } else {
29 | res.send({
30 | status: false,
31 | message: "Invalid input provided.",
32 | } as Message);
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/pages/api/validate/email.ts:
--------------------------------------------------------------------------------
1 | import db from '../../../lib/db';
2 | import {Message} from '../../../index';
3 | import {NextApiRequest, NextApiResponse} from "next";
4 |
5 | export default async (req: NextApiRequest, res: NextApiResponse) => {
6 | // Get credentials from JSON body
7 | const {email} = req.body;
8 |
9 | if (email) {
10 | db.emailExists(email)
11 | .then((result: any) => {
12 | res.send(JSON.stringify(result));
13 | }).catch((error: any) => {
14 | res.send(JSON.stringify(error));
15 | });
16 | } else {
17 | res.send({
18 | status: false,
19 | message: "Error with data provided.",
20 | } as Message);
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/pages/api/validate/user.ts:
--------------------------------------------------------------------------------
1 | import db from '../../../lib/db';
2 | import {Message} from '../../..';
3 | import {NextApiRequest, NextApiResponse} from "next";
4 |
5 | export default async (req: NextApiRequest, res: NextApiResponse) => {
6 | // Get credentials from JSON body
7 | const {username} = req.body;
8 |
9 | if (username) {
10 | db.userExists(username)
11 | .then((result: any) => {
12 | res.send(JSON.stringify(result));
13 | }).catch((error: any) => {
14 | res.status(500);
15 | res.send(JSON.stringify(error));
16 | });
17 | } else {
18 | res.send({
19 | status: false,
20 | message: "input not provided",
21 | } as Message);
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/pages/authenticate.tsx:
--------------------------------------------------------------------------------
1 | import LogIn from '../src/components/authenticate/LogIn/LogIn';
2 | import SignUp from '../src/components/authenticate/SignUp/SignUp';
3 | import Reset from '../src/components/authenticate/Reset/Reset';
4 | import ContentContainer from '../src/components/global/ContentContainer/ContentContainer';
5 | import SEO from "../next-seo.config";
6 | import {DefaultSeo} from "next-seo";
7 |
8 | function Authenticate() {
9 | return (
10 |
11 |
15 |
16 |
18 |
19 |
20 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | }/>
39 |
40 |
41 | );
42 | }
43 |
44 | export default Authenticate;
45 |
--------------------------------------------------------------------------------
/pages/bookmarks/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DashboardLayout from "../../src/components/layouts/DashboardLayout";
3 |
4 | const Bookmarks: React.FC = () => (
5 |
6 | Your Bookmarks
7 |
8 | );
9 |
10 | export default Bookmarks;
11 |
--------------------------------------------------------------------------------
/pages/community/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DashboardLayout from "../../src/components/layouts/DashboardLayout";
3 |
4 | const Community: React.FC = () => (
5 |
6 | Your Community
7 |
8 | );
9 |
10 | export default Community;
11 |
--------------------------------------------------------------------------------
/pages/confirmation.tsx:
--------------------------------------------------------------------------------
1 | import {NextSeo} from "next-seo";
2 | import {useEffect, useState} from 'react';
3 | import notify from "../src/components/utility/Notify";
4 | import Redirect from "../src/components/animation/Redirect/Redirect";
5 |
6 | /**
7 | * Check provided input - activate account if valid.
8 | * @param {any} props
9 | * @return {void} checks validity of input provided.
10 | */
11 | function Confirmation(props: any) {
12 | const {query} = props;
13 | const {user, token} = query;
14 | const url = 'api/validate/confirm';
15 |
16 | const [confirmation, setConfirmation] = useState(undefined);
17 |
18 | useEffect(() => {
19 | if (user && token) {
20 | fetch(process.env.HOST + url, {
21 | method: 'POST',
22 | headers: {
23 | 'Content-Type': 'application/json',
24 | },
25 | body: JSON.stringify({
26 | user,
27 | token,
28 | }),
29 | })
30 | .then((response) => response.json())
31 | .then((response) => {
32 | if (response.status) {
33 | notify({
34 | message: response.message,
35 | status: 'success',
36 | pos: 'top-left',
37 | timeout: 5000,
38 | });
39 |
40 | setTimeout(() => {
41 | if (document) {
42 | document.location.href = "/authenticate";
43 | }
44 | }, 2500);
45 | } else {
46 | notify({
47 | message: response.message,
48 | status: 'danger',
49 | pos: 'top-left',
50 | timeout: 5000,
51 | });
52 | }
53 |
54 | setConfirmation(response.status);
55 | })
56 | .catch((error) => {
57 | notify({
58 | message: error.message,
59 | status: 'danger',
60 | pos: 'top-left',
61 | timeout: 5000,
62 | });
63 | });
64 | }
65 | }, []);
66 |
67 | if (props.query.hasOwnProperty('user') && props.query.hasOwnProperty('token')) {
68 | return (
69 |
70 |
71 |
72 | {confirmation &&
73 | (
74 |
77 |
78 |
79 |
80 |
)
81 | }
82 | {!confirmation &&
83 |
84 |
87 |
88 |
89 |
90 |
91 | }
92 |
93 |
94 |
95 | );
96 | } else {
97 | setTimeout(()=> {
98 | if (document) {
99 | document.location.href = "/authenticate";
100 | }
101 | }, 2500);
102 | return (
103 |
104 | );
105 | }
106 | }
107 |
108 | Confirmation.getInitialProps = ({query}: any) => {
109 | return {query};
110 | };
111 |
112 | export default Confirmation;
113 |
--------------------------------------------------------------------------------
/pages/courses/add.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Head from 'next/head';
3 | import DashboardLayout from "../../src/components/layouts/DashboardLayout";
4 | import AddCourseComponent from '../../src/components/dashboard/panels/addCourse/AddCourse';
5 | const AddCourse: React.FC = () => (
6 |
7 |
8 |
9 |
10 |
11 |
12 | );
13 | export default AddCourse;
14 |
--------------------------------------------------------------------------------
/pages/courses/categories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DashboardLayout from "../../src/components/layouts/DashboardLayout";
3 |
4 | const CourseCategories: React.FC = () => (
5 |
6 | Course Categories
7 |
8 | );
9 |
10 | export default CourseCategories;
11 |
--------------------------------------------------------------------------------
/pages/courses/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DashboardLayout from "../../src/components/layouts/DashboardLayout";
3 | import Courses from "../../src/components/courses/Courses";
4 |
5 | const AllCourses: React.FC = () => (
6 |
7 |
8 |
9 | );
10 |
11 | export default AllCourses;
12 |
--------------------------------------------------------------------------------
/pages/courses/manage.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DashboardLayout from "../../src/components/layouts/DashboardLayout";
3 |
4 | const ManageCourses: React.FC = () => (
5 |
6 | Manage Courses
7 |
8 | );
9 |
10 | export default ManageCourses;
11 |
--------------------------------------------------------------------------------
/pages/courses/tags.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DashboardLayout from "../../src/components/layouts/DashboardLayout";
3 |
4 | const CourseTags: React.FC = () => (
5 |
6 | Courses by Tags
7 |
8 | );
9 |
10 | export default CourseTags;
11 |
--------------------------------------------------------------------------------
/pages/dashboard/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DashboardLayout from "../../src/components/layouts/DashboardLayout";
3 | import DashboardHome, {DashboardHomeProps} from "../../src/components/dashboard/home/DashboardHome";
4 |
5 | const Lessons: React.FC = () => {
6 | const dashboardData: DashboardHomeProps = {
7 | onSummaryItemClicked: (url) => console.log(url),
8 | summaryItems: [
9 | {
10 | title: "Messages",
11 | count: 45,
12 | icon: "fal fa-comments-alt",
13 | url: "dashboard/messages",
14 | },
15 | {
16 | title: "Students",
17 | count: 54,
18 | icon: "fal fa-user-friends",
19 | url: "dashboard/students",
20 | },
21 | {
22 | title: "Courses",
23 | count: 16,
24 | icon: "fal fa-chalkboard-teacher",
25 | url: "dashboard/courses",
26 | },
27 | {
28 | title: "Bookmarks",
29 | count: 27,
30 | icon: "fal fa-heart",
31 | url: "dashboard/bookmarks",
32 | },
33 | ],
34 | notifications: [
35 | {
36 | id: 1,
37 | title: "Message",
38 | content: "Tony: Hey, have you checked out the following resources for the course? They're great!",
39 | },
40 | {
41 | id: 2,
42 | title: "Instructor",
43 | content: "Assignment has been graded - Project One",
44 | },
45 | {
46 | id: 3,
47 | title: "Site Update",
48 | content: "Spark application has been updated to version 1.4.5",
49 | },
50 | {
51 | id: 4,
52 | title: "Status Update",
53 | content: "This is an automated server response message",
54 | },
55 | {
56 | id: 5,
57 | title: "Message",
58 | content: "Felipe: Do you have any notes from the class this past Saturday? HD 4 LIFE!",
59 | },
60 | ],
61 | currentCohort: {
62 | name: "Cohort 24",
63 | updates: [
64 | {
65 | id: 1,
66 | title: "Week 1 - 01/05/2021",
67 | content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Unde esse reprehenderit debitis repellendus dolorem nostrum vel atque fuga magnam! Consequatur sapiente quod ducimus voluptatum, obcaecati optio nulla hic? Perspiciatis, nostrum.",
68 | },
69 | {
70 | id: 2,
71 | title: "Week 2 - 01/05/2021",
72 | content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Unde esse reprehenderit debitis repellendus dolorem nostrum vel atque fuga magnam! Consequatur sapiente quod ducimus voluptatum, obcaecati optio nulla hic? Perspiciatis, nostrum.",
73 | },
74 | {
75 | id: 3,
76 | title: "Week 3 - 01/05/2021",
77 | content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Unde esse reprehenderit debitis repellendus dolorem nostrum vel atque fuga magnam! Consequatur sapiente quod ducimus voluptatum, obcaecati optio nulla hic? Perspiciatis, nostrum.",
78 | },
79 | {
80 | id: 4,
81 | title: "Week 4 - 01/05/2021",
82 | content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Unde esse reprehenderit debitis repellendus dolorem nostrum vel atque fuga magnam! Consequatur sapiente quod ducimus voluptatum, obcaecati optio nulla hic? Perspiciatis, nostrum.",
83 | },
84 | {
85 | id: 5,
86 | title: "Week 5 - 01/05/2021",
87 | content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Unde esse reprehenderit debitis repellendus dolorem nostrum vel atque fuga magnam! Consequatur sapiente quod ducimus voluptatum, obcaecati optio nulla hic? Perspiciatis, nostrum.",
88 | },
89 | ],
90 | },
91 | };
92 |
93 | return (
94 |
95 |
97 |
98 | );
99 | };
100 |
101 | export default Lessons;
102 |
--------------------------------------------------------------------------------
/pages/dashboard/students/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect, useState} from "react";
2 | import {useRouter} from "next/router";
3 | import DashboardLayout from "../../../src/components/layouts/DashboardLayout";
4 | import ManageStudents, {ManageStudentsCourse} from "../../../src/components/manage-students/ManageStudents";
5 |
6 | // mapDataToCourseProps is a utility function to add types to fetched data
7 | // we could do this in api/user/students, but parts should be independent
8 | const mapDataToCourseProps = (data: any): ManageStudentsCourse[] => {
9 | return data.map((course: any) => ({
10 | id: course.id,
11 | code: course.code,
12 | name: course.name,
13 | students: course.students.map((student: any) => ({
14 | id: student.id,
15 | avatarUrl: student.avatar_url,
16 | firstName: student.first_name,
17 | lastName: student.last_name,
18 | email: student.email,
19 | status: student.status,
20 | })),
21 | }));
22 | };
23 |
24 | const Students = () => {
25 | const [courses, setCourses] = useState([]);
26 | const router = useRouter();
27 |
28 | useEffect(() => {
29 | fetch(process.env.HOST + "api/course-students", {
30 | method: 'GET',
31 | headers: {
32 | 'Content-Type': 'application/json',
33 | },
34 | credentials: 'same-origin',
35 | })
36 | .then((res) => res.json()) // TODO - check response code
37 | .then((data) => {
38 | const courses = mapDataToCourseProps(data);
39 | setCourses(courses);
40 | });
41 | }, []);
42 |
43 | const handleManageUser = (id: string) => {
44 | console.log(`Go to manage user page for user with id: ${id}`);
45 | router.push(`/students/${id}`);
46 | };
47 |
48 | const handleViewUser = (id: string) => {
49 | console.log(`Go to user page for user with id: ${id}`);
50 | router.push(`/users/${id}`);
51 | };
52 |
53 | const handleRemoveStudentsFromCourse = (courseID: string, userIDs: string[]) => {
54 | console.log(courseID, userIDs);
55 | };
56 |
57 | return (
58 |
59 | {courses.length !== 0 && (
60 | handleManageUser(id)}
63 | onViewUser={(id) => handleViewUser(id)}
64 | onRemoveUsersFromCourse={({courseID, userIDs}) => handleRemoveStudentsFromCourse(courseID, userIDs)}
65 | />
66 | )}
67 |
68 | );
69 | };
70 |
71 |
72 | export default Students;
73 |
74 |
--------------------------------------------------------------------------------
/pages/dashboard/updates.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DashboardLayout from "../../src/components/layouts/DashboardLayout";
3 |
4 | const Updates: React.FC = () => (
5 |
6 | Updates
7 |
8 | );
9 |
10 | export default Updates;
11 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 |
2 | import SEO from "../next-seo.config";
3 | import {DefaultSeo} from "next-seo";
4 | import Footer from '../src/components/global/Footer/Footer';
5 | import Navigation from "../src/components/global/Navigation/Navigation";
6 |
7 | // render home page.
8 | function Home(): JSX.Element {
9 | return (
10 |
11 |
12 |
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | export default Home;
30 |
--------------------------------------------------------------------------------
/pages/lessons/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DashboardLayout from "../../src/components/layouts/DashboardLayout";
3 |
4 | const Lessons: React.FC = () => (
5 |
6 | Lessons
7 | Learn your rules, learn your rules, if you don&lsquot you&lsquoll be eaten in your sleep. Rawr! - Dwight Schrute
8 |
9 | );
10 |
11 | export default Lessons;
12 |
--------------------------------------------------------------------------------
/pages/logged-out.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import SEO from "../next-seo.config";
3 | import {DefaultSeo} from "next-seo";
4 |
5 | function LoggedOut(): JSX.Element {
6 | return (
7 |
8 |
12 |
13 |
You are now logged out
14 |
15 | Click
16 | here
17 | to return to the login page.
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | export default LoggedOut;
25 |
--------------------------------------------------------------------------------
/pages/messages/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DashboardLayout from "../../src/components/layouts/DashboardLayout";
3 |
4 | const Messages: React.FC = () => (
5 |
6 | Your messages
7 |
8 | );
9 |
10 | export default Messages;
11 |
--------------------------------------------------------------------------------
/pages/resources/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DashboardLayout from "../../src/components/layouts/DashboardLayout";
3 |
4 | const Resources: React.FC = () => (
5 |
6 | Your Resources
7 |
8 | );
9 |
10 | export default Resources;
11 |
--------------------------------------------------------------------------------
/pages/settings/account.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DashboardLayout from "../../src/components/layouts/DashboardLayout";
3 |
4 | const AccountSettings: React.FC = () => (
5 |
6 | Your Account Settings
7 |
8 | );
9 |
10 | export default AccountSettings;
11 |
--------------------------------------------------------------------------------
/pages/settings/preferences.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DashboardLayout from "../../src/components/layouts/DashboardLayout";
3 |
4 | const AccountPreferences: React.FC = () => (
5 |
6 | Your Account Preferences
7 |
8 | );
9 |
10 | export default AccountPreferences;
11 |
--------------------------------------------------------------------------------
/pages/settings/profile.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DashboardLayout from "../../src/components/layouts/DashboardLayout";
3 | import UserInfoInput from "../../src/components/user-info-input/UserInfoInput";
4 |
5 | const Profile: React.FC = () => (
6 |
7 |
8 |
9 | );
10 |
11 | export default Profile;
12 |
--------------------------------------------------------------------------------
/pages/welcome.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | function Welcome(): JSX.Element {
4 | return (
5 |
6 |
7 |
8 |
Thank You
9 |
We appreciate your interest.
10 |
11 | You will be receiving a confirmation email shortly.
12 |
13 |
14 | Confirm that you received the email and then try logging in to
15 | your account.
16 |
17 |
18 |
19 | Go to Log In page
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
28 | export default Welcome;
29 |
--------------------------------------------------------------------------------
/public/fonts/AvenirLTStd-Black.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/fonts/AvenirLTStd-Black.otf
--------------------------------------------------------------------------------
/public/fonts/AvenirLTStd-Book.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/fonts/AvenirLTStd-Book.otf
--------------------------------------------------------------------------------
/public/fonts/AvenirLTStd-Heavy.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/fonts/AvenirLTStd-Heavy.otf
--------------------------------------------------------------------------------
/public/fonts/AvenirLTStd-Light.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/fonts/AvenirLTStd-Light.otf
--------------------------------------------------------------------------------
/public/fonts/AvenirLTStd-Medium.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/fonts/AvenirLTStd-Medium.otf
--------------------------------------------------------------------------------
/public/fonts/Big-John.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/fonts/Big-John.otf
--------------------------------------------------------------------------------
/public/images/authentication-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/authentication-screenshot.png
--------------------------------------------------------------------------------
/public/images/avatars/david-8-avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/avatars/david-8-avatar.jpg
--------------------------------------------------------------------------------
/public/images/avatars/placeholder_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/avatars/placeholder_image.png
--------------------------------------------------------------------------------
/public/images/coursePicture/upload_15eab51c419a5e174bce3860f42d1ac8:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/coursePicture/upload_15eab51c419a5e174bce3860f42d1ac8
--------------------------------------------------------------------------------
/public/images/coursePicture/upload_31cfd508e85dd3586cdc328fd593a261:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/coursePicture/upload_31cfd508e85dd3586cdc328fd593a261
--------------------------------------------------------------------------------
/public/images/coursePicture/upload_35984330de0154260d732bc550a8ecdd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/coursePicture/upload_35984330de0154260d732bc550a8ecdd
--------------------------------------------------------------------------------
/public/images/coursePicture/upload_3d5a1354820f567fbf009de45b4530ed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/coursePicture/upload_3d5a1354820f567fbf009de45b4530ed
--------------------------------------------------------------------------------
/public/images/coursePicture/upload_606e0dfc76128734e971591e573f8257:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/coursePicture/upload_606e0dfc76128734e971591e573f8257
--------------------------------------------------------------------------------
/public/images/coursePicture/upload_a13f2281b60262fe66fdb226cafbbec7:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/coursePicture/upload_a13f2281b60262fe66fdb226cafbbec7
--------------------------------------------------------------------------------
/public/images/coursePicture/upload_a447bbd89145ec5ef69a3f2618eed98f:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/coursePicture/upload_a447bbd89145ec5ef69a3f2618eed98f
--------------------------------------------------------------------------------
/public/images/coursePicture/upload_a5dec182d7c8841843a7ee894aec0856:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/coursePicture/upload_a5dec182d7c8841843a7ee894aec0856
--------------------------------------------------------------------------------
/public/images/coursePicture/upload_b31a54e4e9ec9b82460eaeed7b96e5d7:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/coursePicture/upload_b31a54e4e9ec9b82460eaeed7b96e5d7
--------------------------------------------------------------------------------
/public/images/coursePicture/upload_f6c37dc523bce5af630f65380130b7bf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/coursePicture/upload_f6c37dc523bce5af630f65380130b7bf
--------------------------------------------------------------------------------
/public/images/coursePicture/upload_fa51b1b0c5d8c332ed9e4807542f2bfa:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/coursePicture/upload_fa51b1b0c5d8c332ed9e4807542f2bfa
--------------------------------------------------------------------------------
/public/images/illustrations/access-granted.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/illustrations/access-granted.gif
--------------------------------------------------------------------------------
/public/images/illustrations/forbidden.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/illustrations/forbidden.gif
--------------------------------------------------------------------------------
/public/images/landing/landingcover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/landing/landingcover.jpg
--------------------------------------------------------------------------------
/public/images/landing/landingcover2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/landing/landingcover2.jpg
--------------------------------------------------------------------------------
/public/images/landing/riverside-sample.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/landing/riverside-sample.jpg
--------------------------------------------------------------------------------
/public/images/logo/spark-360x360.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/logo/spark-360x360.png
--------------------------------------------------------------------------------
/public/images/logo/spark-text-carbon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
9 |
16 |
22 |
23 |
29 |
31 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/public/images/logo/spark-text-snow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
9 |
16 |
22 |
23 |
29 |
31 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/public/images/logo/spark-text-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
9 |
16 |
22 |
23 |
29 |
31 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/public/images/spark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/inland-empire-software-development/spark/4eec7d0a1d79014c5fd3af92c21567cdbfbc1e00/public/images/spark.png
--------------------------------------------------------------------------------
/public/scripts/upload.js:
--------------------------------------------------------------------------------
1 | const bar = document.getElementById('js-progressbar');
2 | UIkit.upload('.js-upload', {
3 | // url: '/echo/json/',
4 | 'url': '/api/course/addPicture',
5 | 'data-type': 'json',
6 | 'name': 'test',
7 | 'multiple': false,
8 | "beforeSend": function(...args) {
9 | console.log('beforeSend', args);
10 | },
11 | "beforeAll": function(...args) {
12 | console.log('beforeAll', args);
13 | },
14 | "load": function(...args) {
15 | console.log('load', args);
16 | },
17 | "error": function(...args) {
18 | console.log('error', args);
19 | },
20 | "complete": function(...args) {
21 | console.log('complete', args);
22 | },
23 | "loadStart": function(e, ...args) {
24 | console.log('loadStart', args);
25 | bar.removeAttribute('hidden');
26 | bar.max = e.total;
27 | bar.value = e.loaded;
28 | },
29 | "progress": function(e, ...args) {
30 | console.log('progress', args);
31 | bar.max = e.total;
32 | bar.value = e.loaded;
33 | },
34 | "loadEnd": function(e, ...args) {
35 | console.log('loadEnd', args);
36 | bar.max = e.total;
37 | bar.value = e.loaded;
38 | },
39 | "completeAll": function(...args) {
40 | console.log('completeAll', args);
41 | setTimeout(function() {
42 | bar.setAttribute('hidden', 'hidden');
43 | }, 1000);
44 | // alert('Upload Completed');
45 | console.log('Upload Completed');
46 | },
47 | });
48 |
--------------------------------------------------------------------------------
/src/components/animation/Loader/Loader.scss:
--------------------------------------------------------------------------------
1 | $color: $primary;
2 | $colorRight: $secondary;
3 | $colorLeft: $tertiary;
4 |
5 | #infinite-loader {
6 | display: block;
7 | margin: 0 auto;
8 | width: 130px;
9 | top: 35vh;
10 | position: relative;
11 |
12 | .infinity {
13 | width: 120px;
14 | height: 60px;
15 | position: relative;
16 |
17 | div,
18 | span {
19 | position: absolute;
20 | }
21 |
22 | div {
23 | top: 0;
24 | left: 50%;
25 | width: 60px;
26 | height: 60px;
27 | animation: rotate 6.9s linear infinite;
28 |
29 | span {
30 | left: -8px;
31 | top: 50%;
32 | margin: -8px 0 0 0;
33 | width: 16px;
34 | height: 16px;
35 | display: block;
36 | background: $color;
37 | box-shadow: none;
38 | border-radius: 50%;
39 | transform: rotate(90deg);
40 | animation: move 6.9s linear infinite;
41 |
42 | &::before,
43 | &::after {
44 | content: '';
45 | position: absolute;
46 | display: block;
47 | border-radius: 50%;
48 | width: 14px;
49 | height: 14px;
50 | background: inherit;
51 | top: 50%;
52 | left: 50%;
53 | margin: -7px 0 0 -7px;
54 | }
55 |
56 | &::before {
57 | animation: drop1 0.8s linear infinite;
58 | }
59 |
60 | &::after {
61 | animation: drop2 0.8s linear infinite 0.4s;
62 | }
63 | }
64 |
65 | &:nth-child(2) {
66 | animation-delay: -2.3s;
67 |
68 | span {
69 | animation-delay: -2.3s;
70 | }
71 | }
72 |
73 | &:nth-child(3) {
74 | animation-delay: -4.6s;
75 |
76 | span {
77 | animation-delay: -4.6s;
78 | }
79 | }
80 | }
81 | }
82 |
83 | .infinityChrome {
84 | width: 128px;
85 | height: 60px;
86 |
87 | div {
88 | position: absolute;
89 | width: 16px;
90 | height: 16px;
91 | background: $color;
92 | border-radius: 50%;
93 | animation: moveSvg 6.9s linear infinite;
94 | -webkit-filter: url(#goo);
95 | filter: url(#goo);
96 | transform: scaleX(-1);
97 | offset-path: path("M64.3636364,29.4064278 C77.8909091,43.5203348 84.4363636,56 98.5454545,56 C112.654545,56 124,44.4117395 124,30.0006975 C124,15.5896556 112.654545,3.85282763 98.5454545,4.00139508 C84.4363636,4.14996252 79.2,14.6982509 66.4,29.4064278 C53.4545455,42.4803627 43.5636364,56 29.4545455,56 C15.3454545,56 4,44.4117395 4,30.0006975 C4,15.5896556 15.3454545,4.00139508 29.4545455,4.00139508 C43.5636364,4.00139508 53.1636364,17.8181672 64.3636364,29.4064278 Z");
98 |
99 | &::before,
100 | &::after {
101 | content: '';
102 | position: absolute;
103 | display: block;
104 | border-radius: 50%;
105 | width: 14px;
106 | height: 14px;
107 | background: inherit;
108 | top: 50%;
109 | left: 50%;
110 | margin: -7px 0 0 -7px;
111 | }
112 |
113 | &::before {
114 | animation: drop1 0.8s linear infinite;
115 | }
116 |
117 | &::after {
118 | animation: drop2 0.8s linear infinite 0.4s;
119 | }
120 |
121 | &:nth-child(2) {
122 | animation-delay: -2.3s;
123 | }
124 |
125 | &:nth-child(3) {
126 | animation-delay: -4.6s;
127 | }
128 | }
129 | }
130 |
131 | @keyframes moveSvg {
132 | 0% {
133 | offset-distance: 0%;
134 | }
135 |
136 | 25% {
137 | background: $colorRight;
138 | }
139 |
140 | 75% {
141 | background: $colorLeft;
142 | }
143 |
144 | 100% {
145 | offset-distance: 100%;
146 | }
147 | }
148 |
149 | @keyframes rotate {
150 | 50% {
151 | transform: rotate(360deg);
152 | margin-left: 0;
153 | }
154 |
155 | 50.0001%,
156 | 100% {
157 | margin-left: -60px;
158 | }
159 | }
160 |
161 | @keyframes move {
162 | 0%,
163 | 50% {
164 | left: -8px;
165 | }
166 |
167 | 25% {
168 | background: $colorRight;
169 | }
170 |
171 | 75% {
172 | background: $colorLeft;
173 | }
174 |
175 | 50.0001%,
176 | 100% {
177 | left: auto;
178 | right: -8px;
179 | }
180 | }
181 |
182 | @keyframes drop1 {
183 | 100% {
184 | transform: translate(32px, 8px) scale(0);
185 | }
186 | }
187 |
188 | @keyframes drop2 {
189 | 0% {
190 | transform: translate(0, 0) scale(0.9);
191 | }
192 |
193 | 100% {
194 | transform: translate(32px, -8px) scale(0);
195 | }
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/src/components/animation/Loader/Loader.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {withA11y} from '@storybook/addon-a11y';
3 | import "../../../style/index.scss";
4 | import "./Loader.scss";
5 | import Loader from './Loader';
6 |
7 | export default {
8 | title: 'Loader',
9 | decorators: [withA11y],
10 | };
11 |
12 | export const DefaultLoader = () => ;
13 |
14 | DefaultLoader.story = {
15 | parameters: {
16 | notes: 'Use this component while a whole page is loading.',
17 | },
18 | };
19 |
20 |
--------------------------------------------------------------------------------
/src/components/animation/Loader/Loader.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | /**
4 | * Renders the loader component with animation.
5 | * @constructor
6 | */
7 | export default function Loader(): JSX.Element {
8 | const isChrome = () => {
9 | if ((process as any).browser && navigator) {
10 | return /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
11 | }
12 |
13 | return true;
14 | };
15 |
16 | return (
17 |
18 | {isChrome() && (
19 |
24 | )}
25 |
26 | {!isChrome() && (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | ) }
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/animation/Redirect/Redirect.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {withA11y} from '@storybook/addon-a11y';
3 | import "../../../style/index.scss";
4 | import "./Redirect.scss";
5 | import Redirect from './Redirect';
6 |
7 | export default {
8 | title: 'Redirect',
9 | decorators: [withA11y],
10 | };
11 |
12 | export const DefaultRedirect = () => ;
13 |
14 | DefaultRedirect.story = {
15 | parameters: {
16 | notes: 'Use this component while a whole page is redirecting.',
17 | },
18 | };
19 |
20 |
--------------------------------------------------------------------------------
/src/components/animation/Redirect/Redirect.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {DefaultSeo} from "next-seo";
3 | import SEO from "../../../../next-seo.config";
4 |
5 | import './Redirect.scss';
6 |
7 | export default function Redirect(): JSX.Element {
8 | return (
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | redirecting
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/authenticate/LogIn/LogIn.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {withA11y} from '@storybook/addon-a11y';
3 |
4 | import LogIn from './LogIn';
5 | import Spinner from "../../global/Spinner/Spinner";
6 | import ContentContainer from '../../global/ContentContainer/ContentContainer';
7 |
8 | export default {
9 | title: 'Log in',
10 | decorators: [withA11y],
11 | };
12 |
13 | export const LogInForm = () => {
14 | return (
15 |
16 |
18 |
19 |
29 |
30 | }/>
31 |
32 | );
33 | };
34 |
35 | LogInForm.story = {
36 | parameters: {
37 | notes: "Basic log in form.",
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/src/components/authenticate/LogIn/LogIn.tsx:
--------------------------------------------------------------------------------
1 | import fetch from 'isomorphic-unfetch';
2 | import React from 'react';
3 | import notify from '../../utility/Notify';
4 | import {Message} from '../../../../index';
5 |
6 | import './Login.scss';
7 |
8 | function LogIn(): JSX.Element {
9 | const handleLogin = () => {
10 | const username: HTMLSelectElement | null = document.querySelector('[name="login-username"]');
11 | const password: HTMLSelectElement | null = document.querySelector('[name="login-password"]');
12 | const spinner: HTMLElement | null = document.getElementById('spinner');
13 |
14 | // show spinner while working
15 | if (spinner) spinner.classList.remove('uk-hidden');
16 |
17 | // API route that will handle signing in
18 | const url = 'api/authenticate/login';
19 | const data = {
20 | username: username ? username.value : null,
21 | password: password ? password.value : null,
22 | };
23 |
24 |
25 | fetch(process.env.HOST + url, {
26 | method: 'POST',
27 | headers: {
28 | 'Content-Type': 'application/json',
29 | },
30 | body: JSON.stringify(data),
31 | })
32 | .then((response: { json: () => any }) => response.json())
33 | .then((response: Message) => {
34 | const {status, message} = response;
35 |
36 | if (status) {
37 | if ((process as any).browser && document && UIkit) {
38 | document.location.href = "/dashboard";
39 | }
40 | } else {
41 | // hide spinner as work is essentially done
42 | if (spinner) spinner.classList.add('uk-hidden');
43 |
44 | notify({
45 | message,
46 | status: 'danger',
47 | pos: 'top-left',
48 | timeout: 1500,
49 | });
50 | }
51 | });
52 | };
53 |
54 | return (
55 |
56 | Sign in to your account
57 |
83 |
84 | );
85 | }
86 |
87 | export default LogIn;
88 |
--------------------------------------------------------------------------------
/src/components/authenticate/LogIn/Login.scss:
--------------------------------------------------------------------------------
1 | @import '../../../style/_variables.scss';
2 |
3 | .auth-login {
4 | .fa-user,
5 | .fa-lock-alt {
6 | position: relative;
7 | top: 40px;
8 | font-size: 18px;
9 | left: 15px;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/authenticate/LogOut/LogOut.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Styling for the logout component
3 | */
4 | .logout-link {
5 | padding-left: 15px;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/authenticate/LogOut/LogOut.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {withA11y} from '@storybook/addon-a11y';
3 |
4 | import LogOut from './LogOut';
5 |
6 | export default {
7 | title: 'Log Out',
8 | decorators: [withA11y],
9 | };
10 |
11 | export const LogOutForm = () =>
12 | ;
15 |
16 | LogOutForm.story = {
17 | parameters: {
18 | notes: 'Basic log out form',
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/authenticate/LogOut/LogOut.tsx:
--------------------------------------------------------------------------------
1 | import fetch from 'isomorphic-unfetch';
2 | import React from "react";
3 | import Link from 'next/link';
4 | import './LogOut.scss';
5 |
6 | export default function LogOut(props: any) {
7 | const {content = false} = props;
8 | const handleLogout = (event: any) => {
9 | event.preventDefault();
10 |
11 | // API route that will handle signing out
12 | const url = 'api/authenticate/logout';
13 | const spinner = document.getElementById('spinner');
14 | spinner?.classList.remove('uk-hidden');
15 |
16 | fetch(process.env.HOST + url, {
17 | method: 'POST',
18 | }).then((response) => response.json()).then((response) => {
19 | const {status} = response;
20 |
21 | if (status) {
22 | if (document) {
23 | document.location.href = "/logged-out";
24 | }
25 | }
26 | }).catch((error) => {
27 | console.log(error);
28 | });
29 | };
30 |
31 | if (!content) {
32 | return (
33 |
34 | handleLogout(event)}>Log out
35 |
36 | );
37 | } else {
38 | return (
39 | handleLogout(event)}>{content}
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/authenticate/Password/Password.scss:
--------------------------------------------------------------------------------
1 | @import './../../../style/variables.scss';
2 |
3 | .password-requirements {
4 | font-size: 12px;
5 |
6 | .show-password {
7 | margin: 0 0 -10px 0;
8 | min-width: 90px;
9 | }
10 |
11 | ul {
12 | margin-top: 0;
13 | }
14 |
15 | .fa-check {
16 | margin-left: 5px;
17 | }
18 |
19 | .fa-lock-alt {
20 | font-size: 18px;
21 | position: relative;
22 | left: 15px;
23 | top: 40px;
24 | }
25 | }
26 |
27 | .password-valid {
28 | border: 1px solid $green;
29 | background-color: rgba(2, 174, 114, 0.05);
30 | }
31 |
32 | #password-label {
33 | position: absolute;
34 | font-size: 14px;
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/authenticate/Password/Password.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {withA11y} from '@storybook/addon-a11y';
3 |
4 | import Password from './Password';
5 |
6 | export default {
7 | title: 'Password',
8 | decorators: [withA11y],
9 | };
10 |
11 | export const PasswordForm = () => {
12 | return (
13 |
18 | );
19 | };
20 |
21 | PasswordForm.story = {
22 | parameters: {
23 | notes: "Basic password form.",
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/src/components/authenticate/Password/Password.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Password.scss';
3 | interface PasswordCheck {
4 | [property: string]: boolean;
5 | }
6 |
7 | // Handles the visibility of the check marks for each requirement
8 | const handleVisibilityToggle = (index: string, state: boolean) => {
9 | const requirement = document.querySelector(`[data-check="${index}"]`);
10 |
11 | if (requirement) {
12 | state ?
13 | requirement.classList.remove('uk-hidden') :
14 | requirement.classList.add('uk-hidden');
15 | }
16 | };
17 |
18 | // Handles the testing of individual requirements
19 | const handleRequirements = (password: string) => {
20 | const check: PasswordCheck = {
21 | number: /\d/.test(password),
22 | lower: /[a-z]/.test(password),
23 | upper: /[A-Z]/.test(password),
24 | length: password.length >= 10,
25 | };
26 |
27 | for (const requirement in check) {
28 | if (check.hasOwnProperty(requirement)) {
29 | const result = check[requirement];
30 | handleVisibilityToggle(requirement, result);
31 | }
32 | }
33 | };
34 |
35 | // Allows user to toggle password view
36 | const showPassword = (
37 | event: React.MouseEvent,
38 | ) => {
39 | event.preventDefault();
40 |
41 | const password = document.querySelector('[name="password-component"]');
42 | const showPassword = document.querySelector('.show-password');
43 | const type: string | null = password ? password.getAttribute('type') : null;
44 |
45 | if (password && showPassword) {
46 | if (type === 'password') {
47 | password.setAttribute('type', 'text');
48 | showPassword.innerHTML = 'hide password';
49 | } else {
50 | password.setAttribute('type', 'password');
51 | showPassword.innerHTML = 'show password';
52 | }
53 | }
54 | };
55 |
56 | // Handles password validation as a whole
57 | const handlePassword = (
58 | event: React.ChangeEvent,
59 | passwordRequired = true,
60 | ) => {
61 | if (passwordRequired) {
62 | const password: HTMLInputElement = event.target;
63 | const pattern = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{10,}$/;
64 |
65 | // check off the requirements that are met
66 | handleRequirements(password.value);
67 |
68 | // if pattern is matched
69 | if (pattern.test(password.value)) {
70 | // let user know password is valid
71 | password.classList.add('password-valid');
72 | password.setCustomValidity('');
73 | } else {
74 | // remove styling if password is not valid
75 | password.classList.remove('password-valid');
76 | password.setCustomValidity('Password does not meet minimum requirements');
77 | }
78 | }
79 | };
80 |
81 | function Password(props?: any): JSX.Element {
82 | return (
83 |
87 |
88 | Password Requirements
89 |
90 |
91 |
92 | At least one numeric character{' '}
93 |
94 |
95 |
96 | At least one lowercase character{' '}
97 |
98 |
99 |
100 | At least one uppercase character{' '}
101 |
102 |
103 |
104 | At least 10 characters in length{' '}
105 |
106 |
107 |
108 |
showPassword(event)}
112 | >
113 | show password
114 |
115 |
116 |
117 | {props.label}
118 |
119 |
120 |
121 | handlePassword(event, props.required)}
124 | type='password'
125 | placeholder='password'
126 | name='password-component'
127 | autoComplete={props.autocomplete || 'current-password'}
128 | required={
129 | typeof props.required !== 'undefined' ? props.required : true
130 | }
131 | minLength={10}
132 | />
133 |
134 |
135 | );
136 | }
137 |
138 | export default Password;
139 |
--------------------------------------------------------------------------------
/src/components/authenticate/Reset/Reset.scss:
--------------------------------------------------------------------------------
1 | .auth-recovery {
2 | .fa-envelope {
3 | font-size: 18px;
4 | position: relative;
5 | top: 40px;
6 | left: 15px;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/authenticate/Reset/Reset.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {withA11y} from '@storybook/addon-a11y';
3 |
4 | import Reset from './Reset';
5 |
6 | export default {
7 | title: 'Reset',
8 | decorators: [withA11y],
9 | };
10 |
11 | export const ResetForm = () => {
12 | return (
13 |
24 | );
25 | };
26 |
27 | ResetForm.story = {
28 | parameters: {
29 | notes: "Basic reset form.",
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/authenticate/Reset/Reset.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import notify from '../../utility/Notify';
3 | import './Reset.scss';
4 |
5 | function Reset(): JSX.Element {
6 | const handleReset = () => {
7 | // API route that will handle initiating password reset process.
8 | const url = 'api/authenticate/reset';
9 | const email = document.getElementById('email') as HTMLInputElement;
10 | const spinner: HTMLElement | null = document.getElementById('spinner');
11 |
12 | // show spinner while working
13 | if (spinner) spinner.classList.remove('uk-hidden');
14 |
15 | const data = {
16 | email: email.value,
17 | action: "initiate",
18 | };
19 |
20 | fetch(process.env.HOST + url, {
21 | method: 'POST',
22 | headers: {
23 | 'Content-Type': 'application/json',
24 | },
25 | body: JSON.stringify(data),
26 | }).then((response) => response.json())
27 | .then((response) => {
28 | const {status, message} = response;
29 | // hide spinner as work is essentially done
30 | if (spinner) spinner.classList.add('uk-hidden');
31 |
32 | notify({
33 | message,
34 | status: status ? 'success' : 'danger',
35 | pos: 'top-left',
36 | timeout: 5000,
37 | });
38 | }).catch((error) => {
39 | console.log(error);
40 | });
41 | };
42 |
43 | return (
44 |
45 | Forgot your password?
46 | Enter account email address and we will send you an email to reset your password.
47 |
64 |
65 | );
66 | }
67 |
68 | export default Reset;
69 |
--------------------------------------------------------------------------------
/src/components/authenticate/SignUp/SignUp.scss:
--------------------------------------------------------------------------------
1 | @import '../../../style/_variables.scss';
2 |
3 | .auth-signup {
4 | .fa-exclamation-triangle {
5 | position: relative;
6 | top: 0 !important;
7 | left: 0 !important;
8 | margin-right: 10px;
9 | height: 12px;
10 | color: white;
11 | }
12 |
13 | .fa-user,
14 | .fa-envelope {
15 | position: relative;
16 | top: 42px;
17 | left: 15px;
18 | font-size: 18px;
19 | }
20 |
21 | .fa-envelope {
22 | top: 43px;
23 | }
24 |
25 | .input-spinner,
26 | .uk-spinner,
27 | .fa-check.signup-check {
28 | position: absolute;
29 | top: 45px;
30 | left: -30px;
31 | width: 20px;
32 | }
33 |
34 | .fa-check {
35 | color: $green;
36 | }
37 | }
38 |
39 | .user-message {
40 | display: block;
41 | width: 100%;
42 | color: $snow;
43 | height: 30px;
44 | padding: 10px;
45 | line-height: 10px;
46 | background-color: $yellow;
47 | box-sizing: border-box;
48 | visibility: hidden;
49 |
50 | i {
51 | width: 15px;
52 | position: relative;
53 | color: $snow;
54 | float: right;
55 | font-size: 12px;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/authenticate/SignUp/SignUp.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {withA11y} from '@storybook/addon-a11y';
3 |
4 | import SignUp from './SignUp';
5 |
6 | export default {
7 | title: 'Sign Up',
8 | decorators: [withA11y],
9 | };
10 |
11 | export const SignUpForm = () => {
12 | return (
13 |
27 | );
28 | };
29 |
30 | SignUpForm.story = {
31 | parameters: {
32 | notes: "Basic sign up form.",
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/course/Course.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Styling for course component.
3 | * Individual courses.
4 | */
5 | @import "../../style/variables.scss";
6 |
7 | .course-title {
8 | font-family: "book", sans-serif;
9 | font-size: 18px;
10 | color: #333;
11 | }
12 |
13 | .course-author {
14 | font-family: "light", sans-serif;
15 | font-size: 16px;
16 | color: #7e7e7e;
17 | }
18 |
19 | .course-status {
20 | font-family: "light", sans-serif;
21 | font-size: 14px;
22 | background-color: $green;
23 | text-transform: capitalize;
24 | }
25 |
26 | .course-description {
27 | font-family: "light", sans-serif;
28 | font-size: 14px;
29 | color: $light-gray;
30 | }
31 |
32 | .course-details {
33 | font-size: 14px;
34 | font-family: "light", sans-serif;
35 | color: #7e7e7e;
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/course/Course.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./Course.scss";
3 |
4 | function Course(): JSX.Element {
5 | return (
6 |
7 |
8 |
9 |
10 | Edit
11 | View
12 |
13 |
14 |
15 |
16 |
Armand Villanueva
17 |
18 |
Introduction to Web Development
19 |
20 |
21 | Enrolled
22 |
23 |
24 |
25 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
26 |
27 |
28 |
29 | 1594
30 |
31 |
32 |
33 | 24
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | (4)
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | export default Course;
55 |
--------------------------------------------------------------------------------
/src/components/courses-navigation/Navigation.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Styling for course component.
3 | * Individual courses.
4 | */
5 | .courses-divider {
6 | margin: 25px 0;
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/courses-navigation/Navigation.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import './Navigation.scss';
3 |
4 | function Navigation(): JSX.Element {
5 | return (
6 | <>
7 |
8 |
All Courses
9 |
10 |
11 |
12 | Newly Published
13 | Canceled
14 |
15 |
16 |
17 |
18 |
24 |
25 |
26 |
27 |
28 | >
29 | );
30 | }
31 |
32 | export default Navigation;
33 |
--------------------------------------------------------------------------------
/src/components/courses/Courses.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Styling for all courses page.
3 | * Courses.
4 | */
5 |
6 | #panel-content > div > div.w-20 > figcaption {
7 | opacity: 0%;
8 | cursor: pointer;
9 | margin-top: -25%;
10 | margin-bottom: 24%;
11 | transition-property: all;
12 | transition-duration: 0.3s;
13 | transition-timing-function: ease-in;
14 | transition-delay: 0.2s;
15 | }
16 |
17 | #panel-content > div > div.w-20:hover > figcaption {
18 | opacity: 100%;
19 | }
20 |
21 | @media only screen and (max-width: 1097px) {
22 | #panel-content > div > div.w-20 > figcaption {
23 | margin-top: -16%;
24 | margin-bottom: 12%;
25 | }
26 |
27 | #panel-content > div > div.w-20:hover > figcaption {
28 | opacity: 100%;
29 | }
30 |
31 | #panel-content > div:nth-child(1) {
32 | display: block;
33 | text-align: center;
34 | }
35 |
36 | #panel-content > div:nth-child(1) > div:nth-child(2) {
37 | margin-left: auto;
38 | margin-right: auto;
39 | margin-top: 1%;
40 | width: 30%;
41 | }
42 |
43 | #panel-content > div:nth-child(1) > div:nth-child(3) {
44 | margin-left: auto;
45 | margin-right: auto;
46 | margin-top: 1%;
47 | width: 30%;
48 | }
49 |
50 | #panel-content > div {
51 | display: block;
52 | }
53 |
54 | #panel-content > div > div.w-20 {
55 | width: 50%;
56 | margin-left: auto;
57 | margin-right: auto;
58 | }
59 |
60 | #panel-content > div > div.w-75 {
61 | text-align: center;
62 | margin-left: auto;
63 | margin-right: auto;
64 | width: 52%;
65 | }
66 | }
67 |
68 | @media only screen and (max-width: 753px) {
69 | #panel-content > div:nth-child(1) > div:nth-child(2) {
70 | width: 70%;
71 | }
72 |
73 | #panel-content > div:nth-child(1) > div:nth-child(3) {
74 | width: 70%;
75 | }
76 |
77 | #panel-content > div {
78 | display: block;
79 | }
80 |
81 | #panel-content > div > div.w-20 {
82 | width: 100%;
83 | margin-left: auto;
84 | margin-right: auto;
85 | }
86 |
87 | #panel-content > div > div.w-75 {
88 | text-align: center;
89 | margin-left: auto;
90 | margin-right: auto;
91 | width: 100%;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/components/courses/Courses.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import "./Courses.scss";
3 | import AllCourses from './Courses';
4 |
5 | export default {
6 | title: 'AllCourses',
7 | };
8 |
9 | export const AllTheCourses = () => ;
10 |
--------------------------------------------------------------------------------
/src/components/courses/Courses.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import './Courses.scss';
3 | import CoursesNavBar from '../courses-navigation/Navigation';
4 | import Course from "../course/Course";
5 |
6 | function Courses(): JSX.Element {
7 | return (
8 | <>
9 |
10 |
11 |
12 |
13 |
14 | >
15 |
16 | );
17 | }
18 |
19 | export default Courses;
20 |
--------------------------------------------------------------------------------
/src/components/dashboard/home/DashboardHome.scss:
--------------------------------------------------------------------------------
1 | #dashboard-home {
2 | padding-top: 1em;
3 |
4 | .summary {
5 | .summary-item {
6 | cursor: pointer;
7 | }
8 |
9 | .summary-item-title {
10 | font-family: "Light", sans-serif;
11 | font-size: 16px;
12 | }
13 |
14 | .summary-item-count {
15 | font-family: "Medium", sans-serif;
16 | font-size: 26px;
17 | }
18 | }
19 |
20 | .notifications {
21 | .notification-header {
22 | font-family: "Medium", sans-serif;
23 | font-size: 20px;
24 | }
25 |
26 | .notification-title {
27 | font-family: "Book", sans-serif;
28 | font-size: 16px;
29 | font-weight: bold;
30 | }
31 |
32 | .notification-content {
33 | font-family: "Light", sans-serif;
34 | font-size: 14px
35 | }
36 | }
37 |
38 | // TODO - cleanup duplicate styles in cohort and notifications
39 | .cohort {
40 | .cohort-header {
41 | font-family: "Medium", sans-serif;
42 | font-size: 20px;
43 | }
44 |
45 | .cohort-title {
46 | font-family: "Book", sans-serif;
47 | font-size: 16px;
48 | font-weight: bold;
49 | }
50 |
51 | .cohort-content {
52 | font-family: "Light", sans-serif;
53 | font-size: 14px
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/dashboard/home/DashboardHome.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import DashboardHome, {DashboardHomeProps} from "./DashboardHome";
4 | import {action} from "@storybook/addon-actions";
5 |
6 | export default {
7 | title: "Dashboard Home",
8 | };
9 |
10 | const dashboardData: DashboardHomeProps = {
11 | onSummaryItemClicked: action('summaryItemClicked'),
12 | summaryItems: [
13 | {
14 | title: "Messages",
15 | count: 45,
16 | icon: "fal fa-comments-alt",
17 | url: "dashboard/messages",
18 | },
19 | {
20 | title: "Students",
21 | count: 54,
22 | icon: "fal fa-user-friends",
23 | url: "dashboard/students",
24 | },
25 | {
26 | title: "Courses",
27 | count: 16,
28 | icon: "fal fa-chalkboard-teacher",
29 | url: "dashboard/courses",
30 | },
31 | {
32 | title: "Bookmarks",
33 | count: 27,
34 | icon: "fal fa-heart",
35 | url: "dashboard/bookmarks",
36 | },
37 | ],
38 | notifications: [
39 | {
40 | id: 1,
41 | title: "Message",
42 | content: "Tony: Hey, have you checked out the following resources for the course? They're great!",
43 | },
44 | {
45 | id: 2,
46 | title: "Instructor",
47 | content: "Assignment has been graded - Project One",
48 | },
49 | {
50 | id: 3,
51 | title: "Site Update",
52 | content: "Spark application has been updated to version 1.4.5",
53 | },
54 | {
55 | id: 4,
56 | title: "Status Update",
57 | content: "This is an automated server response message",
58 | },
59 | {
60 | id: 5,
61 | title: "Message",
62 | content: "Felipe: Do you have any notes from the class this past Saturday? HD 4 LIFE!",
63 | },
64 | ],
65 | currentCohort: {
66 | name: "Cohort 24",
67 | updates: [
68 | {
69 | id: 1,
70 | title: "Week 1 - 01/05/2021",
71 | content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Unde esse reprehenderit debitis repellendus dolorem nostrum vel atque fuga magnam! Consequatur sapiente quod ducimus voluptatum, obcaecati optio nulla hic? Perspiciatis, nostrum.",
72 | },
73 | {
74 | id: 2,
75 | title: "Week 2 - 01/05/2021",
76 | content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Unde esse reprehenderit debitis repellendus dolorem nostrum vel atque fuga magnam! Consequatur sapiente quod ducimus voluptatum, obcaecati optio nulla hic? Perspiciatis, nostrum.",
77 | },
78 | {
79 | id: 3,
80 | title: "Week 3 - 01/05/2021",
81 | content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Unde esse reprehenderit debitis repellendus dolorem nostrum vel atque fuga magnam! Consequatur sapiente quod ducimus voluptatum, obcaecati optio nulla hic? Perspiciatis, nostrum.",
82 | },
83 | {
84 | id: 4,
85 | title: "Week 4 - 01/05/2021",
86 | content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Unde esse reprehenderit debitis repellendus dolorem nostrum vel atque fuga magnam! Consequatur sapiente quod ducimus voluptatum, obcaecati optio nulla hic? Perspiciatis, nostrum.",
87 | },
88 | {
89 | id: 5,
90 | title: "Week 5 - 01/05/2021",
91 | content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Unde esse reprehenderit debitis repellendus dolorem nostrum vel atque fuga magnam! Consequatur sapiente quod ducimus voluptatum, obcaecati optio nulla hic? Perspiciatis, nostrum.",
92 | },
93 | ],
94 | },
95 | };
96 |
97 |
98 | export const DefaultDashboardHome = () => (
99 |
100 | );
101 |
102 |
--------------------------------------------------------------------------------
/src/components/dashboard/home/DashboardHome.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./DashboardHome.scss";
3 |
4 | export interface DashboardHomeProps {
5 | summaryItems: SummaryItem[];
6 | notifications: Notification[];
7 | currentCohort: {
8 | name: string;
9 | updates: Update[];
10 | };
11 | onSummaryItemClicked: (id: string) => void;
12 | }
13 |
14 | interface SummaryItem {
15 | title: string;
16 | count: number;
17 | icon: string; // FontAwesome icon string (pro icons available)
18 | url: string;
19 | }
20 |
21 | interface Notification {
22 | id: number;
23 | title: string;
24 | content: string; // TODO: make jsx/html?
25 | }
26 |
27 | interface Update {
28 | id: number;
29 | title: string;
30 | content: string; // TODO: make jsx/html?
31 | }
32 |
33 | const DashboardHome: React.FC = (props) => {
34 | const summaryItems = props.summaryItems.map((item) => (
35 |
36 |
props.onSummaryItemClicked(item.url)}>
39 |
40 |
{item.title}
41 |
{item.count}
42 |
43 |
44 |
45 |
46 |
47 |
48 | ));
49 |
50 | const notifications = props.notifications.map((notification) => (
51 |
52 |
{notification.title}
53 |
{notification.content}
54 |
55 | ));
56 |
57 | const updates = props.currentCohort.updates.map((update) => (
58 |
59 |
{update.title}
60 |
{update.content}
61 |
62 | ));
63 |
64 | return (
65 |
66 |
Dashboard
67 |
68 | {summaryItems}
69 |
70 |
71 |
72 |
Notifications
73 | {notifications}
74 |
75 |
76 |
{props.currentCohort.name}
77 | {updates}
78 |
79 |
80 |
81 | );
82 | };
83 |
84 |
85 | export default DashboardHome;
86 |
--------------------------------------------------------------------------------
/src/components/dashboard/panels/addCourse/AddCourse.scss:
--------------------------------------------------------------------------------
1 | @import '../../../../style/variables';
2 |
3 | .saveButton {
4 | background-color: $tertiary;
5 | color: $white;
6 | }
7 |
8 | .arrowIcon {
9 | margin-left: 10px;
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/global/Authorized/Authorized.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Styling for authorized component.
3 | */
4 | #authorized {
5 | img {
6 | margin: 0 auto;
7 | display: block;
8 | max-width: 200px;
9 | margin-top: 20vh;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/global/Authorized/Authorized.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {withA11y} from '@storybook/addon-a11y';
3 | import marked from 'marked';
4 |
5 | import "./Authorized.scss";
6 | import Authorized from '../Authorized/Authorized';
7 |
8 | const intro = require("./_note_intro.md");
9 | const design = require('./_note_design.md');
10 |
11 | export default {
12 | title: 'Authorized',
13 | decorators: [withA11y],
14 | };
15 |
16 | export const AccessGranted = () => ;
17 |
18 | AccessGranted.story = {
19 | parameters: {
20 | notes: {
21 | "Introduction": marked(intro.default),
22 | 'Design Notes': marked(design.default)},
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/global/Authorized/Authorized.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Authorized.scss';
3 |
4 | function Authorized(): JSX.Element {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
12 | export default Authorized;
13 |
--------------------------------------------------------------------------------
/src/components/global/Authorized/_note_design.md:
--------------------------------------------------------------------------------
1 | ### Design
2 |
3 | ---
4 |
5 | Design lorem ipsum
6 |
7 |
--------------------------------------------------------------------------------
/src/components/global/Authorized/_note_intro.md:
--------------------------------------------------------------------------------
1 | ### Introduction
2 |
3 | ---
4 |
5 | Introduction lorem ipsum
6 |
--------------------------------------------------------------------------------
/src/components/global/ContentContainer/ContentContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | function ContentContainer(props: {content: JSX.Element}): JSX.Element {
3 | // fades content in
4 | return (
5 |
6 | {props.content}
7 |
8 | );
9 | }
10 |
11 | export default ContentContainer;
12 |
--------------------------------------------------------------------------------
/src/components/global/Footer/Footer.scss:
--------------------------------------------------------------------------------
1 | footer {
2 | background-color: #dedede;
3 |
4 | &.stick {
5 | position: absolute;
6 | bottom: 0;
7 | margin: 0 auto;
8 | display: block;
9 | width: 100%;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/global/Footer/Footer.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {withA11y} from '@storybook/addon-a11y';
3 |
4 | import "./Footer.scss";
5 | import Footer from './Footer';
6 |
7 | export default {
8 | title: 'Footer',
9 | decorators: [withA11y],
10 | };
11 |
12 | export const DefaultFooter = () => ;
13 |
14 | DefaultFooter.story = {
15 | parameters: {
16 | notes: "Footer component",
17 |
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/src/components/global/Footer/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import './Footer.scss';
3 |
4 | function Footer(): JSX.Element {
5 | const date = new Date();
6 | return (
7 |
11 | );
12 | }
13 |
14 | export default Footer;
15 |
--------------------------------------------------------------------------------
/src/components/global/Message/Message.scss:
--------------------------------------------------------------------------------
1 | @import '../../../style/variables.scss';
2 |
3 | #global-message {
4 | position: fixed;
5 | top: 0;
6 | width: 100%;
7 | left: 0;
8 | text-align: center;
9 | height: 28px;
10 | z-index: 99999;
11 |
12 | &.bg-primary,
13 | &.bg-secondary,
14 | &.bg-tertiary,
15 | &.bg-blue,
16 | &.bg-red,
17 | &.bg-yellow,
18 | &.bg-green,
19 | &.bg-light-gray,
20 | &.bg-dark-gray {
21 | p {
22 | color: $snow;
23 | }
24 | }
25 |
26 | &.bg-snow {
27 | p {
28 | color: $dark-gray;
29 | }
30 | }
31 | }
32 |
33 | .uk-notification-message {
34 | background: rgba(255, 255, 255, 0.75) !important;
35 | font-weight: 100;
36 | color: $dark-gray;
37 | }
38 |
39 | .uk-notification-message-danger {
40 | border-left: 10px solid $red;
41 | }
42 |
43 | .uk-notification-message-success {
44 | border-left: 10px solid $green;
45 | }
46 |
47 | .uk-notification-message-warning {
48 | border-left: 10px solid $yellow;
49 | }
50 |
51 | .uk-notification-message-primary {
52 | border-left: 10px solid $blue;
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/global/Message/Message.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {withA11y} from '@storybook/addon-a11y';
3 | import "../../../style/index.scss";
4 | import "./Message.scss";
5 | import Message from '../Message/Message';
6 |
7 | export default {
8 | title: 'Message',
9 | decorators: [withA11y],
10 | };
11 |
12 | export const BasicMessage = () =>
13 | ;
16 |
17 | export const ReminderAltMessage = () =>
18 | ;
21 |
22 |
23 | export const ReminderMessage = () =>
24 | ;
27 |
28 | export const ConfirmationMessage = () =>
29 | ;
32 |
33 | export const SuccessMessage = () =>
34 | ;
37 |
38 | export const WarningMessage = () =>
39 | ;
42 |
43 | export const ErrorMessage = () =>
44 | ;
47 |
48 | export const PrimaryMessage = () =>
49 | ;
52 |
53 | export const SecondaryMessage = () =>
54 | ;
57 |
58 | export const TertiaryMessage = () =>
59 | ;
62 |
--------------------------------------------------------------------------------
/src/components/global/Message/Message.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Message.scss';
3 |
4 | interface Message {
5 | message: string;
6 | priority: number;
7 | hidden: boolean;
8 | }
9 |
10 | function Message(props: Message): JSX.Element {
11 | const colors = ['dark-gray', 'light-gray', 'snow', 'blue', 'green', 'yellow', 'red', 'primary', 'secondary', 'tertiary'];
12 | return (
13 |
14 | {props.message}
15 |
16 | );
17 | }
18 |
19 | export default Message;
20 |
--------------------------------------------------------------------------------
/src/components/global/Navigation/Navigation.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {storiesOf} from '@storybook/react';
3 | import Navigation from '../Navigation/Navigation';
4 |
5 | storiesOf('Navigation', module).add('navigation station', () => {
6 | return ;
7 | });
8 |
--------------------------------------------------------------------------------
/src/components/global/Navigation/Navigation.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-use-before-define */
2 | import React from 'react';
3 | import Link from 'next/link';
4 | import './Navigation.scss';
5 | import User from './User/User';
6 | export interface Options {
7 | [option: string]: any;
8 | }
9 | export interface ListObject {
10 | icon?: string | boolean;
11 | children?: Array | boolean;
12 | target?: string;
13 | url?: string;
14 | label?: string;
15 | };
16 |
17 | const identifyLink = (url: string) => url.indexOf(`${process.env.HOST}`);
18 |
19 | const renderLink = (obj: ListObject, opts: object) => {
20 | const { url = undefined } = obj;
21 | return identifyLink(url ? url : '') !== -1 ?
22 |
23 | {obj.label}
24 | :
25 |
26 | {obj.label}
27 | ;
28 | };
29 |
30 | const createListItem = (
31 | obj: ListObject,
32 | mobile = false,
33 | ) => {
34 | const opts = {} as Options;
35 |
36 | if (obj.icon) {
37 | opts["uk-icon"] = "plus";
38 | }
39 |
40 | if (obj.target !== undefined) {
41 | opts["target"] = obj.target;
42 | }
43 |
44 | if (!obj.children && mobile) {
45 | opts["uk-toggle"] = "target: #offcanvas-nav";
46 | }
47 |
48 | return (
49 |
52 |
53 | {renderLink(obj, opts)}
54 |
55 | {obj.children && mobile && getMobileSubList(obj)}
56 | {obj.children && !mobile && getSubList(obj)}
57 |
58 | );
59 | };
60 |
61 | const getSubList = (obj: ListObject) =>
62 |
63 |
64 | {(obj.children as Array) ? (obj.children as Array).map((child: ListObject) => createListItem(child)) : ""}
65 |
66 |
;
67 |
68 | const getMobileSubList = (obj: Options) =>
69 |
70 | {obj.children.map((child: ListObject) => createListItem(child))}
71 | ;
72 |
73 | const createListObject = ({
74 | label,
75 | url = "#",
76 | icon = false,
77 | children = false,
78 | target,
79 | }: ListObject) => ({ label, url, icon, children, target });
80 |
81 |
82 | function Navigation(): JSX.Element {
83 | const navigation: Array = [
84 | { label: "Home", url: "/" },
85 | { label: "Cohorts", url: "/cohorts" },
86 | { label: "Blog", url: "https://www.iesd.com/#blog", target: "_blank" },
87 | { label: "Contact", url: "mailto:community@iesd.com", target: "_blank" },
88 | ].map((item) => createListObject(item));
89 |
90 | return (
91 |
92 |
93 | {/* Left portion for the logo */}
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | {
102 | navigation.map((link) =>
103 | createListItem(link),
104 | )
105 | }
106 |
107 |
108 |
109 | {/* Right portion for user */}
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | {
120 | navigation.map((link) =>
121 | createListItem(link, true),
122 | )
123 | }
124 |
125 |
126 |
127 |
128 | );
129 | }
130 |
131 | export default Navigation;
132 |
--------------------------------------------------------------------------------
/src/components/global/Navigation/User/Messages/Messages.tsx:
--------------------------------------------------------------------------------
1 | import React, {useContext, useState, useEffect} from 'react';
2 | import Link from "next/link";
3 | import {Context} from '../../../../../context';
4 |
5 | function getMessageNotifications(props: {count: number | undefined; mobile: boolean}): JSX.Element {
6 | const {count, mobile} = props;
7 | const notifications = (count !== undefined && count > 0 && !mobile) || (mobile && count !== undefined && count >= 0);
8 |
9 | return (
10 | <>
11 |
12 | {count}
13 |
14 |
15 |
16 | >
17 | );
18 | }
19 |
20 | function Messages(props: {mobile: boolean}): JSX.Element {
21 | const {mobile = false} = props;
22 | const {user, userID} = useContext(Context);
23 | const data = {
24 | user,
25 | userID,
26 | };
27 |
28 | const [messageCount, setMessageCount]= useState({
29 | "count": undefined,
30 | });
31 |
32 | useEffect(() => {
33 | const abortController = new AbortController();
34 |
35 | if (userID !== undefined && user !== undefined) {
36 | fetch(process.env.HOST + "api/messages", {
37 | method: 'POST',
38 | headers: {
39 | 'Content-Type': 'application/json',
40 | },
41 | body: JSON.stringify(data),
42 | })
43 | .then((response) => response.json())
44 | .then((response) => {
45 | setMessageCount({
46 | count: response,
47 | });
48 | });
49 | }
50 |
51 | return () => {
52 | abortController.abort();
53 | };
54 | }, []);
55 |
56 | return (
57 |
58 |
59 | {getMessageNotifications({count: messageCount.count, mobile})}
60 |
61 |
62 | );
63 | }
64 |
65 | export default Messages;
66 |
--------------------------------------------------------------------------------
/src/components/global/Navigation/User/Notifications/Notifications.tsx:
--------------------------------------------------------------------------------
1 | import React, {useContext, useState, useEffect} from 'react';
2 | import Link from "next/link";
3 | import {Context} from '../../../../../context';
4 |
5 | function getNotifications(props: {count: number | undefined; mobile: boolean}): JSX.Element {
6 | const {count, mobile} = props;
7 | const notifications = (count !== undefined && count > 0 && !mobile) || (mobile && count !== undefined && count >= 0);
8 |
9 | return (
10 | <>
11 |
12 | {count}
13 |
14 |
15 |
16 | >
17 | );
18 | }
19 |
20 | function Notifications(props: {mobile: boolean}): JSX.Element {
21 | const {mobile = false} = props;
22 | const {user, userID} = useContext(Context);
23 | const data = {
24 | user,
25 | userID,
26 | };
27 |
28 | const [notificationCount, setNotificationCount]= useState({
29 | "count": undefined,
30 | });
31 |
32 | useEffect(() => {
33 | const abortController = new AbortController();
34 |
35 | if (userID !== undefined && user !== undefined) {
36 | fetch(process.env.HOST + "api/notifications", {
37 | method: 'POST',
38 | headers: {
39 | 'Content-Type': 'application/json',
40 | },
41 | body: JSON.stringify(data),
42 | })
43 | .then((response) => response.json())
44 | .then((response) => {
45 | setNotificationCount({
46 | count: response,
47 | });
48 | });
49 | }
50 |
51 | return () => {
52 | abortController.abort();
53 | };
54 | }, []);
55 |
56 | return (
57 |
58 |
59 | {getNotifications({count: notificationCount.count, mobile})}
60 |
61 |
62 | );
63 | }
64 |
65 | export default Notifications;
66 |
67 |
--------------------------------------------------------------------------------
/src/components/global/Sidebar/Sidebar.scss:
--------------------------------------------------------------------------------
1 | @import '../../../style/variables.scss';
2 |
3 | $selectedLinkColor: #f07f6b; // color in style guidelines is different from primary, secondary, and tertiary
4 | $selectedLinkBackground: rgba(0, 0, 0, 0.01);
5 | $transitionTime: 0.2s;
6 |
7 | #sidebar-container {
8 | min-width: 0;
9 | left: 0;
10 |
11 | &.open {
12 | left: -100%;
13 | min-width: 220px;
14 | }
15 | }
16 |
17 | #dashboard-sidebar {
18 | width: 220px;
19 | z-index: 9;
20 | right: unset;
21 | }
22 |
23 | #sidebar-toggle {
24 | z-index: 10;
25 | position: absolute;
26 | color: #ef906c;
27 | font-size: 18px;
28 | width: 125px;
29 | left: 30px;
30 | top: 90px;
31 |
32 | &::after {
33 | content: " toggle sidebar";
34 | padding-left: 15px;
35 | font-size: 12px;
36 | position: relative;
37 | top: -3px;
38 | }
39 | }
40 |
41 | .sidebar-panel {
42 | position: fixed;
43 | width: 0;
44 | transition: width $transitionTime ease-in-out;
45 | padding: 0 0 0 0;
46 | bottom: 0;
47 | overflow-x: hidden;
48 | overflow-y: auto;
49 | background-color: white;
50 | z-index: 10;
51 |
52 | &.open {
53 | width: $sidebar-width;
54 | }
55 |
56 | .scroll-content {
57 | height: 100%;
58 | overflow: visible;
59 | box-sizing: content-box;
60 | border-right: 1px solid #e8e8e8;
61 | }
62 |
63 | .close-button {
64 | box-sizing: border-box;
65 | width: inherit;
66 | display: flex;
67 | justify-content: flex-end;
68 |
69 | span {
70 | cursor: pointer;
71 | padding: 16px 10px 10px 10px;
72 | }
73 | }
74 |
75 | .menu-primary {
76 | box-sizing: content-box;
77 | visibility: visible;
78 | font-family: "Light", sans-serif;
79 | font-size: 16px;
80 | color: $light-gray;
81 | list-style: none;
82 | width: inherit;
83 | border-right: 1px solid #e8e8e8;
84 |
85 | li {
86 | cursor: pointer;
87 | transition: $transitionTime ease-in-out;
88 |
89 | &:hover {
90 | color: $selectedLinkColor;
91 | background-color: $selectedLinkBackground;
92 | }
93 |
94 | .primary-item-label {
95 | padding: 12px 18px 12px 24px;
96 | display: flex;
97 | align-items: center;
98 | border-right: 2px solid transparent; // so chevron angle doesn't move for active item
99 |
100 | .sub-angle-down {
101 | transition: transform $transitionTime ease-in-out;
102 | margin-left: auto;
103 | }
104 |
105 | .rotate {
106 | transform: rotate(180deg);
107 | }
108 | }
109 |
110 | .active {
111 | color: $selectedLinkColor;
112 | background-color: $selectedLinkBackground;
113 | border-color: $selectedLinkColor;
114 | }
115 | }
116 | }
117 |
118 | .menu-secondary {
119 | transition: height 0.2s ease-in-out;
120 | overflow-y: hidden;
121 | background-color: white;
122 |
123 | ul {
124 | color: $light-gray;
125 |
126 | li {
127 | padding: 8px 0 8px 48px;
128 |
129 | &:hover {
130 | background-color: white;
131 | }
132 | }
133 | }
134 | }
135 |
136 | .icon {
137 | min-width: 36px;
138 | margin-right: 0.5em;
139 | }
140 |
141 | .icon-hidden {
142 | opacity: 0;
143 | }
144 |
145 | .mobile-close {
146 | color: $light-gray;
147 | display: flex;
148 | justify-content: flex-end;
149 |
150 | span {
151 | cursor: pointer;
152 | padding: 12px; // make larger touch area for clicking close button (helps on mobile)
153 | overflow: overlay;
154 | }
155 | }
156 |
157 | .section-title {
158 | font-family: "Medium", sans-serif;
159 | font-size: 24px;
160 | color: $dark-gray;
161 | margin: 24px 0 24px 24px;
162 | }
163 |
164 | .spacer {
165 | height: 24px;
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/components/global/Sidebar/Sidebar.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import {withA11y} from "@storybook/addon-a11y";
3 | import {action} from "@storybook/addon-actions";
4 | import {withKnobs, boolean} from "@storybook/addon-knobs";
5 | import "../../../style/index.scss";
6 | import Sidebar from "./Sidebar";
7 | import marked from "marked";
8 |
9 | const usage = require('./usage.md');
10 |
11 |
12 | export default {
13 | title: "Sidebar",
14 | decorators: [withA11y, withKnobs],
15 | excludeStories: /.*Data$/,
16 | parameters: {
17 | notes: {
18 | "Usage": marked(usage.default),
19 | },
20 | },
21 | };
22 |
23 | export const mainItemsData = [
24 | {
25 | icon: "far fa-tachometer-alt-fastest",
26 | label: "Dashboard",
27 | path: "/dashboard",
28 | subItems: [
29 | {label: "Overview", path: "/dashboard"},
30 | {label: "Updates", path: "/dashboard/updates"},
31 | ],
32 | },
33 | {
34 | icon: "far fa-users-class",
35 | label: "Courses",
36 | path: "/courses",
37 | subItems: [
38 | {label: "All Courses", path: "/courses"},
39 | {label: "Add Course", path: "/courses/add"},
40 | {label: "Category", path: "/courses/categories"},
41 | {label: "Tags", path: "/courses/tags"},
42 | {label: "Manage", path: "/courses/manage"},
43 | ],
44 | },
45 | {icon: "far fa-comment-alt-edit", label: "Lessons", path: "/lessons"},
46 | {icon: "far fa-comments-alt", label: "Messages", path: "/messages"},
47 | {icon: "far fa-bookmark", label: "Bookmarks", path: "/bookmarks"},
48 | {icon: "far fa-heart", label: "Resources", path: "/resources"},
49 | {icon: "far fa-users", label: "Community", path: "/community"},
50 | ];
51 |
52 | export const accountItemsData = [
53 | {
54 | icon: "far fa-cog",
55 | label: "Settings",
56 | path: "/settings",
57 | subItems: [
58 | {label: "Profile", path: "user/profile"},
59 | {label: "Preferences", path: "user/preferences"},
60 | {label: "Account", path: "user/account"},
61 | ],
62 | },
63 | {icon: "far fa-sign-out", label: "Logout", path: "/logout"},
64 | ];
65 |
66 | const missingIconData = [...mainItemsData];
67 | missingIconData.push({icon: "", label: "Nothing", path: ""});
68 |
69 | const actionsData = {
70 | onNavigate: action("onNavigate"),
71 | };
72 |
73 | export const DefaultSidebar = () => (
74 |
81 | );
82 |
83 | export const MissingIcon = () => (
84 |
91 | );
92 |
93 | export const CloseButtonOnSmallScreen = () => (
94 |
101 | );
102 |
103 |
--------------------------------------------------------------------------------
/src/components/global/Sidebar/usage.md:
--------------------------------------------------------------------------------
1 | # Sidebar
2 |
3 | ## Storybook
4 |
5 | Make sure to use the storybook "knobs" to toggle the sidebar. Also see actions to view the paths emitted when clicking on the menu items.
6 |
7 | ## Usage
8 |
9 | This React component displays an [Off-canvas](https://getuikit.com/docs/offcanvas) component containing [Nav](https://getuikit.com/docs/nav) UIkit component.
10 |
11 | The component accepts the following properties to modify its content and behavior.
12 |
13 | ### Accepted Props
14 | An explantion of the props are provided here. All props are typed and examples of provided props are included in Sidebar.stories.tsx.
15 |
16 | #### menuItems
17 |
18 | Accepts an array of navigation items. Each menu item contains the following properties.
19 | * icon: string - The full name of the [font-awesome icon](https://fontawesome.com/icons?from=io) to be used including the icon set.
20 | * label: string - The label displayed in the sidebar.
21 | * path: string - The path emitted in onNavigate if this item is clicked. If this item has subItems (see below), navigation is disabled for this item and enabled only for subItems.
22 | * subItems: A list of items which can be expanded under this main menu item. If included, the main item will expand and close to toggle the display of these items. These subItems include the following properties:
23 | * label: string - The label for this submenu item
24 | * path: string - The path emitted in onNavigate when this item is clicked.
25 |
26 | #### accountMenuItems
27 |
28 | This accepts the same properties as menu items. However, the these items will be included in an accounts section beneath the main menu items.
29 |
30 | #### isOpen
31 |
32 | A boolean value used to toggle the [Off-canvas](https://getuikit.com/docs/offcanvas) visibility. This allows the menu visibility to be controlled (for example on mobile)
33 |
34 | #### activePath
35 | Will be used to keep the active parent item path highlighted. Submenu items are not highlighted.
36 |
37 |
38 | #### onNavigate
39 |
40 | A function that will receive the navigation path of an item clicked. It an item contains subItems, clicking it will toggle subItems instead of calling onNavigate. Clicking the subItems will call onNavigate.
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/components/global/Spinner/Spinner.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Styling for spinner component.
3 | */
4 | #spinner {
5 | z-index: 9999;
6 | position: fixed;
7 | height: 100%;
8 | width: 100%;
9 | left: 0;
10 | background-color: rgba(255, 255, 255, 0.8);
11 |
12 | [uk-spinner] {
13 | display: block;
14 | position: relative;
15 | top: 30vh;
16 | text-align: center;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/global/Spinner/Spinner.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {withA11y} from '@storybook/addon-a11y';
3 | import "../../../style/index.scss";
4 | import "./Spinner.scss";
5 | import Spinner from '../Spinner/Spinner';
6 |
7 | export default {
8 | title: 'Spinner',
9 | decorators: [withA11y],
10 | };
11 |
12 | export const DefaultSpinner = () => ;
13 |
14 | DefaultSpinner.story = {
15 | parameters: {
16 | notes: 'This component is used whenever a user has to wait for something to finish loading.',
17 | },
18 | };
19 |
20 |
--------------------------------------------------------------------------------
/src/components/global/Spinner/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './Spinner.scss';
3 |
4 | function Spinner(props: any): JSX.Element {
5 | const {classes, ratio = 4} = props;
6 | return (
7 |
10 | );
11 | }
12 |
13 | export default Spinner;
14 |
--------------------------------------------------------------------------------
/src/components/global/Unauthorized/Unauthorized.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Styling for the unauthorized component.
3 | */
4 | #unauthorized {
5 | img {
6 | margin: 0 auto;
7 | display: block;
8 | max-width: 200px;
9 | margin-top: 20vh;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/global/Unauthorized/Unauthorized.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {withA11y} from '@storybook/addon-a11y';
3 | import "./Unauthorized.scss";
4 | import Unauthorized from '../Unauthorized/Unauthorized';
5 |
6 | export default {
7 | title: 'Unauthorized',
8 | decorators: [withA11y],
9 | };
10 |
11 | export const AccessDenied = () => ;
12 |
13 | AccessDenied.story = {
14 | parameters: {
15 | notes: 'This component is used whenever a user has been access granted to something globally.',
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/global/Unauthorized/Unauthorized.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SEO from "../../../../next-seo.config";
3 | import {DefaultSeo} from "next-seo";
4 | import "./Unauthorized.scss";
5 |
6 | function Unauthorized(): JSX.Element {
7 | return (
8 |
9 |
13 |
14 |
15 | );
16 | }
17 |
18 | export default Unauthorized;
19 |
--------------------------------------------------------------------------------
/src/components/landing/CourseProgramCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export interface CourseProgramProps {
4 | id: string;
5 | title: string;
6 | contents: string[];
7 | nextDay: string;
8 | nextMonth: string;
9 | classTimeString: string;
10 | }
11 |
12 | const CourseProgramCard: React.FC> = (props) => (
13 |
14 |
15 |
16 |
{props.nextDay}
17 |
{props.nextMonth}
18 |
{props.classTimeString}
19 |
20 |
21 |
{props.title}
22 |
23 | {props.contents.map((item, index) => (
24 | {item}
25 | ),
26 | )}
27 |
28 |
29 |
30 |
31 | );
32 |
33 | export default CourseProgramCard;
34 |
--------------------------------------------------------------------------------
/src/components/landing/Landing.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {action} from "@storybook/addon-actions";
3 | import Landing from '../landing/Landing';
4 | import {CourseProgramProps} from './CourseProgramCard';
5 |
6 | export default {
7 | title: "Landing Page",
8 | decorators: [],
9 | excludeStories: /.*Data$/,
10 | };
11 |
12 | const coursesData: CourseProgramProps[] = [
13 | {
14 | id: "1",
15 | title: "React Fundamentals",
16 | nextDay: "24th",
17 | nextMonth: "June",
18 | classTimeString: "Every Saturday. 1:00 PM - 3:00 PM",
19 | contents: [
20 | "Basics of React",
21 | "Breakdown of the library",
22 | "Creating web apps with the library",
23 | ],
24 | },
25 | {
26 | id: "2",
27 | title: "GraphQL",
28 | nextDay: "23th",
29 | nextMonth: "June",
30 | classTimeString: "Every Friday. 3:00 PM - 5:00 PM",
31 | contents: [
32 | "Understanding queries",
33 | "Creating a GraphQL Server",
34 | "Client-side libaries",
35 | ],
36 | },
37 | {
38 | id: "3",
39 | title: "Databases",
40 | nextDay: "22th",
41 | nextMonth: "June",
42 | classTimeString: "Every Thursday. 7:00 PM - 9:00 PM",
43 | contents: [
44 | "SQL Training",
45 | "NoSQL vs SQL",
46 | "Cloud-managed databases",
47 | ],
48 | },
49 | ];
50 |
51 | const landingDefaultData = {
52 | coverImage: "/images/landing/landingcover.jpg",
53 | venueImage: "/images/landing/riverside-sample.jpg",
54 | programImage: "/images/spark.png",
55 | cohortInfo: {
56 | startDay: "15th",
57 | startMonth: "April",
58 | durationWeeks: 10,
59 | classTimeString: "Every Saturday. 12:00 PM - 3:00 PM",
60 | seats: 16,
61 | },
62 | courses: coursesData,
63 | studentQuote: "This program changed my life. Oh em gee. The instructor is amazing and the students are all passionate.",
64 | studentQuoteAuthor: "Cutie Pie",
65 | venueDescription: "Our program will take place at one of the fastest growing incubators in the city of Riverside. It can host large meetings, be used as a co-hosting space, and is at the center of the bustling city.",
66 | venueAddress: {
67 | street1: "3499 Tenth St.",
68 | city: "Riverside",
69 | state: "CA",
70 | zip: "92501",
71 | },
72 | programDescription: "Spark program is a community driven learning program that provides students a working set of skills to get started building useful applications. Our courses are realistically designed and we make no false promises.",
73 | inspirationalQuote: "Education is the passport to the future, for tomorrow belongs to those who prepare for it today.",
74 | inspirationalQuoteAuthor: "Malcom X",
75 | };
76 |
77 | const actionsData = {
78 | onCourseSelected: action("onCourseSelected"),
79 | onInformationRequested: action("onInformationRequested"),
80 | };
81 |
82 | export const DefaultLandingPage = () => {
83 | return ;
87 | };
88 |
--------------------------------------------------------------------------------
/src/components/layouts/DashboardLayout.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Dashboard page style page
3 | */
4 | @import '../../style/variables';
5 |
6 | #dashboard-container {
7 | background-color: $snow;
8 | }
9 |
10 | #sidebar-container {
11 | transition: width $sidebar-transition-time ease-in-out;
12 | width: 0;
13 | // has(.open) wasn't working for some reason
14 | &.open {
15 | min-width: $sidebar-width;
16 | width: $sidebar-width;
17 | }
18 | }
19 |
20 | @media (max-width: $sidebar-overlay-width) {
21 | #sidebar-container {
22 | // has(.open) wasn't working for some reason
23 | &.open {
24 | min-width: 0;
25 | width: 0;
26 | }
27 | }
28 | }
29 |
30 | .icon-button-container {
31 | display: flex;
32 | top: 30px;
33 | left: 30px;
34 | height: 0;
35 | width: max-content;
36 | cursor: pointer;
37 | align-items: center;
38 | font-family: "Medium", sans-serif;
39 | transition: color 0.2s ease-in;
40 | position: relative;
41 |
42 | .icon-button {
43 | svg {
44 | width: 24px;
45 | }
46 | }
47 |
48 | &:hover {
49 | color: #f07f6b;
50 | }
51 | }
52 |
53 | #panel-container {
54 | width: 100%;
55 |
56 | #panel-content {
57 | padding: 55px 25px 50px 30px;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/layouts/DashboardLayout.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { useRouter } from 'next/router';
3 | import { Context, SidebarOptions } from '../../context';
4 | import { DefaultSeo } from "next-seo";
5 | import SEO from "../../../next-seo.config";
6 | import Sidebar from '../../components/global/Sidebar/Sidebar';
7 | import Navigation from '../../components/global/Navigation/Navigation';
8 | import "./DashboardLayout.scss";
9 |
10 | const Dashboard: React.FC = function (props) {
11 | const context = useContext(Context);
12 | const router = useRouter();
13 | const { user, sidebarIsOpen } = context;
14 | const { children } = props;
15 | const { account, main } = SidebarOptions; // TODO: add ability to save data into DB and retrieve for menu generaetion
16 |
17 | const onNavigate = (path: string) => {
18 | context.setContextProperty({
19 | activeDashboardPath: path,
20 | });
21 | router.push(path);
22 | };
23 |
24 | const handleOpenSidebar = () => {
25 | context.setContextProperty({
26 | sidebarIsOpen: true,
27 | });
28 | };
29 | // TODO: Create system to pull contents of correct panel based on context.
30 |
31 | const menuToggleIcon = sidebarIsOpen ?
32 | null : (
33 | handleOpenSidebar()}
36 | >
37 | Menu
38 |
42 |
43 | );
44 |
45 | return (
46 |
47 |
48 | {/* // TODO: Need to make it so we just need to call - low priority. */}
49 |
50 |
51 |
onNavigate(path)} />
57 |
58 |
59 | {menuToggleIcon}
60 |
61 | {children}
62 |
63 |
64 |
65 |
66 |
67 |
71 |
72 | );
73 | };
74 |
75 | export default Dashboard;
76 |
--------------------------------------------------------------------------------
/src/components/manage-students/ManageStudents.scss:
--------------------------------------------------------------------------------
1 | @import '../../style/variables.scss';
2 |
3 | .manage-students {
4 | .uk-tab {
5 | a {
6 | font-family: "Book", sans-serif;
7 | font-size: 20px;
8 | color: $light-gray;
9 | border-bottom: 2px solid transparent;
10 | }
11 |
12 | .uk-active {
13 | a {
14 | border-color: $tertiary;
15 | color: $dark-gray;
16 | }
17 | }
18 | }
19 |
20 | .uk-checkbox {
21 | transition: none;
22 |
23 | &:checked {
24 | background-color: $tertiary;
25 | }
26 |
27 | &:indeterminate {
28 | background-color: $tertiary;
29 | }
30 | }
31 |
32 | .uk-breadcrumb {
33 | font-family: "Book", sans-serif;
34 | font-size: 18px;
35 |
36 | :first-child {
37 | font-family: "Medium", sans-serif;
38 | color: $black;
39 | }
40 | }
41 |
42 | .student-photo {
43 | width: 64px;
44 | height: 64px;
45 | border-radius: 50%;
46 | overflow: hidden;
47 | display: inline-block;
48 |
49 | img {
50 | width: 100%;
51 | min-height: 100%;
52 | height: 100%;
53 | object-fit: cover;
54 | object-position: center;
55 | }
56 | }
57 |
58 | button {
59 | background-color: $tertiary;
60 | color: $white;
61 | transition: color linear 0.1s;
62 | transition: background-color linear 0.1s;
63 |
64 | &:hover {
65 | background-color: #e56c21;
66 | }
67 |
68 | &:disabled {
69 | background-color: white;
70 | color: rgba(98, 98, 98, 0.3);
71 | border: 1px solid rgba(98, 98, 98, 0.3);
72 | }
73 | }
74 |
75 | table {
76 | font-family: "Book", sans-serif;
77 | font-size: 16px;
78 | color: $light-gray;
79 |
80 | th {
81 | font-family: "Light", sans-serif;
82 | font-size: 18px;
83 | }
84 |
85 | td {
86 | vertical-align: middle;
87 |
88 | img {
89 | height: 150px;
90 | }
91 |
92 | a {
93 | color: $dark-gray;
94 | }
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/components/manage-students/ManageStudents.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import ManageStudents from "./ManageStudents";
4 | import {action} from "@storybook/addon-actions";
5 |
6 | export default {
7 | title: 'ManageStudents',
8 | };
9 |
10 | // used to populate cource data
11 | const studentData = [
12 | {
13 | id: "1",
14 | avatarUrl: "https://images-na.ssl-images-amazon.com/images/M/MV5BMjA3NjYzMzE1MV5BMl5BanBnXkFtZTgwNTA4NDY4OTE@._V1_UX172_CR0,0,172,256_AL_.jpg",
15 | firstName: "Ana",
16 | lastName: "De Armas",
17 | email: "why@benaffleck.com",
18 | status: "Active",
19 | },
20 | {
21 | id: "2",
22 | avatarUrl: "https://images-na.ssl-images-amazon.com/images/M/MV5BOWViYjUzOWMtMzRkZi00MjNkLTk4M2ItMTVkMDg5MzE2ZDYyXkEyXkFqcGdeQXVyODQwNjM3NDA@._V1_UY256_CR36,0,172,256_AL_.jpg",
23 | firstName: "Adam",
24 | lastName: "Driver",
25 | email: "causing@fear.darkside.gov",
26 | status: "Inactive",
27 | },
28 | {
29 | id: "3",
30 | avatarUrl: "https://images-na.ssl-images-amazon.com/images/M/MV5BMTQzMjkwNTQ2OF5BMl5BanBnXkFtZTgwNTQ4MTQ4MTE@._V1_UY256_CR15,0,172,256_AL_.jpg",
31 | firstName: "Ryan",
32 | lastName: "Gosling",
33 | email: "ryan@cityofstars.com",
34 | status: "Inactive",
35 | },
36 | {
37 | id: "4",
38 | avatarUrl: "https://images-na.ssl-images-amazon.com/images/M/MV5BMTgyNzE5OTkzMV5BMl5BanBnXkFtZTgwNzM4ODAzMjE@._V1_UY256_CR1,0,172,256_AL_.jpg",
39 | firstName: "Sophie",
40 | lastName: "Turner",
41 | email: "missedthewhole@gotthing.co.uk",
42 | status: "Dragons or something",
43 | },
44 | {
45 | id: "5",
46 | avatarUrl: "https://images-na.ssl-images-amazon.com/images/M/MV5BMjI4NjM1NDkyN15BMl5BanBnXkFtZTgwODgyNTY1MjE@._V1.._UX172_CR0,0,172,256_AL_.jpg",
47 | firstName: "Emma",
48 | lastName: "Stone",
49 | email: "emmastone@hollywoodelite.com",
50 | status: "Singing better than gosling",
51 | },
52 | {
53 | id: "6",
54 | avatarUrl: "https://m.media-amazon.com/images/M/MV5BMjExOTY3NzExM15BMl5BanBnXkFtZTgwOTg1OTAzMTE@._V1_UX172_CR0,0,172,256_AL_.jpg",
55 | firstName: "Michael B.",
56 | lastName: "Jordan",
57 | email: "qb1@fridaynightlights.com",
58 | status: "Tossing TD's",
59 | },
60 | {
61 | id: "7",
62 | avatarUrl: "https://images-na.ssl-images-amazon.com/images/M/MV5BNjYyNjg1OTU1M15BMl5BanBnXkFtZTgwNzYyNTkzMDI@._V1_UX172_CR0,0,172,256_AL_.jpg",
63 | firstName: "Danai",
64 | lastName: "Gurira",
65 | email: "wakandaforever@hidden.com",
66 | status: "Hiding in Wakanda",
67 | },
68 | ];
69 |
70 | const coursesData = [
71 | {
72 | id: "1",
73 | code: "WEB101",
74 | name: "Intro to Web Developmment",
75 | students: [
76 | {...studentData[0]},
77 | {...studentData[1]},
78 | {...studentData[2]},
79 | {...studentData[3]},
80 | ],
81 | },
82 | {
83 | id: "2",
84 | code: "WEB102",
85 | name: "Intermediate Web Developmment",
86 | students: [
87 | {...studentData[3]},
88 | {...studentData[4]},
89 | {...studentData[5]},
90 | {...studentData[6]},
91 | ],
92 | },
93 | {
94 | id: "3",
95 | code: "CS101",
96 | name: "CS Fundamentals in Python",
97 | students: [studentData[3], studentData[6]],
98 | },
99 | {
100 | id: "4",
101 | code: "GD101",
102 | name: "Graphic Design Fundamentals",
103 | students: [
104 | {...studentData[5]},
105 | {...studentData[0]},
106 | ],
107 | },
108 | ];
109 |
110 |
111 | const actionsData = {
112 | onManageUser: action('onManageUser'),
113 | onViewUser: action('onViewUser'),
114 | onRemoveUsersFromCourse: action('onDeleteUsers'),
115 | };
116 |
117 | export const DefaultManageStudents = () => (
118 |
122 | );
123 |
--------------------------------------------------------------------------------
/src/components/panel/Panel.scss:
--------------------------------------------------------------------------------
1 | #panel-container {
2 | width: 100%;
3 |
4 | #panel-content {
5 | max-width: 1080px;
6 | padding: 60px 25px 50px 30px;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/panel/Panel.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./Panel.scss";
3 |
4 | const Panel: React.FC = function(props): JSX.Element {
5 | const {children} = props;
6 | return (
7 |
8 |
9 | {children}
10 |
11 |
12 | );
13 | };
14 |
15 | export default Panel;
16 |
--------------------------------------------------------------------------------
/src/components/user-info-input/UserInfoInput.scss:
--------------------------------------------------------------------------------
1 | @import '../../style/variables.scss';
2 |
3 | .uk-input {
4 | color: black;
5 | }
6 |
7 | .profile-pic-container {
8 | min-width: 182px;
9 | min-height: 170px;
10 | }
11 |
12 | .img-upload-container {
13 | position: relative;
14 | margin: auto;
15 | height: 100%;
16 |
17 | &.uk-placeholder {
18 | padding: 0;
19 | }
20 |
21 | .uk-form-custom {
22 | position: absolute;
23 | bottom: 0;
24 | left: 0;
25 | }
26 |
27 | img {
28 | width: -moz-available;
29 | width: -webkit-fill-available;
30 | width: fill-available; //stretch
31 | object-fit: cover;
32 | min-height: 100%;
33 | }
34 | }
35 |
36 | .uiif-textarea-width {
37 | resize: vertical;
38 | }
39 |
40 | .uiif-button {
41 | background-color: $tertiary;
42 | color: $snow;
43 | }
44 |
45 | .fas-icon {
46 | margin-left: 10px;
47 | }
48 |
49 | .uk-modal {
50 | z-index: 9999999;
51 | }
52 |
53 | @media screen and (min-width: 960px) {
54 | .profile-pic-container {
55 | max-width: 232px;
56 | max-height: 192px;
57 | overflow: hidden;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/user-info-input/UserInfoInput.stories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {withA11y} from '@storybook/addon-a11y';
3 | import "../../style/index.scss";
4 | import "./UserInfoInput.scss";
5 | import UserInfoInput from './UserInfoInput';
6 |
7 | export default {
8 | title: 'User Information Input Form',
9 | decorators: [withA11y],
10 | };
11 |
12 | export const UserInfoInputForm = () => ;
13 |
14 | UserInfoInputForm.story = {
15 | parameters: {
16 | notes: 'User information input form',
17 | },
18 | };
19 |
20 |
--------------------------------------------------------------------------------
/src/components/user-info-input/phone-number/PhoneNumber.tsx:
--------------------------------------------------------------------------------
1 | // =======================================================================
2 | // Known Issues/Todos
3 | // ===================
4 | // - add phone number verification -- format any way you see fit
5 | // - editing number caret placement after you set an element's value
6 | // - minimum number of characters
7 | // - convert to react hooks number needs to be formated on page load
8 | // Version 2
9 | // =======================================================================
10 |
11 | import React from 'react';
12 |
13 | const isNumericInput = (event: any) => {
14 | const key = event.keyCode;
15 | return (
16 | (key >= 48 && key <= 57) || // Allow number line
17 | (key >= 96 && key <= 105) // Allow number pad
18 | );
19 | };
20 |
21 | const isModifierKey = (event: any) => {
22 | const key = event.keyCode;
23 | return (
24 | event.shiftKey === true ||
25 | key === 35 ||
26 | key === 36 || // Allow Shift, Home, End
27 | key === 8 ||
28 | key === 9 ||
29 | key === 13 ||
30 | key === 46 || // Allow Backspace, Tab, Enter, Delete
31 | (key > 36 && key < 41) || // Allow left, up, right, down
32 | // Allow Ctrl/Command + A,C,V,X,Z
33 | ((event.ctrlKey === true || event.metaKey === true) &&
34 | (key === 65 || key === 67 || key === 86 || key === 88 || key === 90))
35 | );
36 | };
37 |
38 | const enforceFormat = (event: any) => {
39 | // Input must be of a valid number format or a modifier key, and not longer than ten digits
40 | if (!isNumericInput(event) && !isModifierKey(event)) {
41 | event.preventDefault();
42 | }
43 | };
44 |
45 | const formatToPhone = (event: any) => {
46 | if (isModifierKey(event)) {
47 | return;
48 | }
49 |
50 | const target = event.target;
51 | const input = target.value.replace(/\D/g, '').substring(0, 10); // First ten digits of input only
52 | const zip = input.substring(0, 3);
53 | const middle = input.substring(3, 6);
54 | const last = input.substring(6, 10);
55 |
56 | if (input.length > 6) {
57 | target.value = `(${zip}) ${middle} - ${last}`;
58 | } else if (input.length > 3) {
59 | target.value = `(${zip}) ${middle}`;
60 | } else if (input.length > 0) {
61 | target.value = `(${zip}`;
62 | }
63 | };
64 |
65 | const PhoneNumber = (props?: any) => {
66 | return (
67 | enforceFormat(event)}
75 | onKeyUp={(event) => formatToPhone(event)}
76 | // pattern='/^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/im'
77 | >
78 | );
79 | };
80 |
81 | export default PhoneNumber;
82 |
--------------------------------------------------------------------------------
/src/components/user-info-input/sanitize/sanitize.ts:
--------------------------------------------------------------------------------
1 | // https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
2 | const sanitize = (str: string) => {
3 | if (str === null) {
4 | return null;
5 | } else if (str) {
6 | const map: any = {
7 | '&': '&',
8 | '<': '<',
9 | '>': '>',
10 | '"': '"',
11 | "'": ''',
12 | '/': '/',
13 | '`': '`',
14 | };
15 | const reg = /[&<>"'/`]/gi;
16 | return str.replace(reg, (match) => map[match]);
17 | }
18 | return '';
19 | };
20 |
21 | // call this restore?
22 | const desanitize = (str: string) => {
23 | if (str) {
24 | const map: any = {
25 | '&': '&',
26 | '<': '<',
27 | '>': '>',
28 | '"': '"',
29 | ''': "'",
30 | '/': '/',
31 | '`': '`',
32 | };
33 | const reg = /(&|<|>|"|'|/|`)/gi;
34 | return str.replace(reg, (match) => map[match]);
35 | }
36 | return '';
37 | };
38 |
39 | export {sanitize, desanitize};
40 |
--------------------------------------------------------------------------------
/src/components/utility/Notify.ts:
--------------------------------------------------------------------------------
1 | import {Notification} from '../../../index';
2 |
3 | function notify(props: {message: string; status: string; pos: string; timeout: number}): void {
4 | const {message, status, pos, timeout} = props;
5 |
6 | const notifications = document.querySelectorAll('.uk-notification-message');
7 |
8 | if (document && UIkit && notifications && notifications.length === 0) {
9 | (UIkit as unknown as Notification).notification({
10 | message,
11 | status,
12 | pos,
13 | timeout,
14 | });
15 | }
16 | }
17 |
18 | export default notify;
19 |
--------------------------------------------------------------------------------
/src/context.ts:
--------------------------------------------------------------------------------
1 | import {createContext} from 'react';
2 | import {MyAppContext} from '../';
3 |
4 | export const SidebarOptions = {
5 | main: [
6 | {icon: "far fa-tachometer-alt-fastest", label: "Dashboard", path: "/dashboard", subItems: [
7 | {label: "Overview", path: "/dashboard"},
8 | {label: "Updates", path: "/dashboard/updates"},
9 | ]},
10 | {icon: "far fa-users-class", label: "Courses", path: "/courses", subItems: [
11 | {label: "All Courses", path: "/courses"},
12 | {label: "Add Course", path: "/courses/add"},
13 | {label: "Category", path: "/courses/categories"},
14 | {label: "Tags", path: "/courses/tags"},
15 | {label: "Manage", path: "/courses/manage"},
16 | ]},
17 | {icon: "far fa-comment-alt-edit", label: "Lessons", path: "/lessons"},
18 | {icon: "far fa-comments-alt", label: "Messages", path: "/messages"},
19 | {icon: "far fa-bookmark", label: "Bookmarks", path: "/bookmarks"},
20 | {icon: "far fa-heart", label: "Resources", path: "/resources"},
21 | {icon: "far fa-users", label: "Community", path: "/community"},
22 | ],
23 | account: [
24 | {icon: "far fa-cog", label: "Settings", path: "/settings", subItems: [
25 | {label: "Profile", path: "/settings/profile"},
26 | {label: "Preferences", path: "/settings/preferences"},
27 | {label: "Account", path: "/settings/account"},
28 | ]},
29 | {icon: "far fa-sign-out", label: "Logout", path: "/logout"},
30 | ],
31 | };
32 |
33 | export const defaultContext = {
34 | setContextProperty: undefined,
35 | user: undefined,
36 | sidebarIsOpen: true,
37 | notifications: true,
38 | isAccessFetched: false,
39 | activeDashboardPath: "dasboard",
40 | activeDashboardMenus: new Map(),
41 | access: false,
42 | redirect: undefined,
43 | isPublic: false,
44 | userID: undefined,
45 | };
46 |
47 | export const Context = createContext(defaultContext);
48 |
49 |
--------------------------------------------------------------------------------
/src/pages.ts:
--------------------------------------------------------------------------------
1 | import {Redirects} from "..";
2 |
3 | const redirects: Redirects = {
4 | "/": {redirect: undefined},
5 | "/welcome": {redirect: "/dashboard"},
6 | "/confirmation": {redirect: "/dashboard"},
7 | "/logged-out": {redirect: "/dashboard"},
8 | "/authenticate": {redirect: "/dashboard"},
9 | };
10 |
11 | const unprotected: Array = ["/", "/authenticate", "/logged-out", "/welcome", "/confirmation", "/reset-password"];
12 |
13 | export {redirects, unprotected};
14 |
--------------------------------------------------------------------------------
/src/style/_animation.scss:
--------------------------------------------------------------------------------
1 | /* Ripple Out */
2 | @-webkit-keyframes hvr-ripple-out {
3 | 100% {
4 | top: -12px;
5 | right: -12px;
6 | bottom: -12px;
7 | left: -12px;
8 | opacity: 0;
9 | }
10 | }
11 |
12 | @keyframes hvr-ripple-out {
13 | 100% {
14 | top: -12px;
15 | right: -12px;
16 | bottom: -12px;
17 | left: -12px;
18 | opacity: 0;
19 | }
20 | }
21 |
22 | .hvr-ripple-out {
23 | display: inline-block;
24 | vertical-align: middle;
25 | -webkit-transform: perspective(1px) translateZ(0);
26 | transform: perspective(1px) translateZ(0);
27 | box-shadow: 0 0 1px rgba(0, 0, 0, 0);
28 | position: relative;
29 | }
30 |
31 | .hvr-ripple-out::before {
32 | content: '';
33 | position: absolute;
34 | top: 0;
35 | right: 0;
36 | bottom: 0;
37 | left: 0;
38 | -webkit-animation-duration: 1s;
39 | animation-duration: 1s;
40 | }
41 |
42 | .hvr-ripple-out.border-red::before {
43 | border: $red solid 6px;
44 | }
45 |
46 | .hvr-ripple-out.border-yellow::before {
47 | border: $yellow solid 6px;
48 | }
49 |
50 | .hvr-ripple-out.border-blue::before {
51 | border: $blue solid 6px;
52 | }
53 |
54 | .hvr-ripple-out.border-primary::before {
55 | border: $primary solid 6px;
56 | }
57 |
58 | .hvr-ripple-out.border-secondary::before {
59 | border: $secondary solid 6px;
60 | }
61 |
62 | .hvr-ripple-out.border-tertiary::before {
63 | border: $tertiary solid 6px;
64 | }
65 |
66 | .hvr-ripple-out.border-snow::before {
67 | border: $snow solid 6px;
68 | }
69 |
70 | .hvr-ripple-out:hover::before,
71 | .hvr-ripple-out:focus::before,
72 | .hvr-ripple-out:active::before {
73 | -webkit-animation-name: hvr-ripple-out;
74 | animation-name: hvr-ripple-out;
75 | }
76 |
77 | /* Ripple In */
78 | @-webkit-keyframes hvr-ripple-in {
79 | 100% {
80 | top: 0;
81 | right: 0;
82 | bottom: 0;
83 | left: 0;
84 | opacity: 1;
85 | }
86 | }
87 |
88 | @keyframes hvr-ripple-in {
89 | 100% {
90 | top: 0;
91 | right: 0;
92 | bottom: 0;
93 | left: 0;
94 | opacity: 1;
95 | }
96 | }
97 |
98 | .hvr-ripple-in {
99 | display: inline-block;
100 | vertical-align: middle;
101 | -webkit-transform: perspective(1px) translateZ(0);
102 | transform: perspective(1px) translateZ(0);
103 | box-shadow: 0 0 1px rgba(0, 0, 0, 0);
104 | position: relative;
105 | }
106 |
107 | .hvr-ripple-in::before {
108 | content: '';
109 | position: absolute;
110 | border: #e1e1e1 solid 4px;
111 | top: -12px;
112 | right: -12px;
113 | bottom: -12px;
114 | left: -12px;
115 | opacity: 0;
116 | -webkit-animation-duration: 1s;
117 | animation-duration: 1s;
118 | }
119 |
120 | .hvr-ripple-in:hover::before,
121 | .hvr-ripple-in:focus::before,
122 | .hvr-ripple-in:active::before {
123 | -webkit-animation-name: hvr-ripple-in;
124 | animation-name: hvr-ripple-in;
125 | }
126 |
--------------------------------------------------------------------------------
/src/style/_global.scss:
--------------------------------------------------------------------------------
1 | @import './variables.scss';
2 |
3 | html {
4 | background-color: $snow;
5 | scroll-behavior: smooth;
6 | height: 100%;
7 | -webkit-font-smoothing: antialiased;
8 |
9 | ::-webkit-scrollbar {
10 | width: 15px;
11 | position: fixed;
12 | }
13 |
14 | ::-webkit-scrollbar-track-piece {
15 | background-color: #fff;
16 | }
17 |
18 | ::-webkit-scrollbar-thumb {
19 | background-color: #cbcbcb;
20 | outline: 0 solid #fff;
21 | outline-offset: -2px;
22 | border: 0.1px solid #b7b7b7;
23 | }
24 |
25 | ::-webkit-scrollbar-thumb:hover {
26 | background-color: #909090;
27 | }
28 | }
29 |
30 | * {
31 | box-sizing: border-box;
32 |
33 | &::before,
34 | &::after {
35 | box-sizing: border-box;
36 | }
37 | }
38 |
39 | body {
40 | overflow-x: hidden;
41 | height: 100%;
42 | }
43 |
44 | main {
45 | min-height: 100%;
46 | display: flex;
47 | flex-direction: column;
48 | align-items: stretch;
49 | }
50 |
51 | section {
52 | flex-grow: 1;
53 | }
54 |
55 | footer {
56 | flex-shrink: 0;
57 | }
58 |
59 | p {
60 | z-index: 1;
61 | }
62 |
63 | input {
64 | font-weight: 100 !important;
65 | }
66 |
67 | .uk-card-default {
68 | box-shadow: none;
69 | }
70 |
71 | .uk-width-1-6 {
72 | width: 6.25%;
73 | }
74 |
75 | .uk-width-1-7 {
76 | width: 14.2857%;
77 | }
78 |
79 | .uk-width-1-8 {
80 | width: 12.5%
81 | }
82 |
83 | .uk-width-1-9 {
84 | width: 11.1111%
85 | }
86 |
87 | .uk-width-1-10 {
88 | width: 10%
89 | }
90 |
91 | .grid {
92 | display: flex;
93 | flex-wrap: wrap;
94 | margin: 0;
95 | padding: 0;
96 | list-style: none;
97 | justify-content: space-between;
98 |
99 | button { padding: 0; }
100 | }
101 |
102 | .w-5 { width: 5%; }
103 | .w-10 { width: 10%; }
104 | .w-15 { width: 15%; }
105 | .w-20 { width: 20%; }
106 | .w-25 { width: 25%; }
107 | .w-30 { width: 30%; }
108 | .w-35 { width: 35%; }
109 | .w-40 { width: 40%; }
110 | .w-45 { width: 45%; }
111 | .w-50 { width: 50%; }
112 | .w-55 { width: 55%; }
113 | .w-60 { width: 60%; }
114 | .w-65 { width: 65%; }
115 | .w-70 { width: 70%; }
116 | .w-75 { width: 75%; }
117 | .w-80 { width: 80%; }
118 | .w-85 { width: 85%; }
119 | .w-90 { width: 90%; }
120 | .w-95 { width: 95%; }
121 | .w-100 { width: 100%; }
122 |
--------------------------------------------------------------------------------
/src/style/_typography.scss:
--------------------------------------------------------------------------------
1 | $custom-fonts-path : '/fonts';
2 |
3 | // Avenir
4 | @font-face {
5 | font-family: "Light";
6 | src: url($custom-fonts-path + "/AvenirLTStd-Light.otf");
7 | }
8 |
9 | @font-face {
10 | font-family: "Book";
11 | src: url($custom-fonts-path + "/AvenirLTStd-Book.otf");
12 | }
13 |
14 | @font-face {
15 | font-family: "Medium";
16 | src: url($custom-fonts-path + "/AvenirLTStd-Medium.otf");
17 | }
18 |
19 | @font-face {
20 | font-family: "Heavy";
21 | src: url($custom-fonts-path + "/AvenirLTStd-Heavy.otf");
22 | }
23 |
24 | @font-face {
25 | font-family: "Black";
26 | src: url($custom-fonts-path + "/AvenirLTStd-Black.otf");
27 | }
28 |
29 | // Big John
30 | @font-face {
31 | font-family: "Block";
32 | src: url($custom-fonts-path + "/Big-John.otf");
33 | }
34 |
35 | .heading {
36 | font-family: 'Black', sans-serif;
37 | font-size: calc(18px + 0.5vw);
38 | }
39 |
40 | .page-title {
41 | line-height: 40px;
42 | font-family: "Heavy", sans-serif;
43 | font-size: 18px;
44 | }
45 |
--------------------------------------------------------------------------------
/src/style/_variables.scss:
--------------------------------------------------------------------------------
1 | // neutral colors
2 | $black: #000;
3 | $dark-gray: #222;
4 | $light-gray: #595959;
5 | $white: #fff;
6 |
7 | // branding colors
8 | $dark-gray: #333;
9 | $light-gray: #626262;
10 | $primary: #ef906c;
11 | $secondary: #f4a77c;
12 | $tertiary: #ea8a4f;
13 | $snow: #f9fafc;
14 |
15 | // action colors
16 | $yellow: #f1af21;
17 | $red: #ec2242;
18 | $blue: #3fb3e1;
19 | $green: #02ae72;
20 |
21 | // generic dimensions
22 | $form-elements-height: 40px;
23 |
24 | // sidebar
25 | $sidebar-width: 220px;
26 | $sidebar-overlay-width: 767px;
27 | $sidebar-transition-time: 150ms;
28 |
--------------------------------------------------------------------------------
/src/style/index.scss:
--------------------------------------------------------------------------------
1 | // Variables
2 | @import '_variables';
3 |
4 | // Vendors
5 | @import
6 | '../../node_modules/uikit/src/scss/variables-theme',
7 | '../../node_modules/uikit/src/scss/mixins-theme',
8 | '../../node_modules/uikit/src/scss/uikit-theme';
9 |
10 | // Base
11 | @import
12 | './_colors',
13 | './_animation',
14 | './_global',
15 | './_typography';
16 |
17 | // Pages
18 | @import
19 | 'pages/_index',
20 | 'pages/_welcome';
21 |
--------------------------------------------------------------------------------
/src/style/pages/_dashboard.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Dashboard page style page
3 | */
4 | @import '../_variables.scss';
5 |
6 | #dashboard-container {
7 | background-color: $white;
8 | }
9 |
10 | #sidebar-container {
11 | transition: min-width 300ms ease-in-out;
12 | }
13 |
14 | #sidebar-container:has(.open) {
15 | min-width: 238px;
16 | }
17 |
18 | #sidebar-container:not(.open) {
19 | min-width: 0;
20 | }
21 |
--------------------------------------------------------------------------------
/src/style/pages/_index.scss:
--------------------------------------------------------------------------------
1 | .content {
2 | min-height: 400px;
3 | background-color: $white;
4 | }
5 |
6 | #page-index {
7 | footer {
8 | img {
9 | max-width: 125px;
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/style/pages/_welcome.scss:
--------------------------------------------------------------------------------
1 | #welcome {
2 | position: relative;
3 | top: 25vh;
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "esnext",
5 | "jsx": "preserve",
6 | "allowSyntheticDefaultImports": true,
7 | "types": [
8 | "uikit",
9 | "node",
10 | "next",
11 | "jest",
12 | "marked"
13 | ],
14 | "lib": [
15 | "esnext",
16 | "dom"
17 | ],
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "strict": true,
21 | "allowJs": true,
22 | "skipLibCheck": true,
23 | "forceConsistentCasingInFileNames": true,
24 | "noEmit": true,
25 | "esModuleInterop": true,
26 | "moduleResolution": "node",
27 | "resolveJsonModule": true,
28 | "isolatedModules": true,
29 | },
30 | "typeRoots": [
31 | "node_modules/@types",
32 | "@types",
33 | "index.d.ts"
34 | ],
35 | "exclude": [
36 | "node_modules",
37 | "typings",
38 | "**/*.spec.ts",
39 | "**/*.spec.tsx",
40 | "**/*.test.ts",
41 | "**/*.test.tsx",
42 | "src/components/dashboard/panels/addCourse/AddCourse.tsx",
43 | ],
44 | "rules": {
45 | "@typescript-eslint/member-naming": [
46 | "error",
47 | {
48 | "private": "^__",
49 | "protected": "^_"
50 | },
51 | ],
52 | },
53 | "include": [
54 | "next-env.d.ts",
55 | "index.d.ts",
56 | "**/*.ts",
57 | "**/*.tsx"
58 | ]
59 | }
60 |
--------------------------------------------------------------------------------