├── .env.example ├── .github ├── actions │ ├── download-artifact │ │ └── action.yml │ └── upload-artifact │ │ └── action.yml ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .husky └── pre-commit ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── event-scheduler.iml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── prettier.xml └── vcs.xml ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── api ├── graphql │ ├── resolvers │ │ ├── auth.ts │ │ ├── events.ts │ │ ├── index.ts │ │ └── users.ts │ └── schema │ │ └── index.ts ├── index.ts ├── interfaces │ └── types.ts ├── middleware │ └── auth.ts ├── models │ ├── event.ts │ └── user.ts ├── tsconfig.json └── utils │ └── validations.ts ├── codegen.yml ├── eslint.config.mjs ├── graphql.schema.json ├── images └── react-event-pic.gif ├── index.d.ts ├── index.html ├── jest.config.js ├── jest.setup.ts ├── package.json ├── public ├── favicon.ico ├── logo192.png └── logo512.png ├── renovate.json ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── Routes.tsx ├── apolloClient.tsx ├── assets │ ├── images │ │ └── mtb.jpg │ └── logos │ │ └── logo.svg ├── components │ ├── EventBody │ │ └── EventBody.tsx │ ├── Footer │ │ └── Footer.tsx │ ├── Login │ │ └── Login.tsx │ ├── Navbar │ │ └── Navbar.tsx │ ├── PageNotFound │ │ └── PageNotFound.tsx │ ├── Pagination │ │ └── Pagination.tsx │ ├── ServerErrorAlert │ │ └── ServerErrorAlert.tsx │ ├── Signup │ │ └── Signup.tsx │ ├── Timer │ │ └── Timer.tsx │ ├── UserIdleTimer │ │ └── UserIdleTimer.tsx │ └── ui │ │ ├── Alert │ │ └── Alert.tsx │ │ ├── BtnSpinner │ │ └── BtnSpinner.tsx │ │ ├── Card │ │ └── Card.tsx │ │ ├── CardView │ │ └── CardView.tsx │ │ ├── Modal │ │ └── Modal.tsx │ │ ├── Spinner │ │ └── Spinner.tsx │ │ └── TitledCard │ │ └── TitledCard.tsx ├── generated │ └── graphql.tsx ├── graphql │ ├── fragments.graphql │ ├── mutations.graphql │ └── queries.graphql ├── hooks │ ├── useAuth.tsx │ ├── useDebounce.tsx │ ├── useNavigateToHome.tsx │ ├── useSearchBox.tsx │ └── useValidation.tsx ├── index.css ├── main.tsx ├── pages │ ├── calendar │ │ ├── Calendar.test.tsx │ │ └── Calendar.tsx │ ├── events │ │ ├── AddEvent │ │ │ ├── AddEvent.test.tsx │ │ │ └── AddEvent.tsx │ │ ├── SearchEvents │ │ │ └── SearchEvents.tsx │ │ └── ShareEvent │ │ │ └── SharedEvent.tsx │ ├── home │ │ └── Welcome.tsx │ └── user │ │ ├── LoginContainer │ │ └── LoginContainer.tsx │ │ └── MyAccount │ │ ├── MyAccount.tsx │ │ ├── MyEvents │ │ └── MyEvents.tsx │ │ ├── MyProfile │ │ ├── EditMyProfile.tsx │ │ └── MyProfile.tsx │ │ ├── MySettings │ │ └── MySettings.tsx │ │ └── styles.css ├── store │ ├── AuthProvider.tsx │ └── auth-context.tsx ├── types │ └── index.ts ├── utils │ ├── apolloCache.ts │ └── dateTransforms.ts └── vite-env.d.ts ├── tsconfig.jest.json ├── tsconfig.json ├── tsconfig.node.json ├── vercel.json ├── vite.config.mts └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | VITE_APP_GRAPHQL_ENDPOINT=http://localhost:4000/graphql 2 | URI=http://localhost:3000 3 | PORT=4000 4 | MONGODB_URI=mongodb://127.0.0.1:27017/EventScheduler 5 | JWT_SECRET=your_jwt_secret -------------------------------------------------------------------------------- /.github/actions/download-artifact/action.yml: -------------------------------------------------------------------------------- 1 | name: Download artifact 2 | description: Wrapper around GitHub's official action, with additional extraction before download 3 | 4 | # https://github.com/actions/download-artifact/blob/main/action.yml 5 | inputs: 6 | name: 7 | description: Artifact name 8 | required: true 9 | path: 10 | description: Destination path 11 | required: false 12 | default: . 13 | 14 | runs: 15 | using: composite 16 | steps: 17 | - name: Download artifacts 18 | uses: actions/download-artifact@v4 19 | with: 20 | name: ${{ inputs.name }} 21 | path: ${{ inputs.path }} 22 | 23 | - name: Extract artifacts 24 | run: tar -xvf ${{ inputs.name }}.tar 25 | shell: bash 26 | working-directory: ${{ inputs.path }} 27 | 28 | - name: Remove archive 29 | run: rm -f ${{ inputs.name }}.tar 30 | shell: bash 31 | working-directory: ${{ inputs.path }} 32 | -------------------------------------------------------------------------------- /.github/actions/upload-artifact/action.yml: -------------------------------------------------------------------------------- 1 | name: Upload artifact 2 | description: Wrapper around GitHub's official action, with additional archiving before upload 3 | 4 | # https://github.com/actions/upload-artifact/blob/main/action.yml 5 | inputs: 6 | name: 7 | description: Artifact name 8 | required: true 9 | path: 10 | description: One or more files, directories or wildcard pattern that describes what to upload 11 | required: true 12 | if-no-files-found: 13 | description: > 14 | The desired behavior if no files are found using the provided path. 15 | Available Options: 16 | warn: Output a warning but do not fail the action 17 | error: Fail the action with an error message 18 | ignore: Do not output any warnings or errors, the action does not fail 19 | required: false 20 | default: warn 21 | retention-days: 22 | description: > 23 | Duration after which artifact will expire in days. 0 means using default retention. 24 | Minimum 1 day. 25 | Maximum 90 days unless changed from the repository settings page. 26 | required: false 27 | default: '0' 28 | 29 | runs: 30 | using: composite 31 | steps: 32 | - name: Archive artifacts 33 | run: tar -cvf ${{ inputs.name }}.tar $(echo "${{ inputs.path }}" | tr '\n' ' ') 34 | shell: bash 35 | 36 | - name: Upload artifacts 37 | uses: actions/upload-artifact@v4 38 | with: 39 | if-no-files-found: ${{ inputs.if-no-files-found }} 40 | name: ${{ inputs.name }} 41 | path: ${{ inputs.name }}.tar 42 | retention-days: ${{ inputs.retention-days }} 43 | 44 | - name: Remove archive 45 | run: rm -f ${{ inputs.name }}.tar 46 | shell: bash 47 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: npm # See documentation for possible values 9 | directory: '/' # Location of package manifests 10 | schedule: 11 | interval: daily 12 | time: '21:00' 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | setup: 11 | name: Setup 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | 18 | - name: Use Node 18 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: '18' 22 | 23 | - name: Install project 24 | run: yarn install 25 | 26 | - name: Build project 27 | run: yarn run build 28 | 29 | - name: Delete build artifact 30 | uses: geekyeggo/delete-artifact@v4 31 | with: 32 | name: build-artifact 33 | 34 | - name: Upload build artifact 35 | uses: ./.github/actions/upload-artifact 36 | with: 37 | name: build-artifact 38 | path: | 39 | dist 40 | node_modules 41 | package.json 42 | yarn.lock 43 | 44 | lint: 45 | name: Lint 46 | runs-on: ubuntu-latest 47 | needs: setup 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v2 51 | 52 | - name: Download build artifact 53 | uses: ./.github/actions/download-artifact 54 | with: 55 | name: build-artifact 56 | 57 | - name: Run lint 58 | run: yarn lint 59 | 60 | compile: 61 | name: Compile 62 | runs-on: ubuntu-latest 63 | needs: setup 64 | steps: 65 | - name: Checkout 66 | uses: actions/checkout@v2 67 | 68 | - name: Download build artifact 69 | uses: ./.github/actions/download-artifact 70 | with: 71 | name: build-artifact 72 | 73 | - name: Run tsc 74 | uses: icrawl/action-tsc@v1 75 | 76 | test: 77 | name: Test 78 | runs-on: ubuntu-latest 79 | needs: setup 80 | steps: 81 | - name: Checkout 82 | uses: actions/checkout@v2 83 | 84 | - name: Download build artifact 85 | uses: ./.github/actions/download-artifact 86 | with: 87 | name: build-artifact 88 | 89 | - name: Run tests 90 | run: yarn test 91 | 92 | coverage: 93 | name: Coverage 94 | runs-on: ubuntu-latest 95 | needs: setup 96 | steps: 97 | - name: Checkout 98 | uses: actions/checkout@v2 99 | 100 | - name: Download build artifact 101 | uses: ./.github/actions/download-artifact 102 | with: 103 | name: build-artifact 104 | 105 | - name: Run tests coverage 106 | run: yarn test:coverage 107 | 108 | - name: Upload coverage reports to Codecov 109 | uses: codecov/codecov-action@v3 110 | env: 111 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /dist 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 17 | 18 | 26 | 27 | 30 | 31 | 38 | 39 | 46 | 47 | 54 | 55 | 60 | 61 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/event-scheduler.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /dist 11 | 12 | generated/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "tabWidth": 2, 4 | "printWidth": 80, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "jsxSingleQuote": true, 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ahmed 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 | # React Event Scheduler App 2 | 3 | ![](https://github.com/AhmedAlatawi/react-event-scheduler/actions/workflows/main.yml/badge.svg) 4 | [![codecov](https://codecov.io/gh/AhmedAlatawi/react-event-scheduler/graph/badge.svg?token=EG9GTUBOUE)](https://codecov.io/gh/AhmedAlatawi/react-event-scheduler) 5 | [![License: MIT](https://img.shields.io/github/license/AhmedAlatawi/react-event-scheduler)](https://github.com/AhmedAlatawi/react-event-scheduler/blob/master/LICENSE) 6 | 7 | This project was bootstrapped with [Vite](https://vite.dev/). 8 | 9 | ![](./images/react-event-pic.gif) 10 | 11 | Event Scheduler is a React app that allows users to create events. An event can be anything, such as a sport event, team meeting, party announcement, personal advertisement, etc. An event consists of title, start and end date/time, and description. Events can also be shared on FB or Twitter. 12 | All events are public by default (visible to everyone). They can also be private (only visible to you) by checking the private checkbox. 13 | 14 | > If you're looking for a frontend or backend starter project, check these out: 15 | > 16 | > - **[Frontend starter project](https://github.com/ahmedalatawi/react-graphql-starter)** built with React, GraphQL (Apollo client) and Typescript. 17 | > - **[Backend starter project](https://github.com/ahmedalatawi/nodejs-graphql-fake-api)** built with NodeJS, GraphQL (Apollo server), TypeScript, MongoDB and Prisma. 18 | 19 | ### [Demo](https://react-event-scheduler.vercel.app/) :movie_camera: 20 | 21 | ## Tech Stack 22 | 23 | ### Frontend 24 | 25 | - React (react hooks) 26 | - Typescript 27 | - Bootstrap/react-bootstrap 28 | - Styled components 29 | - Apollo client 30 | - JS cookie 31 | 32 | ### Backend 33 | 34 | - NodeJS with Express 35 | - Typescript 36 | - Apollo server express 37 | - JSON web token 38 | - MongoDB with mongoose 39 | 40 | # 41 | 42 | Note that `graphql` schemas are generated on the frontend using [GraphQL Code Generator](https://www.graphql-code-generator.com/docs/getting-started). This means that if you make any changes to the schema (server/graphql/schema/index.ts), make sure that the `.graphql` files in the frontend are also updated accordingly. 43 | 44 | Next, run `yarn codegen` to re-generate the queries and mutations (before you do this, make sure the server is up and running by either running `yarn start` or `yarn start:server`) 45 | 46 | ## Run app locally 47 | 48 | > Make sure MongoDB is up and running 49 | 50 | Create a `.env` file in the project's root directory, and copy & paste what's in `.env.example`, then run `yarn`: 51 | 52 | ### `yarn start` 53 | 54 | Runs the backend and frontend apps simultaneously in the development mode. 55 | 56 | > Or if you prefer running the apps separately by running `yarn start:web` and `yarn start:server` in separate terminals. 57 | 58 | The app will automatically start at [http://localhost:3000](http://localhost:3000) in the browser. 59 | 60 | You will also see any Lint or Typescript errors in the console. 61 | 62 | ## Current functionality 63 | 64 | - User signup and login 65 | - Create, update and delete events 66 | - Search & pagination 67 | - Make events as private (only visible to creators) 68 | - Session expiry warning (displayed when being idle for 3 minutes after logging in) 69 | - Share events with family & friends on Facebook and Twitter 70 | 71 | ### Coming soon 72 | 73 | - User profile 74 | - Admin tab & profile 75 | 76 | ## Run unit tests 77 | 78 | coming soon... 79 | 80 | ## Run E2E tests 81 | 82 | coming soon... 83 | 84 | ### Author :books: 85 | 86 | [Ahmed Alatawi](https://github.com/AhmedAlatawi) 87 | -------------------------------------------------------------------------------- /api/graphql/resolvers/auth.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql' 2 | import bcrypt from 'bcryptjs' 3 | import jwt from 'jsonwebtoken' 4 | import { UserModel } from '../../models/user' 5 | import { validatePassword } from '../../utils/validations' 6 | import type { LoginInput, UserInput } from '../../../src/generated/graphql' 7 | import type { Types } from 'mongoose' 8 | 9 | import dotenv from 'dotenv' 10 | 11 | dotenv.config({ path: '../.env' }) 12 | 13 | const getJwtToken = (userId: Types.ObjectId, username: string) => { 14 | if (!process.env.JWT_SECRET) 15 | throw new Error('getJwtToken: JWT_SECRET is not provided!') 16 | 17 | return jwt.sign({ userId, username }, process.env.JWT_SECRET, { 18 | expiresIn: '1h', 19 | }) 20 | } 21 | 22 | export const Auth = { 23 | signup: async ({ 24 | userInput: { username, password, confirmPassword }, 25 | }: { 26 | userInput: UserInput 27 | }) => { 28 | const userExist = await UserModel.findOne({ username }) 29 | 30 | if (userExist) { 31 | throw new Error( 32 | 'Username is already being used, please try a different username.', 33 | ) 34 | } 35 | 36 | if (username.length < 3) { 37 | throw new Error('Username must be at least 3 characters.') 38 | } 39 | 40 | if (!validatePassword(password)) { 41 | throw new Error('Password does not meet password requirements.') 42 | } 43 | 44 | if (password !== confirmPassword) { 45 | throw new Error('Password and confirm password do not match.') 46 | } 47 | 48 | const hashedPassword = await bcrypt.hash(password, 12) 49 | 50 | const user = new UserModel({ username, password: hashedPassword }) 51 | 52 | const savedUser = await user.save() 53 | 54 | const token = getJwtToken(savedUser._id, savedUser.username) 55 | 56 | return { 57 | userId: savedUser._id, 58 | token, 59 | tokenExpiration: 60, 60 | username: savedUser.username, 61 | } 62 | }, 63 | login: async ({ 64 | loginInput: { username, password }, 65 | }: { 66 | loginInput: LoginInput 67 | }) => { 68 | const ERROR_MESSAGE = 'Username or password is incorrect' 69 | 70 | const user = await UserModel.findOne({ username }) 71 | 72 | if (!user) { 73 | throw new GraphQLError(ERROR_MESSAGE) 74 | } 75 | 76 | const correctPassword = await bcrypt.compare(password, user.password) 77 | 78 | if (!correctPassword) { 79 | throw new GraphQLError(ERROR_MESSAGE) 80 | } 81 | 82 | const token = getJwtToken(user._id, user.username) 83 | 84 | return { 85 | userId: user._id, 86 | token, 87 | tokenExpiration: 60, 88 | username: user.username, 89 | } 90 | }, 91 | } 92 | -------------------------------------------------------------------------------- /api/graphql/resolvers/events.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql' 2 | import { EventModel } from '../../models/event' 3 | import { UserModel } from '../../models/user' 4 | import type { 5 | EventInput, 6 | FilterInput, 7 | PaginationFilter, 8 | } from '../../../src/generated/graphql' 9 | import type { IAuthParams } from '../../interfaces/types' 10 | 11 | import dotenv from 'dotenv' 12 | 13 | dotenv.config({ path: '../.env' }) 14 | 15 | export const Events = { 16 | eventsData: async ( 17 | { 18 | filterInput: { 19 | searchText = '', 20 | pageNumber = 0, 21 | pageSize = 0, 22 | expiredCheck, 23 | currentCheck, 24 | startDate, 25 | endDate, 26 | }, 27 | }: { filterInput: FilterInput }, 28 | { isAuthorized, userId }: IAuthParams, 29 | ) => { 30 | const filter = 31 | isAuthorized && userId 32 | ? { $or: [{ createdBy: userId }, { isPrivate: false }] } 33 | : { isPrivate: false } 34 | const regexFilter = { 35 | ...filter, 36 | title: { $regex: searchText, $options: 'six' }, 37 | } 38 | 39 | const statusFilter = currentCheck 40 | ? { end: { $gte: new Date().toISOString() } } 41 | : expiredCheck 42 | ? { end: { $lt: new Date().toISOString() } } 43 | : {} 44 | 45 | const startDateFilter = startDate ? { start: { $gte: startDate } } : {} 46 | const endDateFilter = endDate ? { end: { $lt: endDate } } : {} 47 | 48 | pageSize = pageSize ?? 0 49 | pageNumber = pageNumber ?? 0 50 | 51 | const events = await EventModel.find({ 52 | ...regexFilter, 53 | ...statusFilter, 54 | ...startDateFilter, 55 | ...endDateFilter, 56 | }) 57 | .sort({ end: -1 }) 58 | .limit(pageSize) 59 | .skip(pageNumber > 0 ? (pageNumber - 1) * pageSize : 0) 60 | .populate('createdBy') 61 | const totalCount = await EventModel.countDocuments({ 62 | ...regexFilter, 63 | ...statusFilter, 64 | }) 65 | 66 | return { totalCount, events } 67 | }, 68 | getEvent: async ({ id }: { id: string }) => { 69 | const event = await EventModel.findOne({ _id: id }).populate('createdBy') 70 | 71 | if (!event) { 72 | throw new Error('Event could not be found') 73 | } 74 | 75 | return event 76 | }, 77 | getUserEvents: async ( 78 | { 79 | id, 80 | paginationFilter: { searchText = '', pageNumber = 0, pageSize = 0 }, 81 | }: { id: string; paginationFilter: PaginationFilter }, 82 | { isAuthorized, userId }: IAuthParams, 83 | ) => { 84 | if (!isAuthorized) { 85 | throw new GraphQLError('Unauthenticated') 86 | } 87 | 88 | if (!id || id !== userId) { 89 | throw new GraphQLError('Unauthenticated') 90 | } 91 | 92 | const filter = { 93 | createdBy: id, 94 | $or: [ 95 | { title: { $regex: searchText, $options: 'six' } }, 96 | { description: { $regex: searchText, $options: 'six' } }, 97 | ], 98 | } 99 | 100 | pageSize = pageSize ?? 0 101 | pageNumber = pageNumber ?? 0 102 | 103 | const events = await EventModel.find(filter) 104 | .limit(pageSize) 105 | .skip(pageNumber > 0 ? (pageNumber - 1) * pageSize : 0) 106 | .populate('createdBy') 107 | const totalCount = await EventModel.countDocuments(filter) 108 | 109 | return { totalCount, events } 110 | }, 111 | saveEvent: async ( 112 | { 113 | event: { id, title, start, end, isPrivate, description }, 114 | }: { event: EventInput }, 115 | { isAuthorized, userId }: IAuthParams, 116 | ) => { 117 | if (!isAuthorized) { 118 | throw new GraphQLError('Unauthenticated') 119 | } 120 | 121 | const user = await UserModel.findById(userId) 122 | 123 | if (!user) { 124 | throw new GraphQLError('Unauthenticated') 125 | } 126 | 127 | let savedEvent 128 | 129 | if (id) { 130 | const event = await EventModel.findOne({ _id: id, createdBy: userId }) 131 | 132 | if (!event) { 133 | throw new GraphQLError('Event could not be found') 134 | } 135 | 136 | savedEvent = await EventModel.findOneAndUpdate( 137 | { _id: id, createdBy: userId }, 138 | { title, start, end, isPrivate, description }, 139 | { new: true }, 140 | ).populate('createdBy') 141 | } else { 142 | const event = new EventModel({ 143 | title, 144 | start, 145 | end, 146 | isPrivate, 147 | description, 148 | createdBy: userId, 149 | }) 150 | 151 | savedEvent = await event.save().then((e) => e.populate('createdBy')) 152 | savedEvent.url = `${process.env.URI}/sharedEvent/${savedEvent._id}` 153 | await savedEvent.save({ timestamps: false }) 154 | } 155 | 156 | return savedEvent 157 | }, 158 | deleteEvent: async ( 159 | { id }: { id: string }, 160 | { isAuthorized, userId }: IAuthParams, 161 | ) => { 162 | if (!isAuthorized) { 163 | throw new GraphQLError('Unauthenticated') 164 | } 165 | 166 | const user = await UserModel.findById(userId) 167 | 168 | if (!user) { 169 | throw new GraphQLError('Unauthenticated') 170 | } 171 | 172 | const event = await EventModel.findOne({ _id: id, createdBy: userId }) 173 | 174 | if (!event) { 175 | throw new Error('Event could not be found') 176 | } 177 | 178 | await EventModel.deleteOne({ _id: id, createdBy: userId }) 179 | 180 | return true 181 | }, 182 | } 183 | -------------------------------------------------------------------------------- /api/graphql/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import { Auth } from './auth' 2 | import { Events } from './events' 3 | import { Users } from './users' 4 | 5 | export const rootValue = { 6 | ...Auth, 7 | ...Events, 8 | ...Users, 9 | } 10 | -------------------------------------------------------------------------------- /api/graphql/resolvers/users.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql' 2 | import { UserModel } from '../../models/user' 3 | import type { IAuthParams } from '../../interfaces/types' 4 | import type { UserInputFull } from '../../../src/generated/graphql' 5 | 6 | export const Users = { 7 | getUser: async ( 8 | { id }: { id: string }, 9 | { isAuthorized, userId }: IAuthParams, 10 | ) => { 11 | if (!isAuthorized) { 12 | throw new GraphQLError('Unauthenticated') 13 | } 14 | 15 | const user = await UserModel.findById(userId) 16 | 17 | if (!user) { 18 | throw new GraphQLError('Unauthenticated') 19 | } 20 | 21 | if (id !== userId) { 22 | throw new GraphQLError('Profile not found') 23 | } 24 | 25 | // const full = await UserModel.findOne({ _id: id }).populate('address'); 26 | 27 | return user 28 | }, 29 | saveUser: async ( 30 | { 31 | user: { _id, username, firstName, lastName, email, phoneNumber, bio }, 32 | }: { user: UserInputFull }, 33 | { isAuthorized, userId }: IAuthParams, 34 | ) => { 35 | if (!isAuthorized) { 36 | throw new GraphQLError('Unauthenticated') 37 | } 38 | 39 | const user = await UserModel.findById(userId) 40 | 41 | if (!user) { 42 | throw new GraphQLError('Unauthenticated') 43 | } 44 | 45 | if (_id !== userId) { 46 | throw new GraphQLError('Unauthenticated') 47 | } 48 | 49 | if (!username) { 50 | throw new Error('Username is required') 51 | } 52 | 53 | const userByUsername = await UserModel.findOne({ username }) 54 | 55 | if (userByUsername && userByUsername._id.toString() !== _id) { 56 | throw new Error( 57 | `"${username}" is already being used, please try a different username.`, 58 | ) 59 | } 60 | 61 | const updatedUser = await UserModel.findOneAndUpdate( 62 | { _id }, 63 | { username, firstName, lastName, email, phoneNumber, bio }, 64 | { new: true }, 65 | ) 66 | 67 | return updatedUser 68 | }, 69 | } 70 | -------------------------------------------------------------------------------- /api/graphql/schema/index.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'graphql-tag' 2 | 3 | export const typeDefs = gql` 4 | type Event { 5 | id: ID! 6 | } 7 | 8 | type User { 9 | _id: String! 10 | username: String! 11 | } 12 | 13 | type UserFull { 14 | _id: ID! 15 | username: String! 16 | firstName: String 17 | lastName: String 18 | email: String 19 | phoneNumber: String 20 | bio: String 21 | createdAt: Float 22 | updatedAt: Float 23 | } 24 | 25 | type EventFull { 26 | id: ID! 27 | title: String! 28 | start: String! 29 | end: String! 30 | url: String 31 | isPrivate: Boolean! 32 | description: String! 33 | createdBy: User 34 | createdAt: Float 35 | updatedAt: Float 36 | } 37 | 38 | type Events { 39 | totalCount: Int 40 | events: [EventFull!]! 41 | } 42 | 43 | type Auth { 44 | userId: ID! 45 | username: String! 46 | token: String! 47 | tokenExpiration: Int! 48 | } 49 | 50 | input UserInput { 51 | username: String! 52 | password: String! 53 | confirmPassword: String! 54 | } 55 | 56 | input UserInputFull { 57 | _id: String! 58 | username: String! 59 | firstName: String 60 | lastName: String 61 | email: String 62 | phoneNumber: String 63 | bio: String 64 | } 65 | 66 | input LoginInput { 67 | username: String! 68 | password: String! 69 | } 70 | 71 | input EventInput { 72 | id: String! 73 | title: String! 74 | start: String! 75 | end: String! 76 | isPrivate: Boolean! 77 | description: String! 78 | } 79 | 80 | input FilterInput { 81 | searchText: String 82 | startDate: String 83 | endDate: String 84 | pageNumber: Int 85 | pageSize: Int 86 | expiredCheck: Boolean 87 | currentCheck: Boolean 88 | } 89 | 90 | input PaginationFilter { 91 | searchText: String 92 | pageNumber: Int 93 | pageSize: Int 94 | } 95 | 96 | type Query { 97 | getUser(id: ID!): UserFull! 98 | getUserEvents(id: ID!, paginationFilter: PaginationFilter): Events! 99 | eventsData(filterInput: FilterInput): Events! 100 | login(loginInput: LoginInput!): Auth! 101 | } 102 | 103 | type Mutation { 104 | signup(userInput: UserInput!): Auth! 105 | saveUser(user: UserInputFull!): UserFull! 106 | saveEvent(event: EventInput!): EventFull! 107 | getEvent(id: ID!): EventFull! 108 | deleteEvent(id: ID!): Boolean! 109 | } 110 | ` 111 | -------------------------------------------------------------------------------- /api/index.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from '@apollo/server' 2 | import { expressMiddleware } from '@apollo/server/express4' 3 | import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer' 4 | import compression from 'compression' 5 | import express from 'express' 6 | import path from 'path' 7 | import cors from 'cors' 8 | import dotenv from 'dotenv' 9 | import cookieParser from 'cookie-parser' 10 | import http from 'http' 11 | 12 | import { connect, set } from 'mongoose' 13 | import { rootValue } from './graphql/resolvers' 14 | import { typeDefs } from './graphql/schema' 15 | import { json, urlencoded } from 'body-parser' 16 | import { context } from './middleware/auth' 17 | import type { IContext } from './interfaces/types' 18 | 19 | dotenv.config({ path: '../.env' }) 20 | 21 | const corsOptions = { 22 | origin: process.env.URI, 23 | credentials: true, 24 | optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204 25 | } 26 | 27 | const app = express() 28 | 29 | app.use(cookieParser()) 30 | app.use(compression()) 31 | 32 | app.use(json()) 33 | app.use(urlencoded({ extended: true })) 34 | 35 | app.use('/', express.static(`${__dirname}/../dist`)) 36 | 37 | app.get('*', (_, res) => { 38 | res.sendFile(path.join(__dirname, '../dist', 'index.html')) 39 | }) 40 | 41 | const startServer = async () => { 42 | const httpServer = http.createServer(app) 43 | const apolloServer = new ApolloServer({ 44 | typeDefs, 45 | rootValue, 46 | plugins: [ApolloServerPluginDrainHttpServer({ httpServer })], 47 | }) 48 | 49 | await apolloServer.start() 50 | app.use( 51 | '/graphql', 52 | cors(corsOptions), 53 | json(), 54 | expressMiddleware(apolloServer, { 55 | context, 56 | }), 57 | ) 58 | 59 | try { 60 | set('strictQuery', false) 61 | if (process.env.MONGODB_URI) { 62 | await connect(process.env.MONGODB_URI) 63 | } else { 64 | throw new Error('MONGODB_URI is not provided!') 65 | } 66 | 67 | await new Promise((resolve) => 68 | httpServer.listen({ port: process.env.PORT }, resolve), 69 | ) 70 | console.log( 71 | `🚀 Server ready at http://localhost:${process.env.PORT}/graphql`, 72 | ) 73 | } catch (err) { 74 | console.error('Error ocurred while connecting to MongoDB: ', err) 75 | } 76 | } 77 | 78 | startServer() 79 | -------------------------------------------------------------------------------- /api/interfaces/types.ts: -------------------------------------------------------------------------------- 1 | import { Types } from 'mongoose' 2 | 3 | export interface IEvent { 4 | id?: string 5 | title: string 6 | start: string 7 | end: string 8 | description: string 9 | url: string 10 | isPrivate: boolean 11 | createdBy: Types.ObjectId 12 | } 13 | 14 | export interface IUser { 15 | username: string 16 | password: string 17 | firstName: string 18 | lastName: string 19 | email: string 20 | phoneNumber: string 21 | bio: string 22 | createdEvents: Types.ObjectId[] 23 | } 24 | 25 | export interface IAuth { 26 | userId: string 27 | username: string 28 | token: string 29 | tokenExpiration?: number 30 | } 31 | 32 | export interface IContext { 33 | auth?: IAuth 34 | } 35 | 36 | export interface IAuthParams { 37 | isAuthorized: boolean 38 | userId: string 39 | } 40 | -------------------------------------------------------------------------------- /api/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import jwt, { type JwtPayload } from 'jsonwebtoken' 2 | import type { Request } from 'express' 3 | import type { IAuthParams } from '../interfaces/types' 4 | 5 | export const context = async ({ req }: { req: Request }) => { 6 | const auth = req.cookies['auth'] ? JSON.parse(req.cookies['auth']) : '' 7 | const customReq = req as Request & IAuthParams 8 | 9 | if (!auth) { 10 | customReq.isAuthorized = false 11 | return customReq 12 | } 13 | 14 | let decodedToken 15 | 16 | try { 17 | if (!process.env.JWT_SECRET) 18 | throw new Error('Context: JWT_SECRET is not provided!') 19 | 20 | decodedToken = jwt.verify(auth.token, process.env.JWT_SECRET) 21 | } catch (err) { 22 | console.error('Error verifying JWT:', err) 23 | customReq.isAuthorized = false 24 | return customReq 25 | } 26 | 27 | if (!decodedToken) { 28 | customReq.isAuthorized = false 29 | return customReq 30 | } 31 | 32 | customReq.isAuthorized = true 33 | customReq.userId = (decodedToken as JwtPayload).userId 34 | 35 | return customReq 36 | } 37 | -------------------------------------------------------------------------------- /api/models/event.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose' 2 | import type { IEvent } from '../interfaces/types' 3 | 4 | const schema = new Schema( 5 | { 6 | title: { 7 | type: String, 8 | required: true, 9 | }, 10 | start: { 11 | type: String, 12 | required: true, 13 | }, 14 | end: { 15 | type: String, 16 | required: true, 17 | }, 18 | description: { 19 | type: String, 20 | required: false, 21 | }, 22 | url: { 23 | type: String, 24 | required: false, 25 | }, 26 | isPrivate: { 27 | type: Boolean, 28 | required: false, 29 | }, 30 | createdBy: { 31 | type: Schema.Types.ObjectId, 32 | ref: 'User', 33 | }, 34 | }, 35 | { timestamps: true }, 36 | ) 37 | 38 | export const EventModel = model('Event', schema) 39 | -------------------------------------------------------------------------------- /api/models/user.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose' 2 | import type { IUser } from '../interfaces/types' 3 | 4 | const schema = new Schema( 5 | { 6 | username: { 7 | type: String, 8 | required: true, 9 | }, 10 | password: { 11 | type: String, 12 | required: true, 13 | }, 14 | firstName: { 15 | type: String, 16 | required: false, 17 | }, 18 | lastName: { 19 | type: String, 20 | required: false, 21 | }, 22 | email: { 23 | type: String, 24 | required: false, 25 | }, 26 | phoneNumber: { 27 | type: String, 28 | required: false, 29 | }, 30 | bio: { 31 | type: String, 32 | required: false, 33 | }, 34 | createdEvents: [ 35 | { 36 | type: Schema.Types.ObjectId, 37 | ref: 'Event', 38 | }, 39 | ], 40 | }, 41 | { timestamps: true }, 42 | ) 43 | 44 | export const UserModel = model('User', schema) 45 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "target": "es6", 7 | "moduleResolution": "node", 8 | "noUnusedLocals": false, 9 | "verbatimModuleSyntax": false 10 | } 11 | //"include": ["graphql/**/*", "models/**/*", "utils/**/*", "index.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /api/utils/validations.ts: -------------------------------------------------------------------------------- 1 | export const validatePassword = (password: string) => { 2 | const passwordRegex = /^(?=.*\d)(?=.*[!@#$%^&*])(?=.*[a-z])(?=.*[A-Z]).{6,}$/ 3 | 4 | return passwordRegex.test(password) 5 | } 6 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: ${VITE_APP_GRAPHQL_ENDPOINT} 3 | documents: 'src/**/*.graphql' 4 | generates: 5 | src/generated/graphql.tsx: 6 | plugins: 7 | - 'typescript' 8 | - 'typescript-operations' 9 | - 'typescript-resolvers' 10 | - 'typescript-react-apollo' 11 | config: 12 | useTypeImports: true 13 | ./graphql.schema.json: 14 | plugins: 15 | - 'introspection' 16 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals' 2 | import pluginJs from '@eslint/js' 3 | import tseslint from 'typescript-eslint' 4 | import pluginReact from 'eslint-plugin-react' 5 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' 6 | 7 | /** @type {import('eslint').Linter.Config[]} */ 8 | export default [ 9 | { 10 | files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'], 11 | }, 12 | { languageOptions: { globals: { ...globals.browser, ...globals.jest } } }, 13 | { 14 | ignores: ['src/generated/**/*', 'dist/**/*'], 15 | }, 16 | pluginJs.configs.recommended, 17 | ...tseslint.configs.recommended, 18 | pluginReact.configs.flat.recommended, 19 | eslintPluginPrettierRecommended, 20 | { 21 | rules: { 22 | 'react/react-in-jsx-scope': 'off', 23 | 'react/jsx-uses-react': 'off', 24 | }, 25 | settings: { 26 | react: { 27 | version: 'detect', 28 | }, 29 | }, 30 | }, 31 | ] 32 | -------------------------------------------------------------------------------- /images/react-event-pic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedalatawi/react-event-scheduler/42ae79e4ed1887d9b19445fd399c5eaaaa309252/images/react-event-pic.gif -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' 2 | declare module '*.jpg' 3 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React Event Scheduler 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | module.exports = { 3 | testEnvironment: 'jsdom', 4 | transform: { 5 | '^.+\\.tsx?$': ['ts-jest', { tsconfig: './tsconfig.jest.json' }], 6 | }, 7 | moduleNameMapper: { 8 | '\\.(css|less|sass|scss)$': 'identity-obj-proxy', 9 | '^.+\\.svg$': 'jest-transformer-svg', 10 | '^@/(.*)$': '/src/$1', 11 | }, 12 | setupFilesAfterEnv: ['/jest.setup.ts'], 13 | } 14 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "event-scheduler", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "run-p --race start:server start:web", 7 | "build": "tsc && vite build", 8 | "test": "jest", 9 | "test:coverage": "jest --watchAll=false --coverage", 10 | "start:server": "cd api && ts-node-dev --respawn --transpile-only ./index.ts && wait-on tcp:4000", 11 | "start:web": "vite --host --open", 12 | "ts-check": "tsc --noEmit --watch", 13 | "lint": "eslint .", 14 | "lint:fix": "eslint --fix", 15 | "format": "prettier --write \"./**/*.{js,jsx,css,tsx,ts,json,md,yml}\" --config ./.prettierrc", 16 | "codegen": "graphql-codegen --config codegen.yml -r dotenv/config", 17 | "prepare": "husky install" 18 | }, 19 | "dependencies": { 20 | "@apollo/server": "^4.9.5", 21 | "@atawi/react-datatable": "^1.0.5", 22 | "@atawi/react-popover": "^1.0.4", 23 | "@fullcalendar/core": "^6.1.5", 24 | "@fullcalendar/daygrid": "^6.1.4", 25 | "@fullcalendar/interaction": "^6.1.4", 26 | "@fullcalendar/react": "^6.1.4", 27 | "@fullcalendar/timegrid": "^6.1.4", 28 | "@types/styled-components": "^5.1.26", 29 | "bcryptjs": "^2.4.3", 30 | "body-parser": "^1.20.2", 31 | "compression": "^1.7.4", 32 | "concurrently": "^8.2.2", 33 | "cookie-parser": "^1.4.6", 34 | "cors": "^2.8.5", 35 | "dataloader": "^2.2.2", 36 | "dotenv": "^16.0.3", 37 | "express": "^4.19.2", 38 | "graphql": "^16.9.0", 39 | "graphql-tag": "^2.12.6", 40 | "js-cookie": "^3.0.5", 41 | "jsonwebtoken": "^9.0.0", 42 | "lodash": "^4.17.21", 43 | "luxon": "^3.3.0", 44 | "mongoose": "^8.1.2", 45 | "react": "^18.3.1", 46 | "react-bootstrap": "^2.10.0", 47 | "react-darkreader": "^1.5.6", 48 | "react-dom": "^18.2.0", 49 | "react-hot-toast": "^2.4.0", 50 | "react-icons": "^5.0.1", 51 | "react-idle-timer": "^5.5.2", 52 | "react-router": "^6.26.2", 53 | "react-router-dom": "^6.10.0", 54 | "react-share": "^5.1.0", 55 | "styled-components": "^6.1.9", 56 | "ts-node": "^10.9.2" 57 | }, 58 | "devDependencies": { 59 | "@apollo/client": "^3.10.8", 60 | "@eslint/js": "^9.14.0", 61 | "@graphql-codegen/cli": "^5.0.3", 62 | "@graphql-codegen/introspection": "^4.0.3", 63 | "@graphql-codegen/typescript": "^4.1.1", 64 | "@graphql-codegen/typescript-operations": "^4.3.1", 65 | "@graphql-codegen/typescript-react-apollo": "^4.3.2", 66 | "@graphql-codegen/typescript-resolvers": "^4.4.0", 67 | "@testing-library/dom": "^10.4.0", 68 | "@testing-library/jest-dom": "^6.6.3", 69 | "@testing-library/react": "^16.0.1", 70 | "@testing-library/user-event": "^14.5.2", 71 | "@types/bcryptjs": "^2.4.6", 72 | "@types/bootstrap": "^5.2.6", 73 | "@types/compression": "^1.7.2", 74 | "@types/cookie-parser": "^1.4.2", 75 | "@types/cors": "^2.8.13", 76 | "@types/express": "^4.17.21", 77 | "@types/express-sslify": "^1.2.2", 78 | "@types/graphql": "^14.5.0", 79 | "@types/jest": "^29.5.14", 80 | "@types/jquery": "^3.5.30", 81 | "@types/js-cookie": "^3.0.6", 82 | "@types/jsonwebtoken": "^9.0.1", 83 | "@types/lodash": "^4.14.191", 84 | "@types/luxon": "^3.3.0", 85 | "@types/mongoose": "^5.11.97", 86 | "@types/node": "^18.15.11", 87 | "@types/react": "^18.3.3", 88 | "@types/react-dom": "^18.2.25", 89 | "@types/react-test-renderer": "^18.0.0", 90 | "@typescript-eslint/eslint-plugin": "^8.12.2", 91 | "@typescript-eslint/parser": "^8.12.2", 92 | "@vitejs/plugin-react": "^4.3.3", 93 | "bootstrap": "^5.2.3", 94 | "eslint": "^9.14.0", 95 | "eslint-config-prettier": "^9.1.0", 96 | "eslint-plugin-prettier": "^5.2.1", 97 | "eslint-plugin-react": "^7.37.2", 98 | "globals": "^15.11.0", 99 | "husky": "^8.0.0", 100 | "identity-obj-proxy": "^3.0.0", 101 | "jest": "^29.7.0", 102 | "jest-environment-jsdom": "^29.7.0", 103 | "jest-transformer-svg": "^2.0.2", 104 | "jquery": "^3.6.4", 105 | "lint-staged": "^13.2.2", 106 | "npm-run-all": "^4.1.5", 107 | "prettier": "^3.3.3", 108 | "pretty-quick": "^4.0.0", 109 | "react-error-overlay": "6.0.11", 110 | "ts-jest": "^29.2.5", 111 | "ts-node-dev": "^2.0.0", 112 | "typescript": "^5.6.3", 113 | "typescript-eslint": "^8.12.2", 114 | "vite": "^5.4.10", 115 | "vite-plugin-checker": "^0.8.0", 116 | "vite-tsconfig-paths": "^5.1.1", 117 | "wait-on": "^7.0.1" 118 | }, 119 | "browserslist": { 120 | "production": [ 121 | ">0.2%", 122 | "not dead", 123 | "not op_mini all" 124 | ], 125 | "development": [ 126 | "last 1 chrome version", 127 | "last 1 firefox version", 128 | "last 1 safari version" 129 | ] 130 | }, 131 | "resolutions": { 132 | "styled-components": "^5", 133 | "react-error-overlay": "^6.0.11" 134 | }, 135 | "lint-staged": { 136 | "**/*.{js,jsx,ts,tsx}": [ 137 | "npx prettier --write", 138 | "npx eslint --fix" 139 | ] 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedalatawi/react-event-scheduler/42ae79e4ed1887d9b19445fd399c5eaaaa309252/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedalatawi/react-event-scheduler/42ae79e4ed1887d9b19445fd399c5eaaaa309252/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmedalatawi/react-event-scheduler/42ae79e4ed1887d9b19445fd399c5eaaaa309252/public/logo512.png -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "packageRules": [ 4 | { 5 | "updateTypes": ["minor", "patch"], 6 | "automerge": true 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .required .form-label:after { 2 | content: '*'; 3 | color: red; 4 | } 5 | 6 | .DataTable { 7 | .virtual-cell, 8 | .header-cell, 9 | .checkbox-cell { 10 | border-right: none !important; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | // import { render, screen } from '@testing-library/react' 2 | // import App from './App' 3 | 4 | xtest('renders learn react link', () => { 5 | console.log() 6 | // render() 7 | // const linkElement = screen.getByText(/learn react/i) 8 | // expect(linkElement).toBeInTheDocument() 9 | }) 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { ApolloProvider } from '@apollo/client' 2 | import client from '@/apolloClient' 3 | import UserIdleTimer from '@/components/UserIdleTimer/UserIdleTimer' 4 | import { useContext } from 'react' 5 | import AuthContext from '@/store/auth-context' 6 | import AppRoutes from '@/Routes' 7 | import { Container } from 'react-bootstrap' 8 | import Footer from '@/components/Footer/Footer' 9 | 10 | import './App.css' 11 | import { Toaster } from 'react-hot-toast' 12 | import styled from 'styled-components' 13 | import { useMatch } from 'react-router-dom' 14 | 15 | const RoutesContainer = styled(Container)` 16 | min-height: calc(100vh - 85px); 17 | ` 18 | 19 | function App() { 20 | const { auth, removeAuth } = useContext(AuthContext) 21 | const welcomePagePath = useMatch('/') 22 | 23 | return ( 24 | <> 25 | 26 | {auth && } 27 | 28 | 29 | 30 | 31 | 32 | {!welcomePagePath &&