├── client ├── .env.example ├── src │ ├── vite-env.d.ts │ ├── features │ │ ├── categories │ │ │ ├── index.ts │ │ │ ├── apis │ │ │ │ └── category-api.ts │ │ │ └── components │ │ │ │ ├── DeleteCategoryAlert.tsx │ │ │ │ ├── AddCategorySheet.tsx │ │ │ │ ├── CategoriesList.tsx │ │ │ │ └── EditCategorySheet.tsx │ │ ├── auth │ │ │ ├── index.ts │ │ │ ├── routes │ │ │ │ ├── LoginPage.tsx │ │ │ │ └── SignupPage.tsx │ │ │ ├── apis │ │ │ │ └── auth-api.ts │ │ │ └── components │ │ │ │ └── LoginCard.tsx │ │ ├── users │ │ │ ├── index.ts │ │ │ ├── routes │ │ │ │ ├── TasksPage.tsx │ │ │ │ └── SettingsPage.tsx │ │ │ └── apis │ │ │ │ └── user-api.ts │ │ ├── comments │ │ │ ├── index.ts │ │ │ ├── apis │ │ │ │ └── comment-api.ts │ │ │ └── components │ │ │ │ ├── DeleteCommentAlert.tsx │ │ │ │ ├── CommentCard.tsx │ │ │ │ ├── CommentSheet.tsx │ │ │ │ └── EditCommentSheet.tsx │ │ ├── projects │ │ │ ├── index.ts │ │ │ ├── routes │ │ │ │ ├── AllProjectsPage.tsx │ │ │ │ ├── ProjectPage.tsx │ │ │ │ └── DashboardPage.tsx │ │ │ ├── components │ │ │ │ ├── ProjectChart.tsx │ │ │ │ ├── ProjectCard.tsx │ │ │ │ ├── ChartBoard.tsx │ │ │ │ ├── DeleteProjectAlert.tsx │ │ │ │ ├── RemoveMemberAlert.tsx │ │ │ │ ├── MembersList.tsx │ │ │ │ └── AddMemberSheet.tsx │ │ │ └── apis │ │ │ │ └── project-api.ts │ │ └── issues │ │ │ ├── index.ts │ │ │ ├── components │ │ │ ├── TablePagination.tsx │ │ │ ├── DeleteIssueAlert.tsx │ │ │ └── IssueActionsDropdown.tsx │ │ │ └── apis │ │ │ └── issue-api.ts │ ├── assets │ │ ├── logo-dark.png │ │ └── logo-light.png │ ├── App.tsx │ ├── main.tsx │ ├── lib │ │ ├── react-query.ts │ │ └── axios.ts │ ├── components │ │ ├── ui │ │ │ ├── collapsible.tsx │ │ │ ├── label.tsx │ │ │ ├── textarea.tsx │ │ │ ├── input.tsx │ │ │ ├── switch.tsx │ │ │ ├── spinner.tsx │ │ │ ├── avatar.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── alert.tsx │ │ │ ├── badge.tsx │ │ │ ├── tabs.tsx │ │ │ ├── card.tsx │ │ │ ├── button.tsx │ │ │ └── table.tsx │ │ ├── UserAvatar.tsx │ │ ├── layout │ │ │ ├── RootLayout.tsx │ │ │ ├── ButtomMenu.tsx │ │ │ └── SideMenu.tsx │ │ ├── AlertMassage.tsx │ │ ├── ModeToggle.tsx │ │ ├── ProtectedRoutes.tsx │ │ └── Authorization.tsx │ ├── utils │ │ └── index.ts │ ├── providers │ │ ├── index.tsx │ │ └── theme-provider.tsx │ ├── routes │ │ └── index.tsx │ └── index.css ├── public │ ├── ERD.png │ ├── logo-light.png │ ├── favicon-dark.png │ ├── favicon-light.png │ └── screenshots │ │ ├── postman.png │ │ ├── issuezy-demo1.gif │ │ ├── issuezy-demo2.gif │ │ ├── issuezy-demo3.gif │ │ ├── issuezy-demo4.gif │ │ ├── issuezy-demo5.gif │ │ ├── issuezy_mobile.png │ │ └── issuezy_desktop.png ├── netlify.toml ├── postcss.config.js ├── prettier.config.cjs ├── vite.config.ts ├── tsconfig.node.json ├── components.json ├── .gitignore ├── .eslintrc.cjs ├── index.html ├── tsconfig.json ├── package.json └── tailwind.config.js └── server ├── .env.example ├── helpers └── error-helper.js ├── .sequelizerc ├── .gitignore ├── migrations ├── 20230909171510-rename-user-name-to-firstname.js ├── 20230909171816-add-lastname-to-users-table.js ├── 20230914113812-change-issue-priority-to-string.js ├── 20230829091856-create-user.js ├── 20230830114812-create-membership.js ├── 20230831085418-create-category.js ├── 20230902195109-create-comment.js ├── 20230829142038-create-project.js └── 20230901130919-create-issue.js ├── middlewares ├── auth.js └── error-handler.js ├── .eslintrc.json ├── config ├── cors.js ├── passport.js └── index.js ├── seeders ├── 20230928135226-categories-seed-file.js ├── 20230928132342-projects-seed-file.js ├── 20230928134227-memberships-seed-file.js ├── 20230928131712-users-seed-file.js ├── 20230928135909-issues-seed-file.js └── 20230928140236-comments-seed-file.js ├── models ├── membership.js ├── category.js ├── comment.js ├── project.js ├── index.js ├── user.js └── issue.js ├── app.js ├── controllers ├── comment-controller.js ├── category-controller.js ├── user-controller.js ├── issue-controller.js └── project-controller.js ├── package.json ├── routes └── index.js └── services ├── comment-service.js ├── user-service.js └── category-service.js /client/.env.example: -------------------------------------------------------------------------------- 1 | VITE_API_ENDPOINT="http://localhost:3000/api" -------------------------------------------------------------------------------- /client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /client/src/features/categories/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/CategoriesList.tsx"; 2 | -------------------------------------------------------------------------------- /client/public/ERD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KellyCHI22/Issuezy/HEAD/client/public/ERD.png -------------------------------------------------------------------------------- /client/netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/*" 3 | to = "/index.html" 4 | status = 200 5 | -------------------------------------------------------------------------------- /client/public/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KellyCHI22/Issuezy/HEAD/client/public/logo-light.png -------------------------------------------------------------------------------- /client/public/favicon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KellyCHI22/Issuezy/HEAD/client/public/favicon-dark.png -------------------------------------------------------------------------------- /client/public/favicon-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KellyCHI22/Issuezy/HEAD/client/public/favicon-light.png -------------------------------------------------------------------------------- /client/src/assets/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KellyCHI22/Issuezy/HEAD/client/src/assets/logo-dark.png -------------------------------------------------------------------------------- /client/src/assets/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KellyCHI22/Issuezy/HEAD/client/src/assets/logo-light.png -------------------------------------------------------------------------------- /client/src/features/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./routes/LoginPage.tsx"; 2 | export * from "./routes/SignupPage.tsx"; 3 | -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /client/public/screenshots/postman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KellyCHI22/Issuezy/HEAD/client/public/screenshots/postman.png -------------------------------------------------------------------------------- /client/src/features/users/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./routes/SettingsPage.tsx"; 2 | export * from "./routes/TasksPage.tsx"; 3 | -------------------------------------------------------------------------------- /client/src/features/comments/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/CommentCard.tsx"; 2 | export * from "./components/CommentSheet.tsx"; 3 | -------------------------------------------------------------------------------- /client/public/screenshots/issuezy-demo1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KellyCHI22/Issuezy/HEAD/client/public/screenshots/issuezy-demo1.gif -------------------------------------------------------------------------------- /client/public/screenshots/issuezy-demo2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KellyCHI22/Issuezy/HEAD/client/public/screenshots/issuezy-demo2.gif -------------------------------------------------------------------------------- /client/public/screenshots/issuezy-demo3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KellyCHI22/Issuezy/HEAD/client/public/screenshots/issuezy-demo3.gif -------------------------------------------------------------------------------- /client/public/screenshots/issuezy-demo4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KellyCHI22/Issuezy/HEAD/client/public/screenshots/issuezy-demo4.gif -------------------------------------------------------------------------------- /client/public/screenshots/issuezy-demo5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KellyCHI22/Issuezy/HEAD/client/public/screenshots/issuezy-demo5.gif -------------------------------------------------------------------------------- /client/public/screenshots/issuezy_mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KellyCHI22/Issuezy/HEAD/client/public/screenshots/issuezy_mobile.png -------------------------------------------------------------------------------- /client/public/screenshots/issuezy_desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KellyCHI22/Issuezy/HEAD/client/public/screenshots/issuezy_desktop.png -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import AppProviders from "./providers"; 2 | 3 | function App() { 4 | return ; 5 | } 6 | 7 | export default App; 8 | -------------------------------------------------------------------------------- /client/prettier.config.cjs: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | module.exports = { 3 | plugins: ['prettier-plugin-tailwindcss'], 4 | tailwindConfig: './tailwind.config.js', 5 | }; 6 | -------------------------------------------------------------------------------- /client/src/features/projects/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./routes/AllProjectsPage.tsx"; 2 | export * from "./routes/DashboardPage.tsx"; 3 | export * from "./routes/ProjectPage.tsx"; 4 | export * from "./routes/IssuePage.tsx"; 5 | -------------------------------------------------------------------------------- /server/.env.example: -------------------------------------------------------------------------------- 1 | PORT=MY_SERVER_PORT 2 | JWT_SECRET=MY_SECRET 3 | SEED_PASSWORD=MY_SEED_PASSWORD 4 | DB_USERNAME=MY_DB_USERNAME 5 | DB_PASSWORD=MY_DB_PASSWORD 6 | DB_NAME=MY_DB_NAME 7 | DB_HOST=MY_DB_HOST 8 | DB_PORT=MY_DB_PORT -------------------------------------------------------------------------------- /server/helpers/error-helper.js: -------------------------------------------------------------------------------- 1 | const customError = (statusCode, message) => { 2 | const error = new Error(message); 3 | error.status = statusCode; 4 | return error; 5 | }; 6 | 7 | module.exports = { 8 | customError, 9 | }; 10 | -------------------------------------------------------------------------------- /server/.sequelizerc: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | config: path.resolve('config', 'index.js'), 5 | 'migrations-path': path.resolve('migrations'), 6 | 'models-path': path.resolve('models'), 7 | 'seeders-path': path.resolve('seeders'), 8 | }; 9 | -------------------------------------------------------------------------------- /client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /client/src/lib/react-query.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from "@tanstack/react-query"; 2 | 3 | export const queryClient = new QueryClient({ 4 | defaultOptions: { 5 | queries: { 6 | staleTime: 10 * (60 * 1000), // 10 mins 7 | cacheTime: 15 * (60 * 1000), // 15 mins 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /client/src/features/issues/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components/AssignIssueSheet.tsx"; 2 | export * from "./components/DeleteIssueAlert.tsx"; 3 | export * from "./components/EditIssueSheet.tsx"; 4 | export * from "./components/IssueSheet.tsx"; 5 | export * from "./components/IssuesTable.tsx"; 6 | export * from "./components/issueColumns.tsx"; 7 | -------------------------------------------------------------------------------- /client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | import react from '@vitejs/plugin-react'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | resolve: { 9 | alias: { 10 | '@': path.resolve(__dirname, './src'), 11 | }, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | # logs 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | .env -------------------------------------------------------------------------------- /client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | } 12 | }, 13 | "include": ["vite.config.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /client/src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 2 | 3 | const Collapsible = CollapsiblePrimitive.Root 4 | 5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 6 | 7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 8 | 9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 10 | -------------------------------------------------------------------------------- /client/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .env 15 | .env.development 16 | .env.production 17 | 18 | # Editor directories and files 19 | .vscode/* 20 | !.vscode/extensions.json 21 | .idea 22 | .DS_Store 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | -------------------------------------------------------------------------------- /server/migrations/20230909171510-rename-user-name-to-firstname.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up(queryInterface, Sequelize) { 6 | return queryInterface.renameColumn('Users', 'name', 'firstname'); 7 | }, 8 | 9 | async down(queryInterface, Sequelize) { 10 | return queryInterface.renameColumn('Users', 'firstname', 'name'); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /server/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | const passport = require('../config/passport'); 2 | 3 | const authenticated = (req, res, next) => { 4 | passport.authenticate('jwt', { session: false }, (err, user) => { 5 | if (err || !user) { 6 | return res.status(401).json({ status: 'error', message: 'unauthorized' }); 7 | } 8 | 9 | next(); 10 | })(req, res, next); 11 | }; 12 | 13 | module.exports = { 14 | authenticated, 15 | }; 16 | -------------------------------------------------------------------------------- /server/middlewares/error-handler.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apiErrorHandler(err, req, res, next) { 3 | if (err instanceof Error) { 4 | res.status(err.status || 500).json({ 5 | status: 'error', 6 | message: `${err.message}`, 7 | }); 8 | } else { 9 | res.status(500).json({ 10 | status: 'error', 11 | message: 'Internal server error', 12 | }); 13 | } 14 | next(err); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "commonjs": true, 6 | "es2021": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "ecmaVersion": "latest" 11 | }, 12 | "rules": { 13 | "indent": ["error", 2], 14 | "linebreak-style": [0], 15 | "quotes": ["error", "single"], 16 | "semi": ["error", "always"], 17 | "no-unused-vars": "off" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server/migrations/20230909171816-add-lastname-to-users-table.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up(queryInterface, Sequelize) { 6 | return queryInterface.addColumn('Users', 'lastname', { 7 | type: Sequelize.STRING, 8 | allowNull: false, 9 | }); 10 | }, 11 | 12 | async down(queryInterface, Sequelize) { 13 | return queryInterface.removeColumn('Users', 'lastname'); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /client/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export function formatTime(isoString: string) { 9 | const date = new Date(isoString); 10 | const americanDate = new Intl.DateTimeFormat("en-US", { 11 | day: "numeric", 12 | month: "short", 13 | year: "numeric", 14 | }).format(date); 15 | return americanDate; 16 | } 17 | -------------------------------------------------------------------------------- /client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /server/config/cors.js: -------------------------------------------------------------------------------- 1 | const whitelist = [ 2 | 'http://localhost:5173', 3 | 'http://localhost:4173', 4 | 'https://issuezy.netlify.app', 5 | ]; 6 | 7 | const corsOptionsDelegate = (req, callback) => { 8 | let corsOptions = {}; 9 | if (whitelist.indexOf(req.header('Origin')) !== -1) { 10 | corsOptions = { 11 | origin: true, 12 | exposedHeaders: ['authorization'], 13 | }; 14 | } else { 15 | corsOptions = { origin: false }; 16 | } 17 | callback(null, corsOptions); 18 | }; 19 | 20 | module.exports = { corsOptionsDelegate }; 21 | -------------------------------------------------------------------------------- /client/src/features/users/routes/TasksPage.tsx: -------------------------------------------------------------------------------- 1 | export function TasksPage() { 2 | return ( 3 |
4 |
5 |
6 |
7 |

My tasks

8 |

Work in progress 🛠️

9 |
10 |
11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /server/migrations/20230914113812-change-issue-priority-to-string.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up(queryInterface, Sequelize) { 6 | return queryInterface.changeColumn('Issues', 'priority', { 7 | type: Sequelize.STRING, 8 | defaultValue: '1', 9 | allowNull: false, 10 | }); 11 | }, 12 | async down(queryInterface, Sequelize) { 13 | return queryInterface.changeColumn('Issues', 'priority', { 14 | type: Sequelize.INTEGER, 15 | defaultValue: 1, 16 | allowNull: false, 17 | }); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /client/src/lib/axios.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const baseURL = import.meta.env.VITE_API_ENDPOINT; 4 | 5 | const axiosInstance = axios.create({ 6 | baseURL, 7 | }); 8 | 9 | export interface ErrorResponseData { 10 | status: "error"; 11 | message: string; 12 | } 13 | 14 | axiosInstance.interceptors.request.use( 15 | (config) => { 16 | const token = localStorage.getItem("token"); 17 | if (token) { 18 | config.headers.Authorization = `Bearer ${token}`; 19 | } 20 | return config; 21 | }, 22 | (error) => { 23 | console.error(error); 24 | }, 25 | ); 26 | 27 | export { baseURL, axiosInstance }; 28 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | issuezy 20 | 21 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /client/src/components/UserAvatar.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarFallback } from "@/components/ui/avatar"; 2 | import { cn } from "@/utils"; 3 | 4 | interface UserAvatarProps extends React.HTMLAttributes { 5 | user: { 6 | id: number; 7 | firstname: string; 8 | lastname: string; 9 | }; 10 | } 11 | 12 | export default function UserAvatar({ user, className }: UserAvatarProps) { 13 | return ( 14 | 15 | 16 | {user?.firstname[0]} 17 | {user?.lastname[0]} 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /client/src/features/auth/routes/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { LoginCard } from "../components/LoginCard"; 3 | import { useNavigate } from "react-router-dom"; 4 | 5 | export const LoginPage = () => { 6 | const navigate = useNavigate(); 7 | useEffect(() => { 8 | const token = localStorage.getItem("token"); 9 | if (token) return navigate("/projects"); 10 | }, []); 11 | 12 | return ( 13 |
14 | 15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /client/src/features/auth/routes/SignupPage.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "react-router-dom"; 2 | import { SignupCard } from "../components/SignupCard"; 3 | import { useEffect } from "react"; 4 | 5 | export const SignupPage = () => { 6 | const navigate = useNavigate(); 7 | useEffect(() => { 8 | const token = localStorage.getItem("token"); 9 | if (token) return navigate("/projects"); 10 | }, []); 11 | 12 | return ( 13 |
14 | 15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /client/src/features/users/apis/user-api.ts: -------------------------------------------------------------------------------- 1 | import { baseURL, axiosInstance } from "@/lib/axios"; 2 | 3 | export type CurrentUser = { 4 | id: number; 5 | firstname: string; 6 | lastname: string; 7 | email: string; 8 | createdAt: string; 9 | }; 10 | 11 | export async function getCurrentUser() { 12 | const res = await axiosInstance.get(`${baseURL}/users/current`); 13 | return res.data.data; 14 | } 15 | 16 | export async function patchUser(payload: { 17 | userId: string; 18 | formData: { 19 | firstname: string; 20 | lastname: string; 21 | }; 22 | }) { 23 | const { userId, formData } = payload; 24 | const res = await axiosInstance.patch(`${baseURL}/users/${userId}`, formData); 25 | return res.data.data; 26 | } 27 | -------------------------------------------------------------------------------- /client/src/components/layout/RootLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router-dom"; 2 | import SideMenu from "./SideMenu"; 3 | import ButtomMenu from "./ButtomMenu"; 4 | 5 | export default function RootLayout() { 6 | return ( 7 |
8 | 9 | 10 |
11 | 12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /client/src/components/AlertMassage.tsx: -------------------------------------------------------------------------------- 1 | import { Info, Ban } from "lucide-react"; 2 | import { Alert, AlertDescription } from "@/components/ui/alert"; 3 | 4 | interface AlertMessageProps extends React.HTMLAttributes { 5 | variant: "default" | "destructive"; 6 | title?: string; 7 | message: string; 8 | } 9 | 10 | export function AlertMessage({ 11 | variant, 12 | message, 13 | className, 14 | }: AlertMessageProps) { 15 | return ( 16 | 17 | {variant === "default" ? ( 18 | 19 | ) : ( 20 | 21 | )} 22 | {message} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /server/seeders/20230928135226-categories-seed-file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | async up(queryInterface, Sequelize) { 6 | await queryInterface.bulkInsert( 7 | 'Categories', 8 | ['bug', 'feature request', 'improvement', 'task', 'other'].map((item) => { 9 | return { 10 | name: item, 11 | project_id: null, 12 | is_default: true, 13 | is_deleted: false, 14 | created_at: new Date(), 15 | updated_at: new Date(), 16 | }; 17 | }), 18 | {} 19 | ); 20 | }, 21 | 22 | async down(queryInterface, Sequelize) { 23 | await queryInterface.bulkDelete('Categories', {}); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /server/config/passport.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const passportJWT = require('passport-jwt'); 3 | const { User } = require('../models'); 4 | 5 | const JWTStrategy = passportJWT.Strategy; 6 | const ExtractJWT = passportJWT.ExtractJwt; 7 | 8 | // setup passport-jwt 9 | const jwtOptions = { 10 | jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(), 11 | secretOrKey: process.env.JWT_SECRET, 12 | passReqToCallback: true, 13 | }; 14 | 15 | passport.use( 16 | new JWTStrategy(jwtOptions, (req, jwtPayload, cb) => { 17 | User.findByPk(jwtPayload.id) 18 | .then((user) => { 19 | req.user = user; 20 | cb(null, user); 21 | }) 22 | .catch((err) => cb(err)); 23 | }) 24 | ); 25 | 26 | module.exports = passport; 27 | -------------------------------------------------------------------------------- /client/src/providers/index.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from "@/providers/theme-provider"; 2 | import { QueryClientProvider } from "@tanstack/react-query"; 3 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 4 | import { RouterProvider } from "react-router-dom"; 5 | import { queryClient } from "@/lib/react-query"; 6 | import router from "@/routes"; 7 | 8 | export default function AppProviders() { 9 | return ( 10 | <> 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /server/models/membership.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { Model } = require('sequelize'); 3 | module.exports = (sequelize, DataTypes) => { 4 | class Membership extends Model { 5 | /** 6 | * Helper method for defining associations. 7 | * This method is not a part of Sequelize lifecycle. 8 | * The `models/index` file will call this method automatically. 9 | */ 10 | static associate(models) { 11 | // define association here 12 | } 13 | } 14 | Membership.init( 15 | { 16 | userId: DataTypes.INTEGER, 17 | projectId: DataTypes.INTEGER, 18 | }, 19 | { 20 | sequelize, 21 | modelName: 'Membership', 22 | tableName: 'Memberships', 23 | underscored: true, 24 | } 25 | ); 26 | return Membership; 27 | }; 28 | -------------------------------------------------------------------------------- /client/src/components/ModeToggle.tsx: -------------------------------------------------------------------------------- 1 | import { Moon, Sun } from "lucide-react"; 2 | import { Button } from "@/components/ui/button"; 3 | import { useTheme } from "@/providers/theme-provider"; 4 | 5 | export function ModeToggle() { 6 | const { theme, setTheme } = useTheme(); 7 | const toggleTheme = () => setTheme(theme === "dark" ? "light" : "dark"); 8 | 9 | return ( 10 | <> 11 | 15 | Toggle theme 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | if (process.env.NODE_ENV !== 'production') { 4 | require('dotenv').config({ path: path.resolve(__dirname, './.env') }); 5 | } 6 | 7 | const express = require('express'); 8 | const cors = require('cors'); 9 | const routes = require('./routes'); 10 | const db = require('./models'); 11 | const passport = require('./config/passport'); 12 | const { corsOptionsDelegate } = require('./config/cors'); 13 | 14 | const app = express(); 15 | const port = process.env.PORT || 3000; 16 | 17 | app.use(express.json()); 18 | app.use(passport.initialize()); 19 | app.use(cors(corsOptionsDelegate)); 20 | 21 | app.use('/api', routes); 22 | 23 | app.listen(port, () => { 24 | console.info(`App listening on http://localhost:${port}`); 25 | }); 26 | 27 | module.exports = app; 28 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | "baseUrl": ".", 24 | "paths": { 25 | "@/*": ["./src/*"] 26 | } 27 | }, 28 | "include": ["src"], 29 | "references": [{ "path": "./tsconfig.node.json" }] 30 | } 31 | -------------------------------------------------------------------------------- /client/src/components/ProtectedRoutes.tsx: -------------------------------------------------------------------------------- 1 | import { checkPermission } from "@/features/auth/apis/auth-api"; 2 | import { useMutation } from "@tanstack/react-query"; 3 | import { useEffect } from "react"; 4 | import { Outlet, useNavigate } from "react-router-dom"; 5 | 6 | export default function ProtectedRoutes() { 7 | const navigate = useNavigate(); 8 | const permissionMutation = useMutation({ 9 | mutationFn: checkPermission, 10 | onSuccess: () => navigate("/projects"), 11 | onError: () => { 12 | return navigate("/login"); 13 | }, 14 | }); 15 | 16 | useEffect(() => { 17 | const token = localStorage.getItem("token"); 18 | if (!token) return navigate("/login"); 19 | permissionMutation.mutate({ token }); 20 | }, []); 21 | 22 | return permissionMutation.isSuccess ? : null; 23 | } 24 | -------------------------------------------------------------------------------- /client/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/utils"; 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 9 | ); 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )); 22 | Label.displayName = LabelPrimitive.Root.displayName; 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /client/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/utils"; 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |