├── .DS_Store
├── .env.example
├── .eslintignore
├── .eslintrc.js
├── .eslintrc.json
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ └── linter.yml
├── .gitignore
├── .vscode
└── settings.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── next.config.js
├── package-lock.json
├── package.json
├── pages
├── _app.tsx
├── api
│ └── poll
│ │ ├── [id].ts
│ │ ├── [id]
│ │ └── [secret].ts
│ │ └── create.ts
├── how-to-vote.tsx
├── how-to.tsx
├── index.tsx
├── poll
│ ├── [id].tsx
│ └── [id]
│ │ └── [secret].tsx
├── privacy.tsx
└── recent-polls.tsx
├── public
├── .DS_Store
├── banner.png
└── favicon.svg
├── src
├── components
│ ├── BottomNav.tsx
│ ├── Layout.tsx
│ ├── LogoSVG.tsx
│ ├── Navbar.tsx
│ ├── SamayRBC.tsx
│ ├── copyText
│ │ ├── CopyTextMain.tsx
│ │ └── index.tsx
│ └── poll
│ │ ├── AdminPollInfo.tsx
│ │ ├── DeletePoll.tsx
│ │ ├── MarkFinalTime.tsx
│ │ ├── MarkTimes.tsx
│ │ ├── MarkTimesOneOnOne.tsx
│ │ ├── PollDateTime.tsx
│ │ ├── PollTableAdmin.tsx
│ │ ├── PollTableVoter.tsx
│ │ ├── SubmitFinalTime.tsx
│ │ ├── SubmitTimes.tsx
│ │ └── VoterPollInfo.tsx
├── helpers
│ ├── index.ts
│ └── toastOptions.ts
├── models
│ └── poll.ts
├── styles
│ ├── bottom-nav.scss
│ ├── datetime.scss
│ ├── form.scss
│ ├── global.scss
│ ├── how-to.scss
│ ├── navbar.scss
│ ├── new-poll.scss
│ ├── poll.scss
│ ├── privacy.scss
│ ├── rat.scss
│ ├── samay-rbc.scss
│ ├── voter-page.scss
│ └── your-polls.scss
└── utils
│ ├── api
│ └── server.ts
│ ├── db.ts
│ └── react-available-times.d.ts
└── tsconfig.json
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anandbaburajan/samay/ae0c7716169723c5629f9a2c5cc007291126179a/.DS_Store
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # In production, NEXT_MONGODB_URI is the connection string
2 | # In development, NEXT_MONGODB_URI is mongodb://localhost:27017/samayPolls
3 | NEXT_MONGODB_URI=
4 |
5 | # In production, NEXT_PUBLIC_BASE_URL is the base URL of your deployment; like https://samay.app
6 | # In development, NEXT_PUBLIC_BASE_URL is http://localhost:3000
7 | NEXT_PUBLIC_BASE_URL=
8 |
9 | # Key and IV for data encryption and decryption
10 | # Generate NEXT_PUBLIC_ENCRYPTION_KEY using $openssl rand -hex 16
11 | # Generate NEXT_PUBLIC_ENCRYPTION_IV using $openssl rand -hex 8
12 | NEXT_PUBLIC_ENCRYPTION_KEY=
13 | NEXT_PUBLIC_ENCRYPTION_IV=
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
3 | build/
4 |
5 | .eslintrc.js
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | node: true,
5 | es2020: true,
6 | },
7 | parser: "@typescript-eslint/parser",
8 | parserOptions: {
9 | ecmaVersion: 2020,
10 | sourceType: "module",
11 | ecmaFeatures: {
12 | jsx: true,
13 | },
14 | project: './tsconfig.json',
15 | },
16 | plugins: ["@typescript-eslint", "react", "prettier"],
17 | extends: [
18 | "airbnb-typescript",
19 | "airbnb/hooks",
20 | "plugin:@typescript-eslint/recommended",
21 | "plugin:react/recommended",
22 | "plugin:import/errors",
23 | "plugin:import/warnings",
24 | "plugin:import/typescript",
25 | "prettier",
26 | "prettier/@typescript-eslint",
27 | "prettier/react",
28 | ],
29 | rules: {
30 | "react/jsx-filename-extension": [1, { extensions: [".ts", ".tsx"] }],
31 | "react/react-in-jsx-scope": "off",
32 | "import/extensions": "off",
33 | "react/prop-types": "off",
34 | "jsx-a11y/anchor-is-valid": "off",
35 | "react/jsx-props-no-spreading": ["error", { custom: "ignore" }],
36 | "prettier/prettier": "warn",
37 | "react/no-unescaped-entities": "off",
38 | "import/no-cycle": [0, { ignoreExternal: true }],
39 | "prefer-const": "off",
40 | // needed because of https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md#how-to-use & https://stackoverflow.com/questions/63818415/react-was-used-before-it-was-defined
41 | "no-use-before-define": "off",
42 | "@typescript-eslint/no-use-before-define": [
43 | "error",
44 | { functions: false, classes: false, variables: true },
45 | ],
46 | "no-underscore-dangle": ["error", { "allow": ["_id", "__persistor"] }]
47 |
48 | },
49 | globals: {
50 | "React": "writable"
51 | },
52 | settings: {
53 | "import/resolver": {
54 | "babel-module": {
55 | extensions: [".js", ".jsx", ".ts", ".tsx"],
56 | },
57 | node: {
58 | extensions: [".js", ".jsx", ".ts", ".tsx"],
59 | paths: ["src"],
60 | },
61 | },
62 | },
63 | };
64 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "airbnb-typescript/base",
4 | "plugin:@typescript-eslint/eslint-recommended",
5 | "plugin:@typescript-eslint/recommended",
6 | "next"
7 | ],
8 | "parser": "@typescript-eslint/parser",
9 | "plugins": [
10 | "@typescript-eslint"
11 | ],
12 | "parserOptions": {
13 | "project": "./tsconfig.json"
14 | },
15 | "rules": {
16 | "object-curly-newline": "off",
17 | "arrow-body-style": "off",
18 | "no-underscore-dangle": "off",
19 | "prefer-destructuring": "off",
20 | "@typescript-eslint/indent": "off",
21 | "@typescript-eslint/semi": "warn",
22 | "@typescript-eslint/no-non-null-assertion": "off"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.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/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/workflows/linter.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 | on:
3 | pull_request:
4 | branches: [main]
5 | jobs:
6 | eslint:
7 | name: eslint
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v1
11 | - name: install node v12
12 | uses: actions/setup-node@v1
13 | with:
14 | node-version: 12
15 | - name: yarn install
16 | run: yarn install
17 | - name: eslint
18 | uses: icrawl/action-eslint@v1
19 | with:
20 | custom-glob: "**/*.{ts,tsx}"
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 |
43 | # TypeScript v1 declaration files
44 | typings/
45 |
46 | # TypeScript cache
47 | *.tsbuildinfo
48 |
49 | # Optional npm cache directory
50 | .npm
51 |
52 | # Optional eslint cache
53 | .eslintcache
54 |
55 | # Next.js build output and env files
56 | .next
57 | next-env.d.ts
58 |
59 | .env
60 |
61 | .DS_Store
62 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.codeActionsOnSave": {
3 | "source.fixAll.eslint": "explicit"
4 | },
5 | "editor.formatOnSave": true
6 | }
7 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting [Anand Baburajan](anandbaburajan@gmail.com). All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The maintainers are
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available [here](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html)
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing
2 |
3 | Hi there! We're thrilled that you'd like to contribute to Samay!
4 |
5 | ## Issues and PRs
6 |
7 | If you have suggestions for how this project could be improved, or want to report a bug, open an issue! We'd love all and any contributions. If you have questions, too, we'd love to hear them.
8 |
9 | We'd also love PRs. If you're thinking of a large PR, please open up an issue first to discuss your proposed changes with a project maintainer!
10 |
11 | ## Submitting a pull request
12 |
13 | 1. Fork and clone the repository.
14 | 2. Configure and install the dependencies.
15 | 3. Make sure the tests and linters pass on your machine.
16 | 4. Create a new branch.
17 | 5. Make your change, add tests, and make sure the tests still pass.
18 | 6. Push to your fork and submit a pull request.
19 | 7. Pat your self on the back and wait for your pull request to be reviewed and merged.
20 |
21 | Here are a few things you can do that will increase the likelihood of your pull request being accepted:
22 |
23 | - Write and update tests.
24 | - Keep your changes as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
25 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
26 |
27 | Work in Progress pull requests are also welcome to get feedback early on, or if there is something blocking you.
28 |
29 | ## Resources
30 |
31 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
32 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
33 | - [GitHub Help](https://help.github.com)
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Samay
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 |
3 |
8 |
9 |
10 |
11 | Samay — free and open source group scheduling tool
12 |
13 |
14 |
15 |
16 | [](https://github.com/anandbaburajan/samay/blob/main/LICENSE)
17 | [](https://samay.app/)
18 | [](https://samay.app/)
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | Samay is a free and open source group scheduling tool. Quickly find a time which works for everyone without the back-and-forth texts/emails!
29 |
30 | > #### Create a poll
31 | >
32 | > Select times you're free (click and drag), and optionally enter the title, description and location. The default poll type is "group" — to find a common time which works for everyone. If you want to have one-on-one meetings (parent-teacher meetings for example), select the "one-on-one" poll type.
33 | >
34 | > #### Share the poll
35 | >
36 | > Copy and share the poll link with the participants to let them mark their availability. In group polls, participants can either vote [yes] by clicking once or [if need be] by clicking twice. In one-on-one polls, participants can select their one preferred time. No login required. No time zone confusion since Samay automatically shows participants times in their local time zone.
37 | >
38 | > #### Book the meeting
39 | >
40 | > In group polls, find the most popular times and see who's free with [yes] votes - or who can be - with [if need be] votes, book the meeting and share the final time with the participants! In one-on-one polls, find who has chosen which time slot for a one-on-one with you!
41 |
42 | Create a poll now at [Samay.app](https://samay.app/)!
43 |
44 | ## Motivation
45 |
46 | After my GSoC '20 at LiberTEM, I wanted to have a video call with my mentors. They said yes, and since the next step was to find a suitable and common time, one of them sent me a link to a meeting poll created using a proprietary online service. It had surprisingly bad UX and was covered with advertisements. I searched for good, free and open source group scheduling tools, but didn't find any. So I decided to fix that problem.
47 |
48 | ## Get in touch
49 |
50 | If you have suggestions for how Samay could be improved or want to report an issue, check if a corresponding GitHub issue is already opened [here](https://github.com/anandbaburajan/samay/issues), otherwise open a new issue.
51 |
52 | ## Self-hosting
53 |
54 | ### Docker
55 |
56 | Coming soon!
57 |
58 | ### Vercel and MongoDB Atlas
59 |
60 | Samay is built with MongoDB and Next.js, so for a quick and free setup, you can use a free MongoDB Atlas cluster and Vercel's hobby plan.
61 |
62 | You can get started with MongoDB Atlas for free [here](https://www.mongodb.com/basics/mongodb-atlas-tutorial). Make sure to add all IP addresses (0.0.0.0/0) to the IP access list of your Atlas cluster since it is not possible to determine the IP addresses of Vercel deployments.
63 |
64 | You can get started with Vercel's hobby plan for free:
65 |
66 | 1. Fork this repo to your own GitHub account
67 | 2. Go to https://vercel.com/dashboard
68 | 3. Create a new project
69 | 4. Import your forked repository
70 | 5. Set the environment variables (according to the instructions in .env.example)
71 | 6. Deploy
72 |
73 | ## Contributing
74 |
75 | ### Development
76 |
77 | First, make sure you have [Node.js](https://nodejs.org/en/) and [MongoDB](https://www.mongodb.com/docs/manual/installation/#mongodb-installation-tutorials) installed. Then, to develop locally:
78 |
79 | 1. Fork this repo to your own GitHub account and then clone it.
80 |
81 | ```sh
82 | git clone https://github.com//samay.git
83 | ```
84 |
85 | 2. Go to the project folder
86 |
87 | ```sh
88 | cd samay
89 | ```
90 |
91 | 3. Create a new branch:
92 |
93 | ```sh
94 | git checkout -b MY_BRANCH_NAME
95 | ```
96 |
97 | 4. Install the dependencies with:
98 |
99 | ```sh
100 | npm i
101 | ```
102 |
103 | 5. Copy `.env.example` to `.env`
104 |
105 | ```sh
106 | cp .env.example .env
107 | ```
108 |
109 | 6. Set the env variables according to the instructions in the .env file
110 |
111 | 7. Start developing and watch for code changes:
112 |
113 | ```sh
114 | npm run dev
115 | ```
116 |
117 | 8. Please make sure that you can make a full production build before opening a PR. You can build the project with:
118 |
119 | ```sh
120 | npm run build
121 | ```
122 |
123 | ## Acknowledgements
124 |
125 | Thanks to FOSS United for selecting Samay as one of the [winning projects](https://forum.fossunited.org/t/foss-hack-3-0-results/1882) at FOSS Hack 3.0.
126 |
127 | Thanks to these amazing projects which help power Samay:
128 |
129 | - React-big-calendar
130 | - React
131 | - Next.js
132 | - Day.js
133 | - Bootstrap
134 | - MongoDB
135 | - Mongoose
136 | - Inter
137 | - Cal Sans
138 |
139 | ## License
140 |
141 | Samay is distributed under the [MIT License](https://github.com/anandbaburajan/samay/blob/main/LICENSE).
142 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | sassOptions: {
5 | includePaths: ["./styles"],
6 | },
7 | eslint: {
8 | ignoreDuringBuilds: true,
9 | },
10 | typescript: {
11 | ignoreBuildErrors: true,
12 | },
13 | };
14 |
15 | module.exports = nextConfig;
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "samay",
3 | "description": "Free and open source group scheduling tool",
4 | "version": "2.0.0",
5 | "scripts": {
6 | "dev": "NODE_OPTIONS='--inspect' next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "npx eslint . --ext .tsx,.ts"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/samayapp/samay.git"
14 | },
15 | "license": "MIT",
16 | "dependencies": {
17 | "@babel/core": "^7.12.10",
18 | "@fontsource/inter": "^5.0.17",
19 | "bootstrap": "^4.5.3",
20 | "cal-sans": "^1.0.1",
21 | "copy-to-clipboard": "^3.3.1",
22 | "dayjs": "^1.9.7",
23 | "mongoose": "^6.4.6",
24 | "nanoid": "^3.3.4",
25 | "next": "^12.1.0",
26 | "react": "^17.0.2",
27 | "react-big-calendar": "^1.6.8",
28 | "react-bootstrap": "^1.6.8",
29 | "react-bootstrap-icons": "^1.2.3",
30 | "react-dom": "^17.0.2",
31 | "react-toastify": "^9.1.1",
32 | "sass": "^1.49.11"
33 | },
34 | "devDependencies": {
35 | "@types/mongoose": "^5.10.5",
36 | "@types/node": "^14.14.16",
37 | "@types/react": "^17.0.0",
38 | "@types/react-big-calendar": "^1.6.1",
39 | "@types/react-toastify": "^4.1.0",
40 | "@typescript-eslint/eslint-plugin": "^2.34.0",
41 | "@typescript-eslint/parser": "^2.34.0",
42 | "babel-core": "^6.26.3",
43 | "babel-jest": "^26.6.3",
44 | "babel-plugin-module-resolver": "^4.0.0",
45 | "babel-preset-env": "^1.7.0",
46 | "babel-preset-react": "^6.24.1",
47 | "eslint": "^6.8.0",
48 | "eslint-config-airbnb-typescript": "^7.2.1",
49 | "eslint-config-prettier": "^6.15.0",
50 | "eslint-import-resolver-babel-module": "^5.2.0",
51 | "eslint-plugin-import": "^2.22.1",
52 | "eslint-plugin-jsx-a11y": "^6.4.1",
53 | "eslint-plugin-prettier": "^3.1.4",
54 | "eslint-plugin-react": "^7.21.5",
55 | "eslint-plugin-react-hooks": "^2.5.1",
56 | "prettier": "^2.2.1",
57 | "typescript": "^4.6.2"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { AppProps } from "next/app";
2 | import "cal-sans";
3 | import "@fontsource/inter/400.css";
4 | import "@fontsource/inter/500.css";
5 | import "../src/styles/global.scss";
6 |
7 | const App = ({
8 | Component,
9 | pageProps: { session, ...pageProps },
10 | }: AppProps): JSX.Element => {
11 | return ;
12 | };
13 |
14 | export default App;
15 |
--------------------------------------------------------------------------------
/pages/api/poll/[id].ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 | import SamayPoll, { Vote, PollDoc } from "../../../src/models/poll";
3 | import { isTimePresentInPollTimes } from "../../../src/helpers";
4 | import connectToDatabase from "../../../src/utils/db";
5 |
6 | export default async (
7 | req: NextApiRequest,
8 | res: NextApiResponse
9 | ): Promise => {
10 | const {
11 | query: { id },
12 | method,
13 | body,
14 | } = req;
15 |
16 | switch (method) {
17 | case "GET":
18 | try {
19 | await connectToDatabase();
20 | const poll: PollDoc | null = await SamayPoll.findOne({
21 | _id: id,
22 | }).lean();
23 | if (!poll) {
24 | res.status(404).json({ message: "Poll does not exist" });
25 | } else {
26 | res.status(200).json(poll);
27 | }
28 | } catch (err) {
29 | res.status(404).json({ message: err.message });
30 | }
31 | break;
32 | case "PUT":
33 | try {
34 | await connectToDatabase();
35 | const poll: PollDoc | null = await SamayPoll.findOne({
36 | _id: id,
37 | });
38 | if (poll) {
39 | const vote: Vote = JSON.parse(body);
40 | if (!poll.open) {
41 | res.status(400).json({ message: "Poll closed" });
42 | } else if (
43 | !vote.times.every((time) =>
44 | isTimePresentInPollTimes(time, poll.times)
45 | )
46 | ) {
47 | res.status(400).json({ message: "Invalid times" });
48 | } else {
49 | const currentVotes: Vote[] | undefined = poll.votes;
50 | let newVotes: Vote[] | undefined;
51 |
52 | if (currentVotes && currentVotes?.length > 0) {
53 | newVotes = currentVotes;
54 | newVotes.push(vote);
55 | } else {
56 | newVotes = [
57 | {
58 | name: vote.name,
59 | times: vote.times,
60 | },
61 | ];
62 | }
63 | const updatedPoll: PollDoc | null = await SamayPoll.findOneAndUpdate(
64 | { _id: id },
65 | { votes: newVotes },
66 | { new: true }
67 | );
68 | res.status(201).json(updatedPoll);
69 | }
70 | } else {
71 | res.status(404).json({ message: "Poll does not exist" });
72 | }
73 | } catch (err) {
74 | res.status(400).json({ message: err.message });
75 | }
76 | break;
77 | default:
78 | res.setHeader("Allow", ["GET", "PUT"]);
79 | res.status(405).end(`Method ${method} Not Allowed`);
80 | }
81 | };
82 |
--------------------------------------------------------------------------------
/pages/api/poll/[id]/[secret].ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 | import SamayPoll, { PollDoc } from "../../../../src/models/poll";
3 | import connectToDatabase from "../../../../src/utils/db";
4 |
5 | export default async (
6 | req: NextApiRequest,
7 | res: NextApiResponse
8 | ): Promise => {
9 | const {
10 | query: { id, secret },
11 | method,
12 | body,
13 | } = req;
14 |
15 | switch (method) {
16 | case "PUT":
17 | try {
18 | await connectToDatabase();
19 | const poll: PollDoc | null = await SamayPoll.findOne({
20 | _id: id,
21 | }).lean();
22 | if (poll) {
23 | if (poll.secret !== secret) {
24 | res.status(403).json({ message: "Forbidden" });
25 | } else {
26 | try {
27 | const updatedPoll: PollDoc | null = await SamayPoll.findOneAndUpdate(
28 | { _id: id },
29 | JSON.parse(body),
30 | { new: true }
31 | );
32 | res.status(201).json(updatedPoll);
33 | } catch (err) {
34 | res.status(400).json({ message: err.message });
35 | }
36 | }
37 | } else {
38 | res.status(404).json({ message: "Poll not found" });
39 | }
40 | } catch (err) {
41 | res.status(400).json({ message: err.message });
42 | }
43 | break;
44 | case "DELETE":
45 | try {
46 | await connectToDatabase();
47 | const poll: PollDoc | null = await SamayPoll.findOne({
48 | _id: id,
49 | }).lean();
50 | if (poll) {
51 | if (poll.secret !== secret) {
52 | res.status(403).json({ message: "Forbidden" });
53 | } else {
54 | try {
55 | const deletedPoll = await SamayPoll.findByIdAndRemove(id);
56 | res.status(200).json(deletedPoll);
57 | } catch (err) {
58 | res.status(400).json({ message: err.message });
59 | }
60 | }
61 | } else {
62 | res.status(404).json({ message: "Poll not found" });
63 | }
64 | } catch (err) {
65 | res.status(400).json({ message: err.message });
66 | }
67 | break;
68 | default:
69 | res.setHeader("Allow", ["PUT", "DELETE"]);
70 | res.status(405).end(`Method ${method} Not Allowed`);
71 | }
72 | };
73 |
--------------------------------------------------------------------------------
/pages/api/poll/create.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 | import SamayPoll, { PollDoc } from "../../../src/models/poll";
3 | import connectToDatabase from "../../../src/utils/db";
4 |
5 | export default async (
6 | req: NextApiRequest,
7 | res: NextApiResponse
8 | ): Promise => {
9 | const { method, body } = req;
10 |
11 | switch (method) {
12 | case "POST":
13 | try {
14 | await connectToDatabase();
15 | const newPoll: PollDoc = new SamayPoll(JSON.parse(body));
16 | await newPoll.save();
17 | res.status(201).json(newPoll);
18 | } catch (err) {
19 | res.status(400).json({ message: err.message });
20 | }
21 | break;
22 | default:
23 | res.setHeader("Allow", ["POST"]);
24 | res.status(405).end(`Method ${method} Not Allowed`);
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/pages/how-to-vote.tsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import { useRouter } from "next/router";
3 | import { Card, CardGroup, Container } from "react-bootstrap";
4 | import { CheckCircleFill, CircleFill } from "react-bootstrap-icons";
5 | import Layout from "../src/components/Layout";
6 |
7 | const HowTo = (): JSX.Element => {
8 | if (typeof window !== "undefined") {
9 | localStorage.setItem("samayNewVisitor", JSON.stringify(false));
10 | }
11 |
12 | const router = useRouter();
13 |
14 | return (
15 | <>
16 |
17 | Samay — how-to
18 |
19 |
20 |
21 |
22 |
26 |
30 |
31 |
32 |
36 |
40 |
41 |
42 |
43 |
47 |
51 |
52 |
53 |
54 |
55 |
56 | Samay — free and open source group scheduling tool
57 |
58 |
59 | Find a time which works for everyone without the back-and-forth
60 | texts/emails!
61 |
62 |
63 |
64 |
65 |
66 | How to vote
67 |
68 |
69 | In group polls, select all time slots you're available at. You
70 | can vote "yes" [
71 | ] by
72 | clicking a slot once or "if need be" [
73 | ] by
74 | clicking twice. Clicking again would remove the vote.
75 |
76 |
77 | In one-on-one polls, you can select your one preferred time.
78 |
79 |
80 | No login required.
81 |
82 |
83 |
84 |
85 |
86 |
87 | >
88 | );
89 | };
90 |
91 | export default HowTo;
92 |
--------------------------------------------------------------------------------
/pages/how-to.tsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import Link from "next/link";
3 | import { Button, Card, CardGroup, Container } from "react-bootstrap";
4 | import { CheckCircleFill, CircleFill } from "react-bootstrap-icons";
5 | import Layout from "../src/components/Layout";
6 |
7 | const HowTo = (): JSX.Element => {
8 | if (typeof window !== "undefined") {
9 | localStorage.setItem("samayNewVisitor", JSON.stringify(false));
10 | }
11 |
12 | return (
13 | <>
14 |
15 | Samay — how-to
16 |
17 |
18 |
19 |
20 |
24 |
28 |
29 |
30 |
34 |
38 |
39 |
40 |
41 |
45 |
49 |
50 |
51 |
52 |
53 |
54 | Samay — free and open source group scheduling tool
55 |
56 |
57 | Find a time which works for everyone without the back-and-forth
58 | texts/emails!
59 |
60 |
61 |
62 |
63 |
64 | 1. Create a poll
65 |
66 |
67 | Select times you're free (click and drag), and optionally
68 | enter the title, description and location. No login required.
69 |
70 |
71 | The default poll type is "group" — to find a common time which
72 | works for everyone. If you want to have one-on-one meetings
73 | (parent-teacher meetings for example), select the "one-on-one"
74 | poll type.
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | 2. Share the poll
84 |
85 |
86 | Copy and share the poll link with the participants to let them
87 | mark their availability. No time zone confusion since Samay
88 | automatically shows participants times in their local time
89 | zone.
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | 3. Book the meeting
99 |
100 |
101 | In group polls, find the most popular times and see who's free
102 | with "yes" [
103 | ] votes or
104 | who can be with "if need be" [
105 | ] votes,
106 | book the meeting and share the final time with the
107 | participants! In one-on-one polls, find who has chosen which
108 | time slot for a one-on-one with you!
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | Create a poll
117 |
118 |
119 |
120 | >
121 | );
122 | };
123 |
124 | export default HowTo;
125 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { nanoid } from "nanoid";
2 | import Head from "next/head";
3 | import Router from "next/router";
4 | import { useState } from "react";
5 | import {
6 | Button,
7 | Col,
8 | Container,
9 | Form,
10 | Jumbotron,
11 | Row,
12 | Spinner,
13 | } from "react-bootstrap";
14 | import { ToastContainer, toast } from "react-toastify";
15 | import Layout from "../src/components/Layout";
16 | import SamayRBC from "../src/components/SamayRBC";
17 | import { encrypt } from "../src/helpers";
18 | import toastOptions from "../src/helpers/toastOptions";
19 | import { Poll, Time } from "../src/models/poll";
20 | import { createPoll } from "../src/utils/api/server";
21 |
22 | const Home = (): JSX.Element => {
23 | const [pollDetails, setPollDetails] = useState<{
24 | pollTitle: string;
25 | pollLocation: string;
26 | pollDescription: string;
27 | }>({
28 | pollTitle: "",
29 | pollLocation: "",
30 | pollDescription: "",
31 | });
32 |
33 | const [pollType, setPollType] = useState("group");
34 |
35 | const { pollTitle, pollLocation, pollDescription } = pollDetails;
36 |
37 | const [pollTimes, setTimes] = useState([]);
38 | const [disabled, setDisabled] = useState(false);
39 |
40 | const handlePollDetailsChange = (
41 | e: React.ChangeEvent
42 | ): void => {
43 | const { name, value } = e.target;
44 |
45 | setPollDetails({
46 | ...pollDetails,
47 | [name]: value,
48 | });
49 | };
50 |
51 | const handlePollTypeChange = (
52 | e: React.ChangeEvent
53 | ): void => {
54 | setPollType(e.target.value);
55 | };
56 |
57 | const handleSubmit = async (
58 | e: React.MouseEvent
59 | ): Promise => {
60 | e.preventDefault();
61 |
62 | if (!pollTimes || (pollTimes && pollTimes?.length < 2)) {
63 | toast.error("Please select at least two time slots", toastOptions);
64 | return;
65 | }
66 |
67 | const secret = nanoid(10);
68 | const encryptedSecret = encrypt(secret);
69 |
70 | const poll: Poll = {
71 | title: pollTitle,
72 | description: pollDescription,
73 | location: pollLocation,
74 | type: pollType,
75 | secret: encryptedSecret,
76 | times: pollTimes,
77 | };
78 |
79 | try {
80 | setDisabled(true);
81 |
82 | const createPollResponse = await createPoll({
83 | poll,
84 | });
85 |
86 | if (createPollResponse.statusCode === 201) {
87 | if (typeof window !== "undefined") {
88 | const samayCreatedPolls = localStorage.getItem("samayCreatedPolls");
89 |
90 | if (!samayCreatedPolls) {
91 | const initSamayCreatedPolls = {
92 | polls: [
93 | {
94 | [`${createPollResponse.data._id}-${pollTitle}`]: `${encryptedSecret}`,
95 | },
96 | ],
97 | };
98 |
99 | localStorage.setItem(
100 | "samayCreatedPolls",
101 | JSON.stringify(initSamayCreatedPolls)
102 | );
103 | } else {
104 | let samayCreatedPollsJSON = JSON.parse(samayCreatedPolls);
105 |
106 | samayCreatedPollsJSON.polls.push({
107 | [`${createPollResponse.data._id}-${pollTitle}`]: `${encryptedSecret}`,
108 | });
109 |
110 | localStorage.setItem(
111 | "samayCreatedPolls",
112 | JSON.stringify(samayCreatedPollsJSON)
113 | );
114 | }
115 | }
116 | Router.push(`/poll/${createPollResponse.data._id}/${secret}`);
117 | } else {
118 | setDisabled(false);
119 | toast.error(
120 | "Poll creation failed, please try again later",
121 | toastOptions
122 | );
123 | }
124 | } catch (err) {
125 | setDisabled(false);
126 | toast.error("Poll creation failed, please try again later", toastOptions);
127 | }
128 | };
129 |
130 | return (
131 | <>
132 |
133 | Samay — find a time which works for everyone
134 |
135 |
136 |
137 |
141 |
145 |
146 |
147 |
151 |
155 |
156 |
157 |
158 |
162 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
185 |
186 |
187 |
195 |
196 |
197 |
205 |
206 |
207 |
208 |
215 | Group poll
216 | One-on-one poll
217 |
218 |
219 |
220 |
221 |
226 | {!disabled ? (
227 | `Create`
228 | ) : (
229 | <>
230 |
238 | >
239 | )}
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 | >
249 | );
250 | };
251 |
252 | export default Home;
253 |
--------------------------------------------------------------------------------
/pages/poll/[id].tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Router from "next/router";
3 | import Head from "next/head";
4 | import { GetServerSideProps } from "next";
5 | import { Container, Jumbotron, Form } from "react-bootstrap";
6 | import dayjs from "dayjs";
7 | import localizedFormat from "dayjs/plugin/localizedFormat";
8 | import { getPoll } from "../../src/utils/api/server";
9 | import VoterPollInfo from "../../src/components/poll/VoterPollInfo";
10 | import PollTableVoter from "../../src/components/poll/PollTableVoter";
11 | import SubmitTimes from "../../src/components/poll/SubmitTimes";
12 | import Layout from "../../src/components/Layout";
13 | import { TimeFromDB, Vote, PollFromDB } from "../../src/models/poll";
14 | import { decrypt } from "../../src/helpers";
15 |
16 | dayjs.extend(localizedFormat);
17 |
18 | const Poll = (props: {
19 | pollFromDB: PollFromDB;
20 | pollID: string;
21 | }): JSX.Element => {
22 | const { pollFromDB, pollID } = props;
23 |
24 | let pageSection = <>>;
25 |
26 | const sortedTimes: TimeFromDB[] = pollFromDB.times.sort(
27 | (a: TimeFromDB, b: TimeFromDB) => a.start - b.start
28 | );
29 |
30 | const [newVote, setNewVote] = useState({
31 | name: "",
32 | times: [],
33 | });
34 |
35 | let showVoteRecordedGroup = false;
36 | let showVoteRecordedOneOnOne = false;
37 | let votedTimeOneOnOne = null;
38 |
39 | const handleNameChange = (e: React.ChangeEvent): void => {
40 | const { value } = e.target;
41 | setNewVote({ name: value, times: newVote.times });
42 | };
43 |
44 | if (typeof window !== "undefined") {
45 | let createdPollsFromLS = JSON.parse(
46 | localStorage.getItem("samayCreatedPolls")
47 | );
48 |
49 | if (createdPollsFromLS && createdPollsFromLS.polls.length) {
50 | const lSKeyForPoll = `${pollID}-${
51 | pollFromDB.title ? pollFromDB.title : ""
52 | }`;
53 |
54 | for (let i = 0; i < createdPollsFromLS.polls.length; i += 1) {
55 | let poll = createdPollsFromLS.polls[i];
56 |
57 | if (
58 | Object.keys(poll)[0] === lSKeyForPoll &&
59 | poll[Object.keys(poll)[0]] === pollFromDB.secret
60 | ) {
61 | Router.push(`/poll/${pollID}/${decrypt(pollFromDB.secret)}`);
62 | return <>>;
63 | }
64 | }
65 | }
66 |
67 | let votedPollsFromLS = JSON.parse(localStorage.getItem("samayVotedPolls"));
68 |
69 | if (votedPollsFromLS && votedPollsFromLS.polls.length) {
70 | for (let i = 0; i < votedPollsFromLS.polls.length; i += 1) {
71 | let poll = votedPollsFromLS.polls[i];
72 |
73 | if (Object.keys(poll)[0] === pollID && pollFromDB.open) {
74 | pageSection = <>>;
75 | if (!pollFromDB.type || pollFromDB.type === "group") {
76 | showVoteRecordedGroup = true;
77 | } else {
78 | votedTimeOneOnOne = JSON.parse(
79 | (Object.values(poll)[0] as string).split("#")[1]
80 | );
81 | showVoteRecordedOneOnOne = true;
82 | }
83 | break;
84 | } else if (pollFromDB.open) {
85 | pageSection = (
86 | <>
87 |
95 |
96 |
104 |
109 |
110 | >
111 | );
112 | }
113 | }
114 | } else if (pollFromDB.open) {
115 | pageSection = (
116 | <>
117 |
125 |
126 |
134 |
139 |
140 | >
141 | );
142 | }
143 | }
144 |
145 | return (
146 | <>
147 |
148 | Samay — mark your availability
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
164 |
165 | {pageSection}
166 |
167 |
168 |
169 | >
170 | );
171 | };
172 |
173 | export const getServerSideProps: GetServerSideProps = async (context) => {
174 | let pollID = null;
175 | if (context.params) {
176 | pollID = context.params.id;
177 | }
178 | const getPollResponse = await getPoll(pollID);
179 | const pollFromDB = getPollResponse.data;
180 |
181 | if (getPollResponse.statusCode === 404) {
182 | return {
183 | redirect: {
184 | destination: "/404",
185 | permanent: false,
186 | },
187 | };
188 | }
189 |
190 | return {
191 | props: { pollFromDB, pollID }, // will be passed to the page component as props
192 | };
193 | };
194 |
195 | export default Poll;
196 |
--------------------------------------------------------------------------------
/pages/poll/[id]/[secret].tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Head from "next/head";
3 | import { GetServerSideProps } from "next";
4 | import { Container, Jumbotron } from "react-bootstrap";
5 | import dayjs from "dayjs";
6 | import localizedFormat from "dayjs/plugin/localizedFormat";
7 | import { getPoll } from "../../../src/utils/api/server";
8 | import Layout from "../../../src/components/Layout";
9 | import AdminPollInfo from "../../../src/components/poll/AdminPollInfo";
10 | import PollTableAdmin from "../../../src/components/poll/PollTableAdmin";
11 | import SubmitFinalTime from "../../../src/components/poll/SubmitFinalTime";
12 | import { Time, TimeFromDB, PollFromDB } from "../../../src/models/poll";
13 | import { decrypt } from "../../../src/helpers";
14 |
15 | dayjs.extend(localizedFormat);
16 |
17 | const Poll = (props: {
18 | pollFromDB: PollFromDB;
19 | pollID: string;
20 | secret: string;
21 | }): JSX.Element => {
22 | const { pollFromDB, pollID, secret } = props;
23 | const sortedTimes: TimeFromDB[] = pollFromDB.times.sort(
24 | (a: TimeFromDB, b: TimeFromDB) => a.start - b.start
25 | );
26 | const [finalTime, setFinalTime] = useState();
27 |
28 | return (
29 | <>
30 |
31 | Samay — finalise time
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {pollFromDB.votes?.length > 0 && (
43 | <>
44 |
45 |
50 |
51 | {pollFromDB.open && (
52 |
58 | )}
59 | >
60 | )}
61 |
62 |
63 |
64 | >
65 | );
66 | };
67 |
68 | export const getServerSideProps: GetServerSideProps = async (context) => {
69 | let pollID = null;
70 | let secret = null;
71 | if (context.params) {
72 | pollID = context.params.id;
73 | secret = context.params.secret;
74 | }
75 | const getPollResponse = await getPoll(pollID);
76 | const pollFromDB = getPollResponse.data;
77 |
78 | if (getPollResponse.statusCode === 404) {
79 | return {
80 | redirect: {
81 | destination: "/404",
82 | permanent: false,
83 | },
84 | };
85 | }
86 |
87 | if (secret !== decrypt(pollFromDB.secret)) {
88 | return {
89 | redirect: {
90 | destination: `/poll/${pollID}`,
91 | permanent: false,
92 | },
93 | };
94 | }
95 |
96 | return {
97 | props: { pollFromDB, pollID, secret }, // will be passed to the page component as props
98 | };
99 | };
100 |
101 | export default Poll;
102 |
--------------------------------------------------------------------------------
/pages/privacy.tsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import { Container, Jumbotron } from "react-bootstrap";
3 | import Layout from "../src/components/Layout";
4 |
5 | const Privacy = (): JSX.Element => {
6 | return (
7 | <>
8 |
9 | Samay — privacy policy
10 |
11 |
12 |
13 |
14 |
18 |
22 |
23 |
24 |
28 |
32 |
33 |
34 |
35 |
39 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | Privacy Policy
50 | Last updated: Apr 2, 2024
51 |
52 |
53 |
54 | Hello! Welcome to Samay. Here's how we protect your data and
55 | respect your privacy.
56 |
57 | Our role in your privacy
58 |
59 | If you are creating a poll on Samay or voting on a poll, or just
60 | visiting our website, this policy applies to you.
61 |
62 | Your responsibilities
63 |
64 |
65 | Read this Privacy Policy.
66 | Do not impersonate anyone else on polls.
67 |
68 | By submitting your availability information for an event,
69 | you confirm that you have the right to authorise us to
70 | process it on your behalf in accordance with this Privacy
71 | Policy.
72 |
73 |
74 |
75 | What data we collect
76 |
77 | Information about events and your availability for those events
78 | which you enter in a poll.
79 |
80 | How and why we use your data
81 |
82 | Data protection law means that we can only use your data for
83 | certain reasons and where we have a legal basis to do so. We use
84 | the data solely to ensure the functionality of Samay — which is
85 | to help you quickly find the best time for meetings.
86 |
87 | Your rights
88 |
89 |
90 |
91 | You have the right to not provide us with event or personal
92 | availability information. If you choose to do this, you can
93 | continue browse this website's pages, but you won't be able
94 | to create polls or vote on polls.
95 |
96 |
97 | You have the right to turn off cookies in your browser by
98 | changing its settings. You can block cookies by activating a
99 | setting on your browser allowing you to refuse cookies. You
100 | can also delete cookies through your browser settings. If
101 | you turn off cookies, you can continue to use the website
102 | and browse its pages, but Samay's user experience would
103 | deteriorate.
104 |
105 |
106 | You have the right to access information we hold about you.
107 | We will provide you with the information within one month of
108 | your request, unless doing so would adversely affect the
109 | rights and freedoms of other (e.g. another person's personal
110 | or event details). We'll tell you if we can't meet your
111 | request for that reason.
112 |
113 |
114 | You have the right to make us correct any inaccurate
115 | personal data about you.
116 |
117 |
118 | You have the right to be 'forgotten' by us. You can do this
119 | by deleting the polls you created.
120 |
121 |
122 | You have the right to lodge a complaint regarding our use of
123 | your data. But please tell us first, so we have a chance to
124 | address your concerns.
125 |
126 |
127 |
128 | Where do we store the data?
129 |
130 | We use MongoDB's Atlas cloud database service to store all the
131 | events and your availability information.
132 |
133 | How long do we store your data?
134 |
135 | We will keep your polls indefinitely. You have the option to
136 | delete your polls from the recent polls page.
137 |
138 | Cookies
139 |
140 | We use cookies. Unless you adjust your browser settings to
141 | refuse cookies, we will issue cookies when you interact with
142 | Samay. These are 'persistent' cookies which do not delete
143 | themselves and help us recognise you when you return so we can
144 | help you manage your polls without asking you to create an
145 | account.
146 |
147 | How can I block cookies?
148 |
149 | You can block cookies by activating a setting on your browser
150 | allowing you to refuse the setting of cookies. You can also
151 | delete cookies through your browser settings. If you use your
152 | browser settings to disable, reject, or block cookies (including
153 | essential cookies), certain parts of our website's user
154 | experience would deteriorate.
155 |
156 | Children's privacy
157 |
158 | We do not address anyone under the age of 13. Personally
159 | identifiable information is not knowingly collected from
160 | children under 13. If discovered that a child under 13 has
161 | provided us with personal information, such information will be
162 | immediately deleted from the servers. If you are a parent or
163 | guardian and you are aware that your child has provided us with
164 | personal information, please contact us using the details below
165 | so that this information can be removed.
166 |
167 | Changes to this privacy policy
168 |
169 | This privacy policy may be updated from time to time. Thus, you
170 | are advised to review this page periodically for any changes.
171 |
172 | Contact us
173 |
174 | If you have any questions or suggestions about this Privacy
175 | Policy, do not hesitate to contact us at{" "}
176 |
177 | anandbaburajan@gmail.com
178 |
179 | .
180 |
181 |
182 |
183 | This privacy notice is based on an open-sourced design from{" "}
184 | Juro and{" "}
185 | Stefania Passera -
186 | get your own{" "}
187 |
188 | free privacy policy template
189 |
190 | .
191 |
192 |
193 |
194 |
195 |
196 | >
197 | );
198 | };
199 |
200 | export default Privacy;
201 |
--------------------------------------------------------------------------------
/pages/recent-polls.tsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import Link from "next/link";
3 | import Router from "next/router";
4 | import { useState } from "react";
5 | import { Button, Card, Container } from "react-bootstrap";
6 | import { Grid, Trash } from "react-bootstrap-icons";
7 | import Modal from "react-bootstrap/Modal";
8 | import Layout from "../src/components/Layout";
9 | import DeletePoll from "../src/components/poll/DeletePoll";
10 | import { decrypt } from "../src/helpers";
11 |
12 | const RemoveVotedPollModal = (props: {
13 | show;
14 | onHide;
15 | deleteVotedPoll;
16 | poll;
17 | }): JSX.Element => {
18 | const { deleteVotedPoll, poll } = props;
19 |
20 | return (
21 |
27 |
28 |
29 | Remove poll
30 |
31 |
32 |
33 | Are you sure you want to remove this poll?
34 |
35 |
36 | deleteVotedPoll(Object.keys(poll)[0])}>
37 | Remove
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | const RecentPolls = (): JSX.Element => {
45 | let createdPolls = [];
46 | let votedPolls = [];
47 |
48 | let pageSection = <>>;
49 |
50 | const [modalShow, setModalShow] = useState(false);
51 |
52 | const deleteVotedPoll = (pollID) => {
53 | if (typeof window !== "undefined") {
54 | const allVotedPolls = localStorage.getItem("samayVotedPolls");
55 |
56 | if (allVotedPolls) {
57 | const samayVotedPollsJSON = JSON.parse(allVotedPolls);
58 |
59 | let newSamayVotedPolls = {
60 | polls: samayVotedPollsJSON.polls.filter(
61 | (poll) => Object.keys(poll)[0] !== `${pollID}`
62 | ),
63 | };
64 |
65 | localStorage.setItem(
66 | "samayVotedPolls",
67 | JSON.stringify(newSamayVotedPolls)
68 | );
69 |
70 | Router.reload();
71 | }
72 | }
73 | };
74 |
75 | if (typeof window !== "undefined") {
76 | const createdPollsFromLS = localStorage.getItem("samayCreatedPolls");
77 |
78 | if (createdPollsFromLS) {
79 | const createdPollsFromLSJSON = JSON.parse(createdPollsFromLS);
80 |
81 | for (let i = 0; i < createdPollsFromLSJSON.polls.length; i += 1) {
82 | createdPolls.push(createdPollsFromLSJSON.polls[i]);
83 | }
84 | }
85 |
86 | let votedPollsFromLS = localStorage.getItem("samayVotedPolls");
87 |
88 | if (votedPollsFromLS) {
89 | const votedPollsFromLSJSON = JSON.parse(votedPollsFromLS);
90 |
91 | for (let i = 0; i < votedPollsFromLSJSON.polls.length; i += 1) {
92 | votedPolls.push(votedPollsFromLSJSON.polls[i]);
93 | }
94 | }
95 |
96 | const votedPollsClassName = `poll-container ${
97 | createdPolls.length > 0 ? "mt-5" : ""
98 | }`;
99 |
100 | if (createdPolls.length || votedPolls.length) {
101 | pageSection = (
102 |
179 | );
180 | } else {
181 | pageSection = (
182 |
183 |
184 |
185 | No recent polls
186 |
187 | Looks like you haven't created or voted on any polls
188 |
189 |
190 |
191 | Create a poll
192 |
193 |
194 |
195 |
196 | );
197 | }
198 | }
199 |
200 | return (
201 | <>
202 |
203 | Samay — recent polls
204 |
205 |
206 |
207 |
208 |
212 |
216 |
217 |
218 |
222 |
226 |
227 |
228 |
229 |
233 |
237 |
238 |
239 | {pageSection}
240 | >
241 | );
242 | };
243 |
244 | export default RecentPolls;
245 |
--------------------------------------------------------------------------------
/public/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anandbaburajan/samay/ae0c7716169723c5629f9a2c5cc007291126179a/public/.DS_Store
--------------------------------------------------------------------------------
/public/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anandbaburajan/samay/ae0c7716169723c5629f9a2c5cc007291126179a/public/banner.png
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
10 |
12 |
13 |
14 |
15 |
16 |
18 |
19 |
--------------------------------------------------------------------------------
/src/components/BottomNav.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | PlusCircle,
3 | Lock,
4 | Grid,
5 | Github,
6 | QuestionCircle,
7 | } from "react-bootstrap-icons";
8 | import Link from "next/link";
9 | import { useRouter } from "next/router";
10 |
11 | const BottomNav = (): JSX.Element => {
12 | const router = useRouter();
13 |
14 | return (
15 |
58 | );
59 | };
60 |
61 | export default BottomNav;
62 |
--------------------------------------------------------------------------------
/src/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | import NavBar from "./Navbar";
2 | import BottomNav from "./BottomNav";
3 |
4 | const Layout = ({ children }: { children: React.ReactNode }): JSX.Element => {
5 | return (
6 | <>
7 |
8 |
9 | {children}
10 |
11 |
12 | >
13 | );
14 | };
15 |
16 | export default Layout;
17 |
--------------------------------------------------------------------------------
/src/components/LogoSVG.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { SVGProps } from "react";
3 |
4 | const LogoSVG = (props: SVGProps) => (
5 |
11 |
16 |
27 |
28 |
29 |
30 |
31 |
37 |
38 | );
39 |
40 | export default LogoSVG;
41 |
--------------------------------------------------------------------------------
/src/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import { Container, Nav, Navbar } from "react-bootstrap";
2 | import {
3 | PlusCircle,
4 | Lock,
5 | Grid,
6 | Github,
7 | QuestionCircle,
8 | Heart,
9 | } from "react-bootstrap-icons";
10 | import Link from "next/link";
11 | import { useRouter } from "next/router";
12 | import { useState, useEffect } from "react";
13 | import LogoSVG from "./LogoSVG";
14 |
15 | const NavBar = (): JSX.Element => {
16 | const router = useRouter();
17 |
18 | const [samayNewVisitor, setSamayNewVisitor] = useState(false);
19 |
20 | useEffect(() => {
21 | if (typeof window !== "undefined") {
22 | setSamayNewVisitor(!localStorage.getItem("samayNewVisitor"));
23 | }
24 | }, [setSamayNewVisitor]);
25 |
26 | return (
27 |
28 |
29 |
30 |
31 | samay
32 |
33 |
37 |
38 |
39 |
40 |
41 | New poll
42 |
43 |
44 |
45 |
46 | Recent polls
47 |
48 |
49 | {router.pathname.match(/\//g).length === 2 ? (
50 | <>
51 |
52 |
57 | {" "}
62 | How-to
63 | {samayNewVisitor && }
64 |
65 |
66 | >
67 | ) : (
68 | <>
69 |
70 |
75 | {" "}
80 | How-to
81 | {samayNewVisitor && }
82 |
83 |
84 | >
85 | )}
86 |
87 |
88 | GitHub
89 |
90 |
91 |
92 |
93 | Privacy
94 |
95 |
96 |
97 |
98 | Buy me a coffee
99 |
100 |
101 |
102 |
103 |
104 |
105 | );
106 | };
107 |
108 | export default NavBar;
109 |
--------------------------------------------------------------------------------
/src/components/SamayRBC.tsx:
--------------------------------------------------------------------------------
1 | import { Calendar, Views, dayjsLocalizer } from "react-big-calendar";
2 | import dayjs from "dayjs";
3 | import { Time } from "../models/poll";
4 |
5 | const localizer = dayjsLocalizer(dayjs);
6 |
7 | const SamayRBC = (props: { pollTimes; setTimes }): JSX.Element => {
8 | const { pollTimes, setTimes } = props;
9 |
10 | const onTimesChange = ({ start, end }): void => {
11 | const newTime: Time = {
12 | start: start.getTime(),
13 | end: end.getTime(),
14 | };
15 |
16 | setTimes([...pollTimes, newTime]);
17 | };
18 |
19 | const onTimeRemove = ({ start, end }): void => {
20 | const newPollTimes = pollTimes.filter(
21 | (time) => !(time.start === start.getTime() && time.end === end.getTime())
22 | );
23 |
24 | setTimes([...newPollTimes]);
25 | };
26 |
27 | return (
28 | ({
31 | start: new Date(time.start),
32 | end: new Date(time.end),
33 | }))}
34 | localizer={localizer}
35 | onSelectSlot={onTimesChange}
36 | onSelectEvent={onTimeRemove}
37 | step={15}
38 | views={["week"]}
39 | selectable
40 | />
41 | );
42 | };
43 |
44 | export default SamayRBC;
45 |
--------------------------------------------------------------------------------
/src/components/copyText/CopyTextMain.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Form,
3 | InputGroup,
4 | Button,
5 | Popover,
6 | OverlayTrigger,
7 | } from "react-bootstrap";
8 | import { Files } from "react-bootstrap-icons";
9 | import dayjs from "dayjs";
10 | import localizedFormat from "dayjs/plugin/localizedFormat";
11 | import copy from "copy-to-clipboard";
12 | import { useState } from "react";
13 | import { PollFromDB } from "../../models/poll";
14 |
15 | dayjs.extend(localizedFormat);
16 |
17 | const NEXT_PUBLIC_BASE_URL =
18 | process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
19 |
20 | const CopyTextMain = (props: { poll: PollFromDB }): JSX.Element => {
21 | const { poll } = props;
22 |
23 | const pollTitle = poll.title;
24 | const pollURL = `${NEXT_PUBLIC_BASE_URL}/poll/${poll._id}`;
25 | const pollLocation = poll.location;
26 | const { finalTime } = poll;
27 |
28 | const finalPollTitle = pollTitle || "Untitled";
29 | const finalPollLocation = pollLocation ? `at ${pollLocation}` : "";
30 |
31 | let textToCopy: string;
32 |
33 | if (finalTime) {
34 | textToCopy = `"${finalPollTitle}": ${dayjs(finalTime?.start).format(
35 | "ddd"
36 | )}, ${dayjs(finalTime?.start).format("MMM")} ${dayjs(
37 | finalTime?.start
38 | ).format("DD")}, ${dayjs(finalTime?.start).format("LT")} - ${dayjs(
39 | finalTime?.end
40 | ).format("LT")} ${finalPollLocation}`;
41 | } else {
42 | textToCopy = pollURL;
43 | }
44 | const [show, setShow] = useState(false);
45 | const handleCopy = (): void => {
46 | setShow(true);
47 | copy(textToCopy);
48 | setTimeout(() => {
49 | setShow(false);
50 | }, 1000);
51 | };
52 |
53 | const popover = (
54 |
55 | Copied!
56 |
57 | );
58 |
59 | return (
60 |
61 |
62 |
63 |
69 |
70 |
76 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | );
89 | };
90 |
91 | export default CopyTextMain;
92 |
--------------------------------------------------------------------------------
/src/components/copyText/index.tsx:
--------------------------------------------------------------------------------
1 | import { Form } from "react-bootstrap";
2 | import { PollFromDB } from "../../models/poll";
3 | import CopyTextMain from "./CopyTextMain";
4 |
5 | const CopyText = (props: { poll: PollFromDB }): JSX.Element => {
6 | const { poll } = props;
7 |
8 | return (
9 |
10 |
17 |
18 | );
19 | };
20 | export default CopyText;
21 |
--------------------------------------------------------------------------------
/src/components/poll/AdminPollInfo.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "react-bootstrap";
2 | import {
3 | CalendarCheck,
4 | GeoAltFill,
5 | Globe,
6 | ShareFill,
7 | } from "react-bootstrap-icons";
8 | import dayjs from "dayjs";
9 | import localizedFormat from "dayjs/plugin/localizedFormat";
10 | import timezone from "dayjs/plugin/timezone";
11 | import { PollFromDB } from "../../models/poll";
12 | import CopyText from "../copyText";
13 |
14 | dayjs.extend(localizedFormat);
15 | dayjs.extend(timezone);
16 |
17 | const AdminPollInfo = (props: {
18 | poll: PollFromDB;
19 | showFinalTime: boolean;
20 | showCopyBox: boolean;
21 | }): JSX.Element => {
22 | const { poll, showFinalTime, showCopyBox } = props;
23 | return (
24 |
25 | {(!poll.type || poll.type === "group") && (
26 |
31 | {poll.open ? "Open" : "Closed"}
32 |
33 | )}
34 |
35 | {!poll.type || poll.type === "group" ? "Group poll" : "One-on-one poll"}
36 |
37 | {poll.title && (
38 |
43 | {poll.title}
44 |
45 | )}
46 | {!poll.title && (
47 |
52 | Untitled
53 |
54 | )}
55 | {poll.description && (
56 | {poll.description}
57 | )}
58 | {poll.location && (
59 |
60 |
61 | {poll.location}
62 |
63 | )}
64 |
65 |
66 | Times are shown in: {dayjs.tz.guess()} timezone
67 |
68 | {!poll.open && showFinalTime && (
69 |
70 |
71 | {dayjs(poll.finalTime?.start).format("ddd")},{" "}
72 | {dayjs(poll.finalTime?.start).format("MMM")}{" "}
73 | {dayjs(poll.finalTime?.start).format("DD")},{" "}
74 | {dayjs(poll.finalTime?.start).format("LT")} -{" "}
75 | {dayjs(poll.finalTime?.end).format("LT")}
76 |
77 | )}
78 | {showCopyBox && (
79 | <>
80 |
81 |
82 |
83 |
84 | >
85 | )}
86 | {showCopyBox && (
87 | <>
88 |
89 |
90 | Share this with the participants
91 |
92 | >
93 | )}
94 |
95 | );
96 | };
97 |
98 | export default AdminPollInfo;
99 |
--------------------------------------------------------------------------------
/src/components/poll/DeletePoll.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "react-bootstrap";
2 | import Modal from "react-bootstrap/Modal";
3 | import { ToastContainer, toast } from "react-toastify";
4 | import { Trash } from "react-bootstrap-icons";
5 | import { useState } from "react";
6 | import Router from "next/router";
7 | import toastOptions from "../../helpers/toastOptions";
8 | import { deletePoll } from "../../utils/api/server";
9 | import { encrypt } from "../../helpers";
10 |
11 | const DeleteModal = (props: { show; onHide; handleDelete }): JSX.Element => {
12 | const { handleDelete } = props;
13 |
14 | return (
15 |
21 |
22 |
23 | Delete poll
24 |
25 |
26 |
27 | Are you sure you want to delete this poll?
28 |
29 |
30 | Delete
31 |
32 |
33 | );
34 | };
35 |
36 | const DeletePoll = (props: {
37 | pollID: string;
38 | pollTitle: string;
39 | secret: string;
40 | }): JSX.Element => {
41 | const { pollID, pollTitle, secret } = props;
42 | const [modalShow, setModalShow] = useState(false);
43 |
44 | const handleDelete = async (
45 | e: React.MouseEvent
46 | ): Promise => {
47 | e.preventDefault();
48 | try {
49 | let deletePollResponse;
50 | const deleteArgs = {
51 | pollID,
52 | secret: encrypt(secret),
53 | };
54 | deletePollResponse = await deletePoll(deleteArgs);
55 | if (
56 | deletePollResponse &&
57 | (deletePollResponse.statusCode === 200 ||
58 | deletePollResponse.statusCode === 404)
59 | ) {
60 | if (typeof window !== "undefined") {
61 | const samayCreatedPolls = localStorage.getItem("samayCreatedPolls");
62 |
63 | if (samayCreatedPolls) {
64 | const samayCreatedPollsJSON = JSON.parse(samayCreatedPolls);
65 |
66 | let newSamayCreatedPolls = {
67 | polls: samayCreatedPollsJSON.polls.filter(
68 | (poll) => Object.keys(poll)[0] !== `${pollID}-${pollTitle}`
69 | ),
70 | };
71 |
72 | localStorage.setItem(
73 | "samayCreatedPolls",
74 | JSON.stringify(newSamayCreatedPolls)
75 | );
76 | }
77 | }
78 | Router.push("/recent-polls");
79 | } else {
80 | toast.info("Please try again later", toastOptions);
81 | Router.reload();
82 | }
83 | } catch (err) {
84 | toast.info("Please try again later", toastOptions);
85 | }
86 | };
87 |
88 | return (
89 | <>
90 | {
93 | e.stopPropagation;
94 | e.preventDefault();
95 | setModalShow(true);
96 | }}
97 | >
98 |
99 |
100 | setModalShow(false)}
103 | handleDelete={handleDelete}
104 | />
105 |
106 | >
107 | );
108 | };
109 |
110 | export default DeletePoll;
111 |
--------------------------------------------------------------------------------
/src/components/poll/MarkFinalTime.tsx:
--------------------------------------------------------------------------------
1 | import { Form } from "react-bootstrap";
2 | import { Dispatch } from "react";
3 | import { Time } from "../../models/poll";
4 |
5 | const MarkFinalTime = (props: {
6 | times: Time[];
7 | setFinalTime: Dispatch;
8 | }): JSX.Element => {
9 | const { times, setFinalTime } = props;
10 |
11 | const handleTimeChange = (e: React.ChangeEvent): void => {
12 | const { dataset, checked } = e.target;
13 | const time: Time = dataset.value ? JSON.parse(dataset.value) : {};
14 | if (checked) {
15 | setFinalTime(time);
16 | }
17 | };
18 |
19 | return (
20 |
21 | Final time
22 | {times.map((time) => (
23 |
27 |
34 |
35 | ))}
36 |
37 | );
38 | };
39 |
40 | export default MarkFinalTime;
41 |
--------------------------------------------------------------------------------
/src/components/poll/MarkTimes.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, useState } from "react";
2 | import { CheckCircleFill, CircleFill } from "react-bootstrap-icons";
3 | import { Time, Vote } from "../../models/poll";
4 |
5 | const MarkTimes = (props: {
6 | times: Time[];
7 | newVote: Vote;
8 | setNewVote: Dispatch;
9 | }): JSX.Element => {
10 | const { times, newVote, setNewVote } = props;
11 |
12 | const [timeBoxStatus, setTimeBoxStatus] = useState>(
13 | times.reduce(
14 | (obj, cur) => ({
15 | ...obj,
16 | [JSON.stringify({ start: cur.start, end: cur.end })]: 0,
17 | }),
18 | {}
19 | )
20 | );
21 |
22 | const statusValues = ["no", "yes", "if-need-be"];
23 |
24 | const handleMarkTimeBoxClick = (e: React.MouseEvent): void => {
25 | if (e.target !== e.currentTarget) return;
26 |
27 | const time = JSON.parse((e.target as HTMLElement).id);
28 |
29 | const newTimeBoxStatus =
30 | (timeBoxStatus[JSON.stringify({ start: time.start, end: time.end })] +
31 | 1) %
32 | 3;
33 |
34 | setTimeBoxStatus((prev) => ({
35 | ...prev,
36 | [JSON.stringify({ start: time.start, end: time.end })]: newTimeBoxStatus,
37 | }));
38 |
39 | let newTimes = newVote.times;
40 |
41 | if (newTimeBoxStatus === 1) {
42 | // yes
43 | newTimes = newTimes.filter(
44 | (item) => item.start !== time.start || item.end !== time.end
45 | );
46 | newTimes.push(time);
47 | setNewVote({ name: newVote.name, times: newTimes });
48 | } else if (newTimeBoxStatus === 2) {
49 | // if-need-be
50 | newTimes = newTimes.filter(
51 | (item) => item.start !== time.start || item.end !== time.end
52 | );
53 | time.ifNeedBe = true;
54 | newTimes.push(time);
55 | setNewVote({ name: newVote.name, times: newTimes });
56 | } else {
57 | // no
58 | newTimes = newTimes.filter(
59 | (item) => item.start !== time.start || item.end !== time.end
60 | );
61 | setNewVote({ name: newVote.name, times: newTimes });
62 | }
63 | };
64 |
65 | return (
66 |
67 | {times.map((time) => (
68 |
69 |
81 | {timeBoxStatus[
82 | JSON.stringify({ start: time.start, end: time.end })
83 | ] === 1 && (
84 |
85 | )}
86 | {timeBoxStatus[
87 | JSON.stringify({ start: time.start, end: time.end })
88 | ] === 2 && (
89 |
90 | )}
91 |
92 |
93 | ))}
94 |
95 | );
96 | };
97 |
98 | export default MarkTimes;
99 |
--------------------------------------------------------------------------------
/src/components/poll/MarkTimesOneOnOne.tsx:
--------------------------------------------------------------------------------
1 | import { Form } from "react-bootstrap";
2 | import { Dispatch, useState } from "react";
3 | import { Time, Vote, PollFromDB } from "../../models/poll";
4 | import { isTimePresentInPollTimes } from "../../helpers";
5 |
6 | const MarkTimesOneOnOne = (props: {
7 | times: Time[];
8 | newVote: Vote;
9 | poll: PollFromDB;
10 | setNewVote: Dispatch;
11 | }): JSX.Element => {
12 | const { times, newVote, poll, setNewVote } = props;
13 |
14 | let availableTimes = [];
15 |
16 | let VotedTimes = poll.votes.map((vote) => vote.times[0]);
17 |
18 | poll.times.map((time) => {
19 | if (!isTimePresentInPollTimes(time, VotedTimes)) {
20 | availableTimes.push(time);
21 | }
22 | });
23 |
24 | const [radioButtonChecked, setRadioButtonChecked] = useState("");
25 |
26 | const handleMarkTimeRadioButton = (
27 | e: React.ChangeEvent
28 | ): void => {
29 | if (e.target !== e.currentTarget) return;
30 | const { dataset, checked } = e.target;
31 | const time: Time = dataset.value ? JSON.parse(dataset.value) : {};
32 | let newTimes = [];
33 |
34 | if (checked) {
35 | setRadioButtonChecked(JSON.stringify(time));
36 | newTimes.push(time);
37 | setNewVote({ name: newVote.name, times: newTimes });
38 | }
39 | };
40 |
41 | return (
42 |
43 | {availableTimes.map((time) => (
44 |
48 |
56 |
57 | ))}
58 |
59 | );
60 | };
61 |
62 | export default MarkTimesOneOnOne;
63 |
--------------------------------------------------------------------------------
/src/components/poll/PollDateTime.tsx:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import localizedFormat from "dayjs/plugin/localizedFormat";
3 | import { Time } from "../../models/poll";
4 | import { isDayAndMonthSame } from "../../helpers";
5 |
6 | dayjs.extend(localizedFormat);
7 |
8 | const PollDateTime = (props: {
9 | time: Time;
10 | type: string;
11 | index: number;
12 | times: Time[];
13 | }): JSX.Element => {
14 | const { time, type, index, times } = props;
15 |
16 | let topComponent = <>>;
17 |
18 | if (index === 0) {
19 | topComponent = (
20 | <>
21 |
22 | {dayjs(time.start).format("ddd")}
23 |
24 | {dayjs(time.start).format("D")}
25 | {dayjs(time.start).format("MMM")}
26 | >
27 | );
28 | } else if (!isDayAndMonthSame(time, times[index - 1])) {
29 | topComponent = (
30 | <>
31 |
32 | {dayjs(time.start).format("ddd")}
33 |
34 | {dayjs(time.start).format("D")}
35 | {dayjs(time.start).format("MMM")}
36 | >
37 | );
38 | } else if (
39 | index + 1 >= times.length ||
40 | !isDayAndMonthSame(time, times[index + 1])
41 | ) {
42 | topComponent =
;
43 | } else {
44 | topComponent =
;
45 | }
46 |
47 | return (
48 |
49 | {topComponent}
50 |
51 |
52 |
59 | [
60 |
61 |
62 |
63 |
64 | {dayjs(time.start).format("LT")}
65 |
66 |
67 | {dayjs(time.end).format("LT")}
68 |
69 |
70 |
71 |
72 | );
73 | };
74 |
75 | export default PollDateTime;
76 |
--------------------------------------------------------------------------------
/src/components/poll/PollTableAdmin.tsx:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import localizedFormat from "dayjs/plugin/localizedFormat";
3 | import { Dispatch } from "react";
4 | import { Table } from "react-bootstrap";
5 | import { CheckCircleFill, CircleFill, PersonFill } from "react-bootstrap-icons";
6 | import {
7 | isTimeIfNeedBe,
8 | isTimePresentInPollTimes,
9 | slotCheckClassName,
10 | } from "../../helpers";
11 | import { PollFromDB, Time, Vote } from "../../models/poll";
12 | import MarkFinalTime from "./MarkFinalTime";
13 | import PollDateTime from "./PollDateTime";
14 |
15 | dayjs.extend(localizedFormat);
16 |
17 | const PollTableAdmin = (props: {
18 | pollFromDB: PollFromDB;
19 | sortedTimes: Time[];
20 | setFinalTime: Dispatch;
21 | }): JSX.Element => {
22 | const { pollFromDB, sortedTimes, setFinalTime } = props;
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
30 | {sortedTimes.map((time, i) => (
31 |
32 |
38 |
39 | ))}
40 |
41 |
42 |
43 | {pollFromDB.open &&
44 | (!pollFromDB.type || pollFromDB.type === "group") && (
45 |
46 | )}
47 |
48 |
49 | {pollFromDB.type === "group"
50 | ? pollFromDB.votes?.length + 1
51 | : pollFromDB.votes?.length}
52 |
53 | {pollFromDB.type === "oneonone" && pollFromDB.votes?.length === 1
54 | ? "PARTICIPANT"
55 | : "PARTICIPANTS"}
56 |
57 | {sortedTimes.map((time: Time) => (
58 |
59 | {pollFromDB.votes?.filter((vote: Vote) =>
60 | isTimePresentInPollTimes(time, vote.times)
61 | ).length !== 0 && (
62 |
63 |
64 | {pollFromDB.type === "group"
65 | ? pollFromDB.votes?.filter((vote: Vote) =>
66 | isTimePresentInPollTimes(time, vote.times)
67 | ).length + 1
68 | : pollFromDB.votes?.filter((vote: Vote) =>
69 | isTimePresentInPollTimes(time, vote.times)
70 | ).length}
71 |
72 | )}
73 |
74 | ))}
75 |
76 | {pollFromDB.votes?.map((vote: Vote, idx: number) => (
77 |
78 | {vote.name}
79 | {sortedTimes.map((time: Time) => (
80 |
84 | {isTimeIfNeedBe(time, vote.times) ? (
85 |
86 | ) : (
87 |
88 | )}
89 |
90 | ))}
91 |
92 | ))}
93 | {pollFromDB.type === "group" && (
94 |
95 | You
96 | {sortedTimes.map((time: Time) => (
97 |
98 |
99 |
100 | ))}
101 |
102 | )}
103 |
104 |
105 |
106 | );
107 | };
108 |
109 | export default PollTableAdmin;
110 |
--------------------------------------------------------------------------------
/src/components/poll/PollTableVoter.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch } from "react";
2 | import { Table } from "react-bootstrap";
3 | import dayjs from "dayjs";
4 | import localizedFormat from "dayjs/plugin/localizedFormat";
5 | import MarkTimes from "./MarkTimes";
6 | import MarkTimesOneOnOne from "./MarkTimesOneOnOne";
7 | import PollDateTime from "./PollDateTime";
8 | import { Time, PollFromDB, Vote } from "../../models/poll";
9 | import { isTimePresentInPollTimes } from "../../helpers";
10 |
11 | dayjs.extend(localizedFormat);
12 |
13 | const PollTableVoter = (props: {
14 | pollFromDB: PollFromDB;
15 | sortedTimes: Time[];
16 | newVote: Vote;
17 | setNewVote: Dispatch;
18 | }): JSX.Element => {
19 | const { pollFromDB, sortedTimes, newVote, setNewVote } = props;
20 |
21 | let availableTimes = [];
22 | let votedTimes = pollFromDB.votes.map((vote) => vote.times[0]);
23 |
24 | pollFromDB.times.map((time) => {
25 | if (!isTimePresentInPollTimes(time, votedTimes)) {
26 | availableTimes.push(time);
27 | }
28 | });
29 |
30 | return (
31 |
32 |
33 |
34 |
35 | {(!pollFromDB.type || pollFromDB.type === "group") &&
36 | sortedTimes.map((time, i) => (
37 |
38 |
44 |
45 | ))}
46 | {pollFromDB.type === "oneonone" &&
47 | availableTimes.map((time, i) => (
48 |
49 |
55 |
56 | ))}
57 |
58 |
59 |
60 | {pollFromDB.open &&
61 | (!pollFromDB.type || pollFromDB.type === "group") && (
62 |
67 | )}
68 | {pollFromDB.open && pollFromDB.type === "oneonone" && (
69 |
75 | )}
76 |
77 |
78 |
79 | );
80 | };
81 |
82 | export default PollTableVoter;
83 |
--------------------------------------------------------------------------------
/src/components/poll/SubmitFinalTime.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Spinner } from "react-bootstrap";
2 | import { useState } from "react";
3 | import Router from "next/router";
4 | import { ToastContainer, toast } from "react-toastify";
5 | import toastOptions from "../../helpers/toastOptions";
6 | import { markFinalTime } from "../../utils/api/server";
7 | import { Time, PollFromDB } from "../../models/poll";
8 | import { encrypt } from "../../helpers";
9 |
10 | const SubmitFinalTime = (props: {
11 | finalTime: Time | undefined;
12 | pollID: string;
13 | secret: string;
14 | poll: PollFromDB;
15 | }): JSX.Element => {
16 | const { finalTime, pollID, secret, poll } = props;
17 |
18 | const [disabled, setDisabled] = useState(false);
19 |
20 | const handleSubmit = async (
21 | e: React.MouseEvent
22 | ): Promise => {
23 | e.preventDefault();
24 | if (finalTime && (!poll.type || poll.type === "group")) {
25 | setDisabled(true);
26 | try {
27 | const voterArgs = {
28 | finalTime: {
29 | finalTime,
30 | open: false,
31 | },
32 | pollID,
33 | secret: encrypt(secret),
34 | };
35 | const submitFinalTimeResponse = await markFinalTime(voterArgs);
36 | if (submitFinalTimeResponse.statusCode === 201) {
37 | Router.reload();
38 | } else {
39 | setDisabled(false);
40 | toast.info("Please try again later", toastOptions);
41 | Router.reload();
42 | }
43 | } catch (err) {
44 | setDisabled(false);
45 | toast.info("Please try again later", toastOptions);
46 | Router.reload();
47 | }
48 | } else if (!poll.type || poll.type === "group") {
49 | toast.error("Please choose the final time", toastOptions);
50 | } else {
51 | setDisabled(true);
52 | }
53 | };
54 |
55 | if (!poll.type || poll.type === "group") {
56 | return (
57 | <>
58 |
64 | {!disabled ? (
65 | `Finalise time`
66 | ) : (
67 | <>
68 |
76 | >
77 | )}
78 |
79 |
80 | >
81 | );
82 | }
83 | return ;
84 | };
85 |
86 | export default SubmitFinalTime;
87 |
--------------------------------------------------------------------------------
/src/components/poll/SubmitTimes.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Spinner } from "react-bootstrap";
2 | import { useState } from "react";
3 | import Router from "next/router";
4 | import { ToastContainer, toast } from "react-toastify";
5 | import toastOptions from "../../helpers/toastOptions";
6 | import { markTimes } from "../../utils/api/server";
7 | import { Vote, PollFromDB } from "../../models/poll";
8 | import { isUserPresentInVotes } from "../../helpers";
9 |
10 | const SubmitTimes = (props: {
11 | newVote: Vote;
12 | pollID: string;
13 | pollFromDB: PollFromDB;
14 | }): JSX.Element => {
15 | const { newVote, pollID, pollFromDB } = props;
16 |
17 | const [disabled, setDisabled] = useState(false);
18 |
19 | const handleSubmit = async (
20 | e: React.MouseEvent
21 | ): Promise => {
22 | e.preventDefault();
23 |
24 | if (!newVote.name) {
25 | toast.error("Please enter your name", toastOptions);
26 | return;
27 | }
28 |
29 | if (
30 | pollFromDB.votes &&
31 | isUserPresentInVotes(newVote.name, pollFromDB.votes)
32 | ) {
33 | toast.error(
34 | "An invitee with the same name has voted before - please choose a different name",
35 | toastOptions
36 | );
37 |
38 | return;
39 | }
40 |
41 | if (newVote.times.length === 0) {
42 | toast.error("Please select at least one time slot", toastOptions);
43 |
44 | return;
45 | }
46 |
47 | setDisabled(true);
48 | try {
49 | let submitTimeResponse;
50 | const voterArgs = {
51 | newVote,
52 | pollID,
53 | };
54 | submitTimeResponse = await markTimes(voterArgs);
55 | let time = JSON.stringify(newVote.times[0]);
56 |
57 | if (submitTimeResponse && submitTimeResponse.statusCode === 201) {
58 | if (typeof window !== "undefined") {
59 | const votedPolls = localStorage.getItem("samayVotedPolls");
60 |
61 | if (!votedPolls) {
62 | if (pollFromDB.type === "oneonone") {
63 | const initSamayPolls = {
64 | polls: [
65 | {
66 | [`${pollID}`]: `${pollFromDB.title}#${time}`,
67 | },
68 | ],
69 | };
70 |
71 | localStorage.setItem(
72 | "samayVotedPolls",
73 | JSON.stringify(initSamayPolls)
74 | );
75 | } else {
76 | const initSamayPolls = {
77 | polls: [
78 | {
79 | [`${pollID}`]: `${pollFromDB.title}`,
80 | },
81 | ],
82 | };
83 | localStorage.setItem(
84 | "samayVotedPolls",
85 | JSON.stringify(initSamayPolls)
86 | );
87 | }
88 | } else if (pollFromDB.type === "oneonone") {
89 | const votedPollsJSON = JSON.parse(votedPolls);
90 |
91 | votedPollsJSON.polls.push({
92 | [`${pollID}`]: `${pollFromDB.title}#${time}`,
93 | });
94 |
95 | localStorage.setItem(
96 | "samayVotedPolls",
97 | JSON.stringify(votedPollsJSON)
98 | );
99 | } else {
100 | const votedPollsJSON = JSON.parse(votedPolls);
101 |
102 | votedPollsJSON.polls.push({
103 | [`${pollID}`]: `${pollFromDB.title}`,
104 | });
105 |
106 | localStorage.setItem(
107 | "samayVotedPolls",
108 | JSON.stringify(votedPollsJSON)
109 | );
110 | }
111 | }
112 | Router.reload();
113 | } else if (submitTimeResponse && submitTimeResponse.statusCode === 404) {
114 | toast.error("The poll has been deleted by the creator", toastOptions);
115 | Router.push("/");
116 | } else if (submitTimeResponse && submitTimeResponse.statusCode === 400) {
117 | toast.error("The poll has been closed by the creator", toastOptions);
118 | Router.reload();
119 | } else {
120 | setDisabled(false);
121 | toast.info("Please try again later", toastOptions);
122 | Router.reload();
123 | }
124 | } catch (err) {
125 | setDisabled(false);
126 | toast.info("Please try again later", toastOptions);
127 | }
128 | };
129 |
130 | return (
131 | <>
132 |
138 | {!disabled ? (
139 | `Mark your availability`
140 | ) : (
141 | <>
142 |
150 | >
151 | )}
152 |
153 |
154 | >
155 | );
156 | };
157 |
158 | export default SubmitTimes;
159 |
--------------------------------------------------------------------------------
/src/components/poll/VoterPollInfo.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "react-bootstrap";
2 | import {
3 | CalendarCheck,
4 | CheckCircleFill,
5 | GeoAltFill,
6 | Globe,
7 | } from "react-bootstrap-icons";
8 | import dayjs from "dayjs";
9 | import localizedFormat from "dayjs/plugin/localizedFormat";
10 | import timezone from "dayjs/plugin/timezone";
11 | import { PollFromDB, Time } from "../../models/poll";
12 |
13 | dayjs.extend(localizedFormat);
14 | dayjs.extend(timezone);
15 |
16 | const PollInfo = (props: {
17 | poll: PollFromDB;
18 | showFinalTime: boolean;
19 | showVoteRecordedGroup: boolean;
20 | showVoteRecordedOneOnOne: boolean;
21 | votedTimeOneOnOne: Time;
22 | }): JSX.Element => {
23 | const {
24 | poll,
25 | showFinalTime,
26 | showVoteRecordedGroup,
27 | showVoteRecordedOneOnOne,
28 | votedTimeOneOnOne,
29 | } = props;
30 |
31 | return (
32 |
33 | {(!poll.type || poll.type === "group") && (
34 |
39 | {poll.open ? "Open" : "Closed"}
40 |
41 | )}
42 |
43 | {!poll.type || poll.type === "group" ? "Group" : "One-on-one"}
44 |
45 | {poll.title && (
46 |
51 | {poll.title}
52 |
53 | )}
54 | {!poll.title && (
55 |
60 | Untitled
61 |
62 | )}
63 | {poll.description && (
64 | {poll.description}
65 | )}
66 | {poll.location && (
67 |
68 |
69 | {poll.location}
70 |
71 | )}
72 |
73 |
74 | Times are shown in: {dayjs.tz.guess()} timezone
75 |
76 | {showFinalTime && (
77 |
78 |
79 | {dayjs(poll.finalTime?.start).format("ddd")},{" "}
80 | {dayjs(poll.finalTime?.start).format("MMM")}{" "}
81 | {dayjs(poll.finalTime?.start).format("DD")},{" "}
82 | {dayjs(poll.finalTime?.start).format("LT")} -{" "}
83 | {dayjs(poll.finalTime?.end).format("LT")}
84 |
85 | )}
86 | {showVoteRecordedGroup && !showFinalTime && (
87 |
88 |
89 | Your vote has been successfully recorded.
90 |
91 | )}
92 | {showVoteRecordedOneOnOne && votedTimeOneOnOne && !showFinalTime && (
93 |
94 |
95 | Your vote for the time {dayjs(votedTimeOneOnOne.start).format(
96 | "LT"
97 | )} - {dayjs(votedTimeOneOnOne.end).format("LT")},{" "}
98 | {dayjs(votedTimeOneOnOne.start).format("ddd")},{" "}
99 | {dayjs(votedTimeOneOnOne.start).format("MMM")}{" "}
100 | {dayjs(votedTimeOneOnOne.start).format("DD")}, has been successfully
101 | recorded.
102 |
103 | )}
104 |
105 | );
106 | };
107 |
108 | export default PollInfo;
109 |
--------------------------------------------------------------------------------
/src/helpers/index.ts:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 | import dayjs from "dayjs";
3 | import localizedFormat from "dayjs/plugin/localizedFormat";
4 | import { Time, VoteFromDB } from "../models/poll";
5 |
6 | dayjs.extend(localizedFormat);
7 |
8 | export const isTimePresentInPollTimes = (
9 | timeToSearch: Time,
10 | times: Time[]
11 | ): boolean => {
12 | return times.some(
13 | (time) => time.start === timeToSearch.start && time.end === timeToSearch.end
14 | );
15 | };
16 |
17 | export const slotCheckClassName = (time: Time, times: Time[]): string => {
18 | if (isTimePresentInPollTimes(time, times)) {
19 | if (
20 | times.find(
21 | (currentTime) =>
22 | currentTime.start === time.start && currentTime.end === time.end
23 | )?.ifNeedBe
24 | )
25 | return "poll-slot-checked-if-need-be";
26 | return "poll-slot-checked";
27 | }
28 | return "poll-slot-unchecked";
29 | };
30 |
31 | export const isTimeIfNeedBe = (time: Time, times: Time[]): boolean => {
32 | if (isTimePresentInPollTimes(time, times)) {
33 | if (
34 | times.find(
35 | (currentTime) =>
36 | currentTime.start === time.start && currentTime.end === time.end
37 | )?.ifNeedBe
38 | )
39 | return true;
40 | return false;
41 | }
42 | return false;
43 | };
44 |
45 | export const slotTimeClassName = (
46 | time: Time,
47 | voteTimes: Time[],
48 | finalTime?: Time
49 | ): string => {
50 | if (time.start === finalTime?.start && time.end === finalTime?.end)
51 | return "slot-time slot-final-time";
52 |
53 | if (isTimePresentInPollTimes(time, voteTimes)) {
54 | if (
55 | voteTimes.find(
56 | (currentTime) =>
57 | currentTime.start === time.start && currentTime.end === time.end
58 | )?.ifNeedBe
59 | )
60 | return "slot-time slot-if-need-be-time";
61 | return "slot-time slot-normal-time";
62 | }
63 | return "slot-time";
64 | };
65 |
66 | export const isUserPresentInVotes = (
67 | userToSearch: string,
68 | votes: VoteFromDB[]
69 | ): boolean => {
70 | return votes.some((vote) => vote.name === userToSearch);
71 | };
72 |
73 | const ENCRYPTION_KEY = process.env.NEXT_PUBLIC_ENCRYPTION_KEY || "";
74 | const ENCRYPTION_IV = process.env.NEXT_PUBLIC_ENCRYPTION_IV || "";
75 |
76 | export const encrypt = (text: string): string => {
77 | let cipher = crypto.createCipheriv(
78 | "aes-256-cbc",
79 | Buffer.from(ENCRYPTION_KEY),
80 | ENCRYPTION_IV
81 | );
82 | let encrypted = cipher.update(text);
83 | encrypted = Buffer.concat([encrypted, cipher.final()]);
84 | return encrypted.toString("hex");
85 | };
86 |
87 | export const decrypt = (text: string): string => {
88 | const encryptedText = Buffer.from(text, "hex");
89 | const decipher = crypto.createDecipheriv(
90 | "aes-256-cbc",
91 | Buffer.from(ENCRYPTION_KEY),
92 | ENCRYPTION_IV
93 | );
94 | let decrypted = decipher.update(encryptedText);
95 | decrypted = Buffer.concat([decrypted, decipher.final()]);
96 | return decrypted.toString();
97 | };
98 |
99 | export const isDayAndMonthSame = (
100 | firstTime: Time,
101 | secondTime: Time
102 | ): boolean => {
103 | if (
104 | dayjs(firstTime.start).format("D") ===
105 | dayjs(secondTime.start).format("D") &&
106 | dayjs(firstTime.start).format("MMM") ===
107 | dayjs(secondTime.start).format("MMM")
108 | ) {
109 | return true;
110 | }
111 | return false;
112 | };
113 |
--------------------------------------------------------------------------------
/src/helpers/toastOptions.ts:
--------------------------------------------------------------------------------
1 | import { ToastOptions } from "react-toastify";
2 |
3 | export default {
4 | position: "top-right",
5 | autoClose: 3000,
6 | hideProgressBar: false,
7 | closeOnClick: true,
8 | pauseOnHover: true,
9 | draggable: true,
10 | progress: undefined,
11 | theme: "light",
12 | } as ToastOptions;
--------------------------------------------------------------------------------
/src/models/poll.ts:
--------------------------------------------------------------------------------
1 | import mongoose, { model, Model, Document, Schema } from "mongoose";
2 |
3 | export interface Time {
4 | start: number;
5 | end: number;
6 | ifNeedBe?: boolean;
7 | }
8 |
9 | export interface TimeFromDB {
10 | _id: string;
11 | start: number;
12 | end: number;
13 | ifNeedBe?: boolean;
14 | }
15 |
16 | export interface Vote {
17 | name: string;
18 | times: Time[];
19 | }
20 |
21 | export interface VoteFromDB {
22 | _id: string;
23 | name: string;
24 | times: TimeFromDB[];
25 | }
26 |
27 | export interface Poll {
28 | title?: string;
29 | description?: string;
30 | open?: boolean;
31 | secret: string;
32 | location?: string;
33 | type?: string;
34 | times: Time[];
35 | finalTime?: Time;
36 | votes?: Vote[];
37 | }
38 |
39 | export interface PollFromDB {
40 | _id: string;
41 | title?: string;
42 | description?: string;
43 | open?: boolean;
44 | secret: string;
45 | location?: string;
46 | type?: string;
47 | times: TimeFromDB[];
48 | finalTime?: TimeFromDB;
49 | votes?: VoteFromDB[];
50 | createdAt: string;
51 | updatedAt: string;
52 | __v: number;
53 | }
54 |
55 | export interface PollDoc extends Document {
56 | title?: string;
57 | description?: string;
58 | open?: boolean;
59 | secret: string;
60 | location?: string;
61 | type?: string;
62 | times: Time[];
63 | finalTime?: Time;
64 | votes?: Vote[];
65 | }
66 |
67 | const PollSchema: Schema = new Schema(
68 | {
69 | title: { type: String },
70 | description: { type: String },
71 | open: { type: Boolean, default: true },
72 | secret: { type: String, required: true },
73 | location: { type: String },
74 | type: { type: String },
75 | times: {
76 | type: [{ start: Number, end: Number }],
77 | required: true,
78 | },
79 | finalTime: { type: { start: Number, end: Number } },
80 | votes: [
81 | {
82 | name: String,
83 | times: [{ start: Number, end: Number, ifNeedBe: Boolean }],
84 | },
85 | ],
86 | },
87 | { timestamps: true }
88 | );
89 |
90 | const SamayPoll: Model =
91 | mongoose.models.Poll || model("Poll", PollSchema);
92 |
93 | export interface HttpResponse {
94 | data: any;
95 | statusCode: number;
96 | }
97 |
98 | export default SamayPoll;
99 |
--------------------------------------------------------------------------------
/src/styles/bottom-nav.scss:
--------------------------------------------------------------------------------
1 | .bottom-nav {
2 | align-items: center;
3 | padding-top: 0.5rem;
4 | padding-bottom: 0.5rem;
5 | background-color: #fafafa;
6 | padding-right: 1.5rem;
7 | padding-left: 1.5rem;
8 | height: 2.5rem;
9 | position: fixed;
10 | overflow: hidden;
11 | text-align-last: justify;
12 | bottom: 0;
13 | width: 100%;
14 | display: flex;
15 | column-gap: 20px;
16 | margin-left: auto;
17 | margin-right: auto;
18 | justify-content: space-between;
19 | border-top: 0.1rem solid #e5e7eb;
20 |
21 | @include media-breakpoint-up(lg) {
22 | display: none;
23 | padding-right: 0;
24 | padding-left: 0;
25 | border-top: unset;
26 | }
27 | }
28 |
29 | .bottom-nav-link {
30 | color: #3b414a;
31 | font-size: 0.85rem;
32 | font-weight: 500;
33 | font-family: "Inter";
34 | letter-spacing: 0.03rem;
35 | padding-top: 0.5rem;
36 | padding-bottom: 0.5rem;
37 | padding-left: 0.7rem;
38 | padding-right: 0.7rem;
39 | border-radius: 0.5rem;
40 | cursor: pointer;
41 | display: inline-block;
42 |
43 | @include media-breakpoint-up(sm) {
44 | margin-left: 0.3rem;
45 | border-bottom: none;
46 | }
47 |
48 | &:hover {
49 | text-decoration: none;
50 | color: #3b414a;
51 | background-color: #e5e7eb;
52 | }
53 |
54 | &:last-of-type {
55 | border-bottom: 0rem solid #e0e1e0;
56 | }
57 | }
58 |
59 | .bottom-nav-link-icon {
60 | margin-top: -0.15rem;
61 | margin-right: 0.1rem;
62 | font-size: 1.3rem;
63 | color: #4b5563;
64 |
65 | @include media-breakpoint-up(sm) {
66 | margin-top: -0.15rem;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/styles/datetime.scss:
--------------------------------------------------------------------------------
1 | .datetime-weekday {
2 | display: block;
3 | text-align: center;
4 | font-family: "Cal Sans";
5 | text-transform: uppercase;
6 | font-size: 0.8rem;
7 | font-weight: 500;
8 | margin-bottom: -0.2em;
9 | color: #8c8c8c;
10 |
11 | @include media-breakpoint-up(sm) {
12 | font-size: 1rem;
13 | }
14 | }
15 |
16 | .datetime-day {
17 | display: block;
18 | text-align: center;
19 | font-family: "Cal Sans";
20 | font-size: 1.5rem;
21 | margin-bottom: -0.2em;
22 |
23 | @include media-breakpoint-up(sm) {
24 | font-size: 2rem;
25 | }
26 | }
27 |
28 | .datetime-mon {
29 | display: block;
30 | text-align: center;
31 | font-size: 0.8rem;
32 | font-family: "Cal Sans";
33 | letter-spacing: 0.02rem;
34 | font-weight: 500;
35 | margin-bottom: 0.8em;
36 | color: #8c8c8c;
37 |
38 | @include media-breakpoint-up(sm) {
39 | font-size: 1.2rem;
40 | }
41 | }
42 |
43 | .datetime-time-1 {
44 | font-family: "Inter";
45 | display: block;
46 | text-align: center;
47 | margin-bottom: -0.2em;
48 | letter-spacing: 0.03rem;
49 | font-size: 0.7rem;
50 |
51 | @include media-breakpoint-up(sm) {
52 | font-size: 1rem;
53 | }
54 | }
55 |
56 | .datetime-time-2 {
57 | font-family: "Inter";
58 | display: block;
59 | text-align: center;
60 | letter-spacing: 0.03rem;
61 | font-size: 0.7rem;
62 |
63 | @include media-breakpoint-up(sm) {
64 | font-size: 1rem;
65 | }
66 | }
67 |
68 | .datetime-time-sep-voter {
69 | font-size: 1.2rem;
70 | font-weight: 300;
71 | color: #d7d7d7;
72 | margin-left: -0.7rem;
73 |
74 | @include media-breakpoint-up(sm) {
75 | font-size: 1.7rem;
76 | margin-left: -1rem;
77 | }
78 | }
79 |
80 | .datetime-time-sep-admin {
81 | font-size: 1.2rem;
82 | font-weight: 300;
83 | color: #d7d7d7;
84 | margin-left: -0.7rem;
85 |
86 | @include media-breakpoint-up(sm) {
87 | font-size: 1.7rem;
88 | margin-left: -1rem;
89 | }
90 | }
91 |
92 | .datetime-sep-time-div {
93 | display: flex;
94 | flex-direction: row;
95 | justify-content: center;
96 | align-items: center;
97 | }
98 |
99 | .datetime-same-middle-line {
100 | border-top: 0.2rem solid #c4c4c4 !important;
101 | height: 1.5rem;
102 | margin-bottom: 1rem;
103 |
104 | @include media-breakpoint-up(sm) {
105 | height: 2rem;
106 | margin-bottom: 2rem;
107 | }
108 | }
109 |
110 | .datetime-same-last-line {
111 | border-top: 0.2rem solid #c4c4c4 !important;
112 | border-right: 0.2rem solid #c4c4c4 !important;
113 | height: 1.5rem;
114 | margin-right: 2.5rem;
115 | margin-bottom: 1rem;
116 |
117 | @include media-breakpoint-up(sm) {
118 | height: 2rem;
119 | margin-right: 3.5rem;
120 | margin-bottom: 2rem;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/styles/form.scss:
--------------------------------------------------------------------------------
1 | .form-label {
2 | font-weight: 400;
3 | font-family: "Inter";
4 | font-size: 0.875rem;
5 | color: #101827;
6 | }
7 |
8 | .form-text {
9 | background-color: #ffffff;
10 | color: #101827;
11 | border: 0.09rem solid #e5e7eb;
12 | border-radius: 0.5rem;
13 | font-family: "Inter";
14 | font-size: 0.9rem;
15 | font-weight: 400;
16 | transition: none;
17 | margin-bottom: 1rem;
18 | margin-top: 0;
19 | transition: 0.2s;
20 |
21 | @include media-breakpoint-up(sm) {
22 | margin-bottom: 0;
23 | margin-top: 0;
24 | }
25 | }
26 |
27 | .form-text::placeholder {
28 | color: #818181;
29 | opacity: 1;
30 | }
31 |
32 | .form-text:hover {
33 | color: #101827;
34 | background-color: #ffffff;
35 | border: 0.09rem solid #b7b9bd;
36 | }
37 |
38 | .form-text:focus {
39 | color: #101827;
40 | background-color: #ffffff;
41 | border: 0.1rem solid #101827;
42 | }
43 |
44 | .form-control:focus {
45 | -webkit-box-shadow: none;
46 | box-shadow: none;
47 | }
48 |
49 | .form-button-spinner {
50 | vertical-align: -0.2rem;
51 | }
52 |
53 | .form-select {
54 | background-color: #ffffff;
55 | color: #101827;
56 | border: 0.09rem solid #e5e7eb;
57 | border-radius: 0.5rem;
58 | font-family: "Inter";
59 | font-size: 0.9rem;
60 | font-weight: 400;
61 | margin-bottom: 1rem;
62 | margin-top: 0;
63 | transition: 0.2s;
64 | padding-left: 0.5rem !important;
65 |
66 | @include media-breakpoint-up(sm) {
67 | margin-bottom: 0;
68 | margin-top: 0;
69 | }
70 | }
71 |
72 | .form-select:hover {
73 | color: #101827;
74 | background-color: #ffffff;
75 | border: 0.09rem solid #b7b9bd;
76 | }
77 |
78 | .form-select:focus {
79 | color: #101827;
80 | background-color: #ffffff;
81 | border: 0.1rem solid #101827;
82 | }
83 |
84 | .form-group {
85 | margin-bottom: 0 !important;
86 | }
87 |
88 | .samay-form-col {
89 | padding-right: 1rem;
90 | padding-left: 1rem;
91 |
92 | @include media-breakpoint-up(sm) {
93 | padding-right: 0rem;
94 | &.first-of-type {
95 | padding-left: 2rem;
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/styles/global.scss:
--------------------------------------------------------------------------------
1 | @import "/node_modules/bootstrap/scss/bootstrap.scss";
2 |
3 | html,
4 | body {
5 | padding: 0;
6 | margin: 0;
7 | background-color: #ffffff;
8 | }
9 | * {
10 | box-sizing: border-box;
11 | }
12 |
13 | ::-webkit-scrollbar {
14 | -webkit-appearance: none;
15 | height: 0.6rem;
16 | width: 0.6rem;
17 | }
18 |
19 | ::-webkit-scrollbar-track {
20 | border-radius: 0.3rem;
21 | background-color: #f2f2f2;
22 | }
23 |
24 | ::-webkit-scrollbar-thumb {
25 | border-radius: 0.3rem;
26 | background: #dbd9d9;
27 | }
28 |
29 | ::-moz-selection {
30 | color: #f9fafa;
31 | background: #101827;
32 | }
33 |
34 | ::selection {
35 | color: #f9fafa;
36 | background: #101827;
37 | }
38 |
39 | main {
40 | flex: 1;
41 | justify-content: center;
42 | align-items: center;
43 | }
44 |
45 | .global-flex-wrapper {
46 | display: flex;
47 | min-height: 100vh;
48 | flex-direction: column;
49 | justify-content: space-between;
50 | }
51 |
52 | .global-container {
53 | padding-left: 0.5rem;
54 | width: 95%;
55 | padding-right: 0.5rem;
56 |
57 | @include media-breakpoint-up(sm) {
58 | width: 75rem;
59 | padding-left: 0;
60 | padding-right: 0;
61 | }
62 | }
63 |
64 | .global-page-section {
65 | margin-top: 1.5rem;
66 |
67 | @include media-breakpoint-up(sm) {
68 | margin-top: 2rem;
69 | }
70 | }
71 |
72 | .Toastify__toast-body {
73 | font-size: 0.8rem;
74 | font-family: "Inter";
75 | }
76 |
77 | .global-primary-button {
78 | background-color: #101827;
79 | color: #ffffff;
80 | font-weight: 500;
81 | font-size: 1rem;
82 | font-family: "Cal Sans";
83 | letter-spacing: 0.09rem;
84 | border-radius: 0.5rem;
85 | border: none;
86 | padding: 0.35rem 1rem 0.35rem 1rem;
87 | width: 100%;
88 | transition: 0.2s;
89 |
90 | &:hover {
91 | color: #ffffff !important;
92 | box-shadow: none !important;
93 | border: none !important;
94 | background-color: #353535 !important;
95 | }
96 |
97 | &:active,
98 | &:focus {
99 | color: #ffffff !important;
100 | border: none !important;
101 | box-shadow: none !important;
102 | background-color: #101827 !important;
103 | }
104 |
105 | &[disabled] {
106 | opacity: 1 !important;
107 | box-shadow: none !important;
108 | color: #ffffff !important;
109 | border: none !important;
110 | background-color: #101827 !important;
111 | pointer-events: none;
112 | }
113 |
114 | @include media-breakpoint-up(sm) {
115 | width: unset;
116 | float: right;
117 | }
118 | }
119 |
120 | .global-primary-button:hover {
121 | box-shadow: 0rem 0.15rem 0.3rem rgba(0, 0, 0, 0.2);
122 | }
123 |
124 | .global-primary-button[disabled] {
125 | opacity: 0.4;
126 | pointer-events: none;
127 | }
128 |
129 | .global-small-primary-btn {
130 | background-color: #101827;
131 | color: #ffffff;
132 | font-weight: 500;
133 | font-size: 1rem;
134 | font-family: "Cal Sans";
135 | letter-spacing: 0.09rem;
136 | border-radius: 0.5rem;
137 | border: none;
138 | padding: 0.35rem 1.2rem 0.35rem 1.2rem;
139 |
140 | &:hover {
141 | color: #ffffff !important;
142 | box-shadow: none !important;
143 | border: none !important;
144 | background-color: #424242 !important;
145 | }
146 |
147 | &:active,
148 | &:focus {
149 | color: #ffffff !important;
150 | border: none !important;
151 | box-shadow: none !important;
152 | background-color: #101827 !important;
153 | }
154 | }
155 |
156 | @import "./navbar.scss";
157 | @import "./bottom-nav.scss";
158 | @import "./poll.scss";
159 | @import "./datetime.scss";
160 | @import "./new-poll.scss";
161 | @import "./rat.scss";
162 | @import "./form.scss";
163 | @import "./privacy.scss";
164 | @import "./your-polls.scss";
165 | @import "./how-to.scss";
166 | @import "./voter-page.scss";
167 | @import "react-big-calendar/lib/css/react-big-calendar.css";
168 | @import "react-toastify/dist/ReactToastify.css";
169 | @import "./samay-rbc.scss";
170 |
--------------------------------------------------------------------------------
/src/styles/how-to.scss:
--------------------------------------------------------------------------------
1 | .how-to-root-container {
2 | min-height: 100vh;
3 | }
4 |
5 | .how-to-flex-wrapper {
6 | display: flex;
7 | min-height: 100vh;
8 | flex-direction: column;
9 | justify-content: space-between;
10 | }
11 |
12 | //
13 | // Container
14 | // ..................................................................../
15 |
16 | .how-to-container {
17 | margin-top: 7rem;
18 | margin-bottom: 2rem;
19 | padding-left: 0.5rem;
20 | width: 95%;
21 | padding-right: 0.5rem;
22 |
23 | @include media-breakpoint-up(sm) {
24 | width: 100rem;
25 | padding-left: 0;
26 | padding-right: 0;
27 | }
28 |
29 | &.cta {
30 | text-align: center;
31 | margin-top: 3rem;
32 | margin-bottom: 7rem;
33 | }
34 | }
35 |
36 | //
37 | // Page
38 | // ..................................................................../
39 |
40 | .how-to-features {
41 | &.title {
42 | font-size: 1.6rem;
43 | color: #101827;
44 | font-family: "Cal Sans";
45 | font-weight: 700;
46 | line-height: 2rem;
47 | padding-right: 0.5rem;
48 | letter-spacing: 0.05rem;
49 | padding-left: 0.5rem;
50 | display: block;
51 | text-align: center;
52 | margin-top: 3rem;
53 |
54 | @include media-breakpoint-up(sm) {
55 | font-size: 2.7rem;
56 | padding-bottom: 1rem;
57 | margin-top: 1rem;
58 | }
59 | }
60 |
61 | &.desc {
62 | font-size: 1rem;
63 | color: #868686;
64 | font-family: "Inter";
65 | font-weight: 500;
66 | letter-spacing: 0.05rem;
67 | display: block;
68 | text-align: center;
69 | padding-left: 0.5rem;
70 | padding-right: 0.5rem;
71 | margin-bottom: 5rem;
72 | margin-top: 1rem;
73 |
74 | @include media-breakpoint-up(sm) {
75 | margin-top: 0.5rem;
76 | font-size: 1.5rem;
77 | }
78 | }
79 | }
80 |
81 | .how-to-card-group {
82 | margin-bottom: 1rem;
83 | margin-left: auto;
84 | margin-right: auto;
85 |
86 | @include media-breakpoint-up(sm) {
87 | width: 50%;
88 | }
89 | }
90 |
91 | .how-to-card {
92 | background-color: #f9fafb;
93 | border: none;
94 | border-radius: 0.5rem;
95 |
96 | &.icon-yes {
97 | color: #49de80;
98 | font-size: 1rem;
99 | padding: 0;
100 | margin-top: -0.1rem;
101 | }
102 |
103 | &.icon-if-need-be {
104 | color: #fcd34d;
105 | font-size: 1rem;
106 | padding: 0;
107 | margin-top: -0.1rem;
108 | }
109 |
110 | &.title {
111 | font-family: "Cal Sans";
112 | font-size: 1.2rem;
113 | letter-spacing: 0.05rem;
114 | font-weight: 700;
115 | color: #101827;
116 | }
117 |
118 | &.desc {
119 | font-family: "Inter";
120 | font-weight: 400;
121 | font-size: 0.9rem;
122 |
123 | @include media-breakpoint-up(sm) {
124 | font-size: 1rem;
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/styles/navbar.scss:
--------------------------------------------------------------------------------
1 | .navbar {
2 | padding-top: 0.3rem;
3 | padding-bottom: 0.3rem;
4 | height: 2.5rem;
5 | background-color: #fafafa;
6 | padding-right: 0.5rem;
7 | padding-left: 0.5rem;
8 | border-bottom: 0.1rem solid #e5e7eb;
9 |
10 | @include media-breakpoint-up(sm) {
11 | padding-right: 0;
12 | padding-left: 0;
13 | }
14 | }
15 |
16 | .navbar-container {
17 | padding-left: 0.5rem;
18 | width: 95%;
19 | padding-right: 0.5rem;
20 | justify-content: center !important;
21 |
22 | @include media-breakpoint-up(sm) {
23 | width: 75rem;
24 | padding-left: 0;
25 | padding-right: 0;
26 | justify-content: space-between !important;
27 | }
28 | }
29 |
30 | .navbar-brand {
31 | padding: 0 !important;
32 | font-size: unset !important;
33 | }
34 |
35 | .navbar-logo {
36 | width: auto;
37 | height: 1.4rem;
38 | display: inline-block;
39 | margin-top: -0.15rem;
40 | }
41 |
42 | .navbar-logo-text {
43 | font-family: "Cal Sans";
44 | margin-left: 0.3rem;
45 | font-size: 1.1rem;
46 | color: #101827;
47 | display: inline-block;
48 | margin-top: -0.4rem;
49 | }
50 |
51 | .navbar-hamburger {
52 | border: none;
53 | padding: 0;
54 | outline: none;
55 | display: none;
56 |
57 | &:hover,
58 | &:active,
59 | &:focus {
60 | border: none;
61 | outline: none;
62 | }
63 | @include media-breakpoint-up(lg) {
64 | display: unset;
65 | }
66 | }
67 |
68 | .navbar-link {
69 | color: #3b414a;
70 | font-size: 0.85rem;
71 | font-weight: 500;
72 | font-family: "Inter";
73 | letter-spacing: 0.03rem;
74 | margin-right: 0rem;
75 | margin-left: 0rem;
76 | padding-top: 0.2rem;
77 | padding-bottom: 0.2rem;
78 | padding-left: 0.5rem;
79 | padding-right: 0.5rem;
80 | border-radius: 0.5rem;
81 | cursor: pointer;
82 | transition: 0.2s;
83 |
84 | @include media-breakpoint-up(sm) {
85 | margin-left: 0.9rem;
86 | border-bottom: none;
87 | }
88 |
89 | &:hover {
90 | text-decoration: none;
91 | color: #3b414a;
92 | background-color: #e5e7eb;
93 |
94 | .how-to-link-new-visitor {
95 | color: #3b414a;
96 | }
97 | }
98 |
99 | &:last-of-type {
100 | border-bottom: 0rem solid #e0e1e0;
101 | }
102 | }
103 |
104 | .navbar-link-icon {
105 | margin-top: -0.15rem;
106 | margin-right: 0.2rem;
107 | font-size: 0.9rem;
108 | color: #4b5563;
109 |
110 | @include media-breakpoint-up(sm) {
111 | margin-top: -0.15rem;
112 | }
113 | }
114 |
115 | .beacon {
116 | background-color: #ff5f1f;
117 | border-radius: 50%;
118 | width: 0.35rem;
119 | height: 0.35rem;
120 | display: block;
121 | z-index: 1;
122 | position: absolute;
123 | right: 0;
124 | top: 0;
125 | }
126 |
127 | .beacon::before {
128 | background-color: #ff5f1f;
129 | content: "";
130 | top: calc(50% - 0.25rem);
131 | left: calc(50% - 0.25rem);
132 | width: 0.5rem;
133 | height: 0.5rem;
134 | opacity: 1;
135 | border-radius: 50%;
136 | position: absolute;
137 | animation: burst-animation 1.5s infinite;
138 | animation-fill-mode: forwards;
139 | z-index: 0;
140 | }
141 |
142 | @keyframes burst-animation {
143 | from {
144 | opacity: 1;
145 | transform: scale(1);
146 | }
147 | to {
148 | opacity: 0;
149 | transform: scale(2);
150 | }
151 | }
152 |
153 | .how-to-link {
154 | position: relative;
155 | }
156 |
157 | .how-to-link-new-visitor {
158 | color: #ff5f1f;
159 | }
160 |
--------------------------------------------------------------------------------
/src/styles/new-poll.scss:
--------------------------------------------------------------------------------
1 | .new-poll-jumbo {
2 | background-color: #fefffe;
3 | color: #ffffff;
4 | padding: 1rem;
5 | margin-bottom: 5rem;
6 | border-radius: 0.5rem;
7 | border: 0.09rem solid #e5e7eb;
8 |
9 | @include media-breakpoint-up(lg) {
10 | margin-bottom: 2rem;
11 | }
12 | }
13 |
14 | .new-poll-timeslot-jumbo {
15 | background-color: #fefffe;
16 | position: relative;
17 | color: #101827;
18 | padding-top: 1rem;
19 | padding-left: 1rem;
20 | padding-right: 1rem;
21 | padding-bottom: 1rem;
22 | border-radius: 0.5rem;
23 | margin-top: 0rem;
24 | margin-bottom: 1.7rem;
25 | border: 0.09rem solid #e5e7eb;
26 | height: 37rem;
27 | }
28 |
--------------------------------------------------------------------------------
/src/styles/poll.scss:
--------------------------------------------------------------------------------
1 | .poll-info-jumbo {
2 | font-family: "Cal Sans";
3 | background-color: #fefffe;
4 | color: #101827;
5 | margin-top: 1rem;
6 | margin-bottom: 1.7rem;
7 | border-radius: 0.5rem;
8 | border: 0.09rem solid #e5e7eb;
9 | padding: 1rem;
10 |
11 | @include media-breakpoint-up(sm) {
12 | width: unset;
13 | padding: 4rem;
14 | }
15 | }
16 |
17 | .poll-table-jumbo {
18 | background-color: #fefffe;
19 | color: #101827;
20 | padding: 1rem;
21 | border-radius: 0.5rem;
22 | margin-bottom: 1rem;
23 | border: 0.09rem solid #e5e7eb;
24 |
25 | @include media-breakpoint-up(sm) {
26 | padding: 4rem;
27 | }
28 |
29 | &.first {
30 | margin-top: 1rem;
31 | }
32 |
33 | &.second {
34 | margin-top: 5rem;
35 |
36 | @include media-breakpoint-up(sm) {
37 | margin-top: 8rem;
38 | }
39 | }
40 | }
41 |
42 | .poll-message {
43 | font-family: "Inter";
44 | background-color: #fefffe;
45 | color: #101827;
46 | border-radius: 0.5rem;
47 | margin-bottom: 1rem;
48 | padding: 1rem 1rem 1rem 1rem;
49 |
50 | @include media-breakpoint-up(sm) {
51 | padding-top: 2rem;
52 | padding-right: 4rem;
53 | padding-bottom: 2rem;
54 | padding-left: 4rem;
55 | }
56 | }
57 |
58 | .poll-badge-open {
59 | font-size: 0.6rem;
60 | padding-top: 0.6em;
61 | padding-left: 0.6em;
62 | padding-right: 0.6em;
63 | padding-bottom: 0.6em;
64 | background-color: #f0fdf4;
65 | color: #49de80;
66 | border: 0.01rem solid #49de80;
67 | transition: none;
68 | font-size: 0.5rem;
69 | letter-spacing: 0.02rem;
70 | margin-right: 0.5rem;
71 |
72 | @include media-breakpoint-up(sm) {
73 | font-size: 0.6rem;
74 | }
75 | }
76 |
77 | .poll-badge-closed {
78 | font-size: 0.6rem;
79 | padding-top: 0.6em;
80 | padding-left: 0.6em;
81 | padding-right: 0.6em;
82 | padding-bottom: 0.6em;
83 | background-color: #e7e7e7;
84 | border: 0.01rem solid #404040;
85 | color: #404040;
86 | transition: none;
87 | font-size: 0.5rem;
88 | letter-spacing: 0.02rem;
89 | margin-right: 0.5rem;
90 |
91 | @include media-breakpoint-up(sm) {
92 | font-size: 0.6rem;
93 | }
94 | }
95 |
96 | .poll-badge-polltype {
97 | font-size: 0.6rem;
98 | padding-top: 0.6em;
99 | padding-left: 0.6em;
100 | padding-right: 0.6em;
101 | padding-bottom: 0.6em;
102 | background-color: transparent;
103 | border: 0.01rem solid #585858;
104 | color: #585858;
105 | transition: none;
106 | font-size: 0.5rem;
107 | letter-spacing: 0.02rem;
108 |
109 | @include media-breakpoint-up(sm) {
110 | font-size: 0.6rem;
111 | }
112 | }
113 |
114 | .poll-info-title {
115 | display: block;
116 | margin-top: 1rem;
117 | margin-bottom: 0.5rem;
118 | color: #101827;
119 | letter-spacing: 0.02rem;
120 | font-size: 1.8rem;
121 | line-height: 1.8rem;
122 | font-weight: 600;
123 | white-space: nowrap;
124 | overflow: hidden;
125 | text-overflow: ellipsis;
126 |
127 | @include media-breakpoint-up(sm) {
128 | font-size: 2.8rem;
129 | line-height: 3.5rem;
130 | }
131 | }
132 |
133 | .poll-info-title-no-desc {
134 | margin-bottom: 1.5rem;
135 | }
136 |
137 | .poll-info-desc {
138 | display: block;
139 | font-weight: 500;
140 | color: #565656;
141 | font-size: 1rem;
142 | letter-spacing: 0.02rem;
143 | margin-top: 0.5rem;
144 | margin-bottom: 1.4rem;
145 | line-height: 1.5rem;
146 | white-space: nowrap;
147 | overflow: hidden;
148 | text-overflow: ellipsis;
149 |
150 | @include media-breakpoint-up(sm) {
151 | font-size: 1.5rem;
152 | line-height: 1.8rem;
153 | margin-bottom: 2rem;
154 | }
155 | }
156 |
157 | .poll-info-detail-title {
158 | font-family: "Inter";
159 | font-size: 0.7rem;
160 | display: block;
161 | font-weight: 500;
162 | color: #5e5e5e;
163 | margin-top: 0.5rem;
164 |
165 | @include media-breakpoint-up(sm) {
166 | font-size: 1rem;
167 | }
168 | }
169 |
170 | .poll-info-detail-title-share {
171 | font-family: "Inter";
172 | font-size: 0.7rem;
173 | display: block;
174 | font-weight: 500;
175 | color: #000;
176 | margin-top: 2rem;
177 |
178 | @include media-breakpoint-up(sm) {
179 | font-size: 1rem;
180 | }
181 | }
182 |
183 | .poll-info-icon {
184 | margin-top: -0.2rem;
185 | margin-right: 0.5rem;
186 | }
187 |
188 | .poll-participant-cell {
189 | border-top: 0 !important;
190 | border-bottom: 0 !important;
191 | width: 5rem;
192 | }
193 |
194 | .poll-slot-time {
195 | border-top: 0 !important;
196 | border-bottom: 0 !important;
197 | min-width: 5rem;
198 | padding-left: 0 !important;
199 | padding-right: 0 !important;
200 |
201 | @include media-breakpoint-up(sm) {
202 | min-width: 7rem;
203 | }
204 | }
205 |
206 | .poll-table-total-participants {
207 | font-size: 0.7rem !important;
208 | font-family: "Cal Sans";
209 | color: #8c8c8c;
210 | font-weight: 600;
211 | border-top: 0 !important;
212 | vertical-align: middle !important;
213 | width: 5rem;
214 | min-width: 8.3rem;
215 |
216 | @include media-breakpoint-up(sm) {
217 | font-size: 0.8rem !important;
218 | }
219 | }
220 |
221 | .poll-slot-total-votes {
222 | font-family: "Cal Sans";
223 | background-color: #fefffe;
224 | text-align: center;
225 | border-top: 0.5rem solid #fefffe !important;
226 | border-left: 0.5rem solid #fefffe !important;
227 | width: 7rem;
228 | font-weight: 600;
229 | }
230 |
231 | .poll-total-votes-icon {
232 | margin-top: -0.1rem;
233 | margin-right: 0.2rem;
234 | color: #c4c4c4;
235 |
236 | @include media-breakpoint-up(sm) {
237 | margin-top: -0.2rem;
238 | }
239 | }
240 |
241 | .poll-slot-checked {
242 | background-color: #f0fdf4;
243 | text-align: center;
244 | border-top: 0.5rem solid #fefffe !important;
245 | border-left: 0.5rem solid #fefffe !important;
246 | width: 7rem;
247 | }
248 |
249 | .poll-slot-checked-if-need-be {
250 | background-color: #fffbeb;
251 | text-align: center;
252 | border-top: 0.5rem solid #fefffe !important;
253 | border-left: 0.5rem solid #fefffe !important;
254 | width: 7rem;
255 | }
256 |
257 | .poll-slot-unchecked {
258 | border-top: 0.5rem solid #fefffe !important;
259 | border-left: 0.5rem solid #fefffe !important;
260 | background-image: linear-gradient(
261 | 135deg,
262 | #fefffe 30%,
263 | #e0e0e0 30%,
264 | #e0e0e0 50%,
265 | #fefffe 50%,
266 | #fefffe 80%,
267 | #e0e0e0 80%,
268 | #e0e0e0 100%
269 | );
270 | background-size: 7.07px 7.07px;
271 | width: 7rem;
272 | }
273 |
274 | .poll-slot-checked .poll-slot-check {
275 | width: 1.5rem;
276 | height: 1.5rem;
277 | color: #49de80;
278 | }
279 |
280 | .poll-slot-checked-if-need-be .poll-slot-check {
281 | width: 1.5rem;
282 | height: 1.5rem;
283 | color: #fcd34d;
284 | }
285 |
286 | .poll-slot-unchecked .poll-slot-check {
287 | display: none;
288 | }
289 |
290 | .poll-table-participants {
291 | font-size: 0.8rem !important;
292 | font-family: "Cal Sans";
293 | color: #101827;
294 | font-weight: 600;
295 | border-top: 0 !important;
296 | vertical-align: middle !important;
297 | width: 5rem;
298 | max-width: 8.3rem;
299 | white-space: nowrap;
300 | overflow: hidden;
301 | text-overflow: ellipsis;
302 |
303 | @include media-breakpoint-up(sm) {
304 | font-size: 1.1rem !important;
305 | }
306 | }
307 |
308 | .delete-poll-on-creator-page {
309 | float: right;
310 | }
311 |
312 | .poll-mark-time-name {
313 | background-color: #ffffff;
314 | color: #101827;
315 | border: 0.09rem solid #e5e7eb;
316 | border-radius: 0.5rem;
317 | font-family: "Inter";
318 | font-size: 0.9rem;
319 | font-weight: 400;
320 | transition: none;
321 | }
322 |
323 | .poll-mark-time-name:hover {
324 | color: #101827;
325 | background-color: #ffffff;
326 | border: 0.09rem solid #b7b9bd;
327 | }
328 |
329 | .poll-mark-time-name:focus {
330 | color: #101827;
331 | background-color: #ffffff;
332 | border: 0.1rem solid #101827;
333 | }
334 |
335 | .poll-mark-time-name.logged-in {
336 | box-shadow: none;
337 | outline: none;
338 | }
339 |
340 | .poll-table-choose-text {
341 | font-family: "Cal Sans";
342 | font-size: 1.2rem;
343 | font-weight: 600;
344 | border-top: 0 !important;
345 | color: #101827;
346 | min-width: 9rem;
347 | }
348 |
349 | .poll-slot-checkbox {
350 | width: 1.5rem;
351 | margin: 0 auto;
352 | }
353 |
354 | .poll-slot-checkbox-final-cell {
355 | background-color: #fefffe;
356 | border-top: 0 !important;
357 | text-align: center;
358 | }
359 |
360 | .poll-slot-checkbox input[type="radio"] {
361 | width: 1.5rem;
362 | height: 1.5rem;
363 | accent-color: #101827;
364 | }
365 |
366 | .poll-slot-checkbox-one-on-one {
367 | width: 1.5rem;
368 | margin: 0 auto;
369 | }
370 |
371 | .poll-slot-checkbox-one-on-one input[type="radio"] {
372 | width: 1.5rem;
373 | height: 1.5rem;
374 | appearance: none;
375 | border-radius: 50%;
376 | background-clip: content-box;
377 | border: 0.15rem solid #b6b6b6;
378 | background-color: white;
379 | }
380 |
381 | .poll-slot-checkbox-one-on-one input[type="radio"]:checked {
382 | background-color: #49de80;
383 | padding: 4px;
384 | border: 0.15rem solid #49de80;
385 | }
386 |
387 | .copy-text-desktop {
388 | display: none;
389 |
390 | @include media-breakpoint-up(sm) {
391 | display: block;
392 | }
393 | }
394 |
395 | .copy-text-mobile {
396 | @include media-breakpoint-up(sm) {
397 | display: none;
398 | }
399 | }
400 |
401 | .poll-shareinvite-content {
402 | font-family: "Inter";
403 | color: #484848;
404 | font-size: 0.9rem;
405 | padding-left: 0;
406 | padding-right: 0;
407 | border-left: 0rem;
408 | display: inline-block;
409 | margin-top: -0.5rem;
410 |
411 | @include media-breakpoint-up(sm) {
412 | padding-left: unset;
413 | margin-left: 0.2rem;
414 | margin-right: 0.2rem;
415 | padding-right: unset;
416 | border-left: unset;
417 | }
418 | }
419 |
420 | .poll-share-textbox {
421 | background-color: #f9fafa !important;
422 | color: #515151;
423 | border: 0.1rem solid #363636;
424 | border-radius: 0.5rem;
425 | font-family: "Inter";
426 | font-size: 0.6rem;
427 | font-weight: 500;
428 | transition: none;
429 | width: 15rem !important;
430 | text-overflow: ellipsis;
431 | padding: 0.5rem;
432 |
433 | &:focus {
434 | color: #515151;
435 | border: 0.1rem solid #363636;
436 | }
437 |
438 | @include media-breakpoint-up(sm) {
439 | font-size: 0.7rem;
440 | }
441 | }
442 |
443 | .poll-copy-btn {
444 | font-size: 0.5rem;
445 | border: none;
446 | color: #ffffff;
447 | border-top-right-radius: 0.5rem;
448 | border-bottom-right-radius: 0.5rem;
449 | background-color: #101827;
450 | transition: none;
451 |
452 | &:hover,
453 | &:active,
454 | &:focus {
455 | background-color: #101827 !important;
456 | color: #ffffff !important;
457 | border: none !important;
458 | }
459 |
460 | @include media-breakpoint-up(sm) {
461 | font-size: 0.7rem;
462 | }
463 | }
464 |
465 | .poll-mark-time-cell {
466 | border-top: 0 !important;
467 | text-align: center;
468 | width: 7rem;
469 | padding: 0;
470 | vertical-align: middle;
471 | }
472 |
473 | .poll-mark-time-box {
474 | width: 2rem;
475 | height: 2rem;
476 | margin: 0 auto;
477 | border-radius: 0.5rem;
478 | align-items: center;
479 | display: flex;
480 | cursor: pointer;
481 | }
482 |
483 | .poll-mark-time-box-check {
484 | width: 1rem;
485 | height: 1rem;
486 | margin: 0 auto;
487 | align-items: center;
488 | }
489 |
490 | .poll-mark-time-box.no {
491 | border: 0.15rem solid #b6b6b6;
492 | }
493 |
494 | .poll-mark-time-box.yes {
495 | border: 0.15rem solid #49de80;
496 | background-color: #f0fdf4;
497 | }
498 |
499 | .poll-mark-time-box-check.yes {
500 | color: #49de80;
501 | pointer-events: none;
502 | }
503 |
504 | .poll-mark-time-box.if-need-be {
505 | border: 0.15rem solid #fcd34d;
506 | background-color: #fffbeb;
507 | }
508 |
509 | .poll-mark-time-box-check.if-need-be {
510 | color: #fcd34d;
511 | pointer-events: none;
512 | }
513 |
514 | .submit-final-time-button {
515 | margin-bottom: 6rem;
516 | }
517 |
518 | .mark-times-button {
519 | margin-bottom: 6rem;
520 | }
521 |
522 | .bmc {
523 | margin-top: 2rem;
524 | }
525 |
--------------------------------------------------------------------------------
/src/styles/privacy.scss:
--------------------------------------------------------------------------------
1 | .privacy-jumbo {
2 | background-color: #ffffff;
3 | margin-top: 1rem;
4 | margin-bottom: 5rem;
5 | border-radius: 0.5rem;
6 | padding: 1rem;
7 |
8 | @include media-breakpoint-up(sm) {
9 | width: unset;
10 | padding: 5rem 10rem 5rem 10rem;
11 | }
12 |
13 | p {
14 | font-size: 0.8rem;
15 | color: #383838;
16 | margin-top: 0rem;
17 | font-family: "Inter";
18 |
19 | @include media-breakpoint-up(sm) {
20 | font-size: 1rem;
21 | }
22 | }
23 |
24 | a {
25 | color: #101827;
26 | text-decoration: underline;
27 | }
28 |
29 | h1 {
30 | color: #101827;
31 | font-family: "Cal Sans";
32 | letter-spacing: 0.02rem;
33 | font-size: 1.5rem;
34 |
35 | @include media-breakpoint-up(sm) {
36 | font-size: 2.5rem;
37 | }
38 | }
39 |
40 | h3 {
41 | color: #101827;
42 | margin-top: 2.5rem;
43 | letter-spacing: 0.02rem;
44 | font-family: "Cal Sans";
45 | font-size: 1rem;
46 |
47 | @include media-breakpoint-up(sm) {
48 | font-size: 1.3rem;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/styles/rat.scss:
--------------------------------------------------------------------------------
1 | // need to overwrite react-available-times's styles hence !important is required
2 |
3 | .rat-TimeSlot_component {
4 | background-color: #101827 !important;
5 | color: #f9fafa !important;
6 | }
7 |
8 | .rat-TimeSlot_handle {
9 | color: #f9fafa !important;
10 | }
11 |
12 | .rat-TimeSlot_title {
13 | font-weight: 700 !important;
14 | font-family: "Inter" !important;
15 | color: #f9fafa !important;
16 | padding: 0.5rem 0 0 0 !important;
17 | font-size: 0.6rem !important;
18 | line-height: 0.6rem !important;
19 | text-align: center !important;
20 |
21 | @include media-breakpoint-up(sm) {
22 | font-size: 0.8rem !important;
23 | }
24 | }
25 |
26 | .rat-AvailableTimes_toolbar {
27 | padding-top: 1rem !important;
28 | padding-bottom: 1.5rem !important;
29 | padding-left: 0 !important;
30 | padding-right: 0 !important;
31 | }
32 |
33 | .rat-AvailableTimes_interval {
34 | font-family: "Inter" !important;
35 | color: #101827 !important;
36 | letter-spacing: 0.05rem;
37 | margin-top: 0.3rem;
38 | font-size: 0.9rem !important;
39 | flex-basis: 8rem !important;
40 | }
41 |
42 | .rat-DayHeader_day {
43 | font-size: 0.8rem !important;
44 | font-family: "Inter" !important;
45 | font-weight: 600 !important;
46 | }
47 |
48 | .rat-Ruler_inner {
49 | font-family: "Inter" !important;
50 | font-size: 0.8rem !important;
51 | }
52 |
53 | .rat-AvailableTimes_button {
54 | fill: #101827 !important;
55 |
56 | &:hover {
57 | background-color: #e5e7eb !important;
58 | }
59 | }
60 |
61 | .rat-Ruler_component {
62 | background-color: #fefffe !important;
63 | }
64 |
65 | .rat-AvailableTimes_home {
66 | display: none !important;
67 | }
68 |
69 | .rat-DayHeader_day {
70 | color: #101827 !important;
71 | }
72 |
73 | .rat-Week_allDayLabel {
74 | display: none !important;
75 | }
76 |
77 | .rat-Ruler_inner {
78 | color: #101827 !important;
79 | }
80 |
81 | .rat-DayHeader_events {
82 | display: none !important;
83 | }
84 |
85 | .rat-TimeSlot_delete {
86 | line-height: normal !important;
87 | font-size: 1rem !important;
88 | }
89 |
90 | .rat-Week_days {
91 | background-color: #fefffe !important;
92 | }
93 |
94 | .rat-Week_lines {
95 | background-color: #fefffe !important;
96 | }
97 |
98 | .rat-Day_component {
99 | border-left: 1px solid #e0e1e0 !important;
100 | }
101 |
102 | .rat-Day_mouseTarget {
103 | cursor: pointer !important;
104 | height: 75rem !important;
105 | }
--------------------------------------------------------------------------------
/src/styles/samay-rbc.scss:
--------------------------------------------------------------------------------
1 | .rbc-calendar {
2 | font-family: "Inter";
3 | }
4 |
5 | .rbc-event {
6 | background-color: #000000;
7 | }
8 |
9 | .rbc-day-slot .rbc-event, .rbc-day-slot .rbc-background-event {
10 | border: 1px solid #000000;
11 | display: inline-block;
12 | }
13 |
14 | .rbc-time-slot {
15 | text-align: center;
16 | }
17 |
18 | .rbc-day-slot .rbc-events-container {
19 | margin-right: 0;
20 | }
21 |
22 | .rbc-day-slot .rbc-event-label {
23 | padding-right: 0;
24 | margin: 0 auto;
25 | text-align: center;
26 | }
27 |
28 | .rbc-event-label {
29 | font-size: 75%;
30 | padding-top: 0.35rem;
31 | }
32 |
33 | .rbc-label {
34 | font-size: 0.8rem;
35 | color: #646464;
36 | }
37 |
38 | .rbc-timeslot-group {
39 | min-height: 69px;
40 | }
41 |
42 | .rbc-time-header-gutter {
43 | width: 54.2812px !important;
44 | min-width: 67.2812px !important;
45 | max-width: 62.2812px !important;
46 | }
47 |
48 | .rbc-header {
49 | border-bottom: 0;
50 | display: flex;
51 | align-items: center;
52 | justify-content: center;
53 | white-space: normal;
54 | background-color: #fefffe !important;
55 |
56 | @include media-breakpoint-up(sm) {
57 | white-space: nowrap;
58 | }
59 | }
60 |
61 | .rbc-slot-selection {
62 | text-align: center;
63 | padding-top: 0.48rem;
64 | border-radius: 5px;
65 | }
66 |
67 | .rbc-time-view .rbc-allday-cell {
68 | display: none;
69 | }
70 |
71 | .rbc-header + .rbc-header {
72 | border-left: 0;
73 | display: flex;
74 | align-items: center;
75 | justify-content: center;
76 | }
77 |
78 | .rbc-time-view .rbc-row {
79 | min-height: 44px;
80 | }
81 |
82 | .rbc-time-view {
83 | border-radius: 0.4rem;
84 | }
85 |
86 | .rbc-button-link {
87 | cursor: auto !important;
88 | color: #646464;
89 |
90 | &:focus {
91 | outline: 0 !important;
92 | }
93 | }
94 |
95 | .rbc-toolbar .rbc-toolbar-label {
96 | text-align: right;
97 | font-size: 0.9rem;
98 | color: #646464;
99 | }
100 |
101 | .rbc-today {
102 | background-color: #fefffe;
103 | }
104 |
105 | .rbc-time-header.rbc-overflowing {
106 | border-right: 0;
107 | }
108 |
109 | .rbc-current-time-indicator {
110 | background-color: #000000;
111 | }
112 |
113 | .rbc-toolbar button {
114 | color: #646464;
115 | font-size: 0.8rem;
116 | background-color: #fefffe;
117 |
118 | &:hover {
119 | background-color: #f9fafc;
120 | }
121 |
122 | &:focus {
123 | outline: 0 !important;
124 | background-color: #fefffe;
125 | }
126 | }
127 |
128 | .rbc-toolbar-label {
129 | display: none;
130 | @include media-breakpoint-up(sm) {
131 | display: unset;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/styles/voter-page.scss:
--------------------------------------------------------------------------------
1 | .voter-page-main {
2 | background-color: #f3f4f6;
3 | }
4 |
5 | .voter-page-main-container {
6 | background-color: #fefffe;
7 | border: 0.09rem solid #e5e7eb;
8 | padding: 1rem;
9 | border-radius: 0.5rem;
10 |
11 | @include media-breakpoint-up(lg) {
12 | width: 100%;
13 | overflow: auto;
14 | padding: 4rem;
15 | }
16 | }
17 |
18 | .voter-page-final-container {
19 | background-color: #fefffe;
20 | margin-top: 1rem;
21 |
22 | @include media-breakpoint-up(sm) {
23 | width: unset;
24 | display: flex;
25 | justify-content: flex-end;
26 | }
27 | }
28 |
29 | .voter-page-poll-mark-time-name {
30 | background-color: #ffffff;
31 | color: #101827;
32 | border: 0.09rem solid #e5e7eb;
33 | border-radius: 0.5rem;
34 | font-family: "Inter";
35 | font-size: 0.9rem;
36 | font-weight: 400;
37 | transition: none;
38 | width: 100%;
39 | margin-top: 1rem;
40 | margin-bottom: 1rem;
41 |
42 | @include media-breakpoint-up(sm) {
43 | margin-bottom: 0rem;
44 | margin-right: 1rem;
45 | width: 20%;
46 | margin-top: 0rem;
47 | }
48 | }
49 |
50 | .voter-page-poll-mark-time-name.form-control {
51 | display: inline-block !important;
52 | }
53 |
54 | .voter-page-poll-mark-time-name:hover {
55 | color: #101827;
56 | background-color: #ffffff;
57 | border: 0.09rem solid #b7b9bd;
58 | }
59 |
60 | .voter-page-poll-mark-time-name:focus {
61 | color: #101827;
62 | background-color: #ffffff;
63 | border: 0.1rem solid #101827;
64 | }
65 |
66 | .voter-page-poll-mark-time-name.logged-in {
67 | box-shadow: none;
68 | outline: none;
69 | }
70 |
71 | .voter-page-vote-recorded {
72 | font-family: "Inter";
73 | font-size: 0.7rem;
74 | display: block;
75 | font-weight: 500;
76 | color: #000;
77 | margin-top: 2rem;
78 |
79 | @include media-breakpoint-up(sm) {
80 | font-size: 1rem;
81 | }
82 | }
83 |
84 | .poll-vote-recorded-icon {
85 | margin-top: -0.2rem;
86 | margin-right: 0.5rem;
87 | color: #49de80;
88 | }
89 |
90 | .poll-info-final-time {
91 | font-family: "Inter";
92 | font-size: 0.7rem;
93 | display: block;
94 | font-weight: 500;
95 | color: #000;
96 | margin-top: 2rem;
97 |
98 | @include media-breakpoint-up(sm) {
99 | font-size: 1rem;
100 | }
101 | }
102 |
103 | .poll-info-final-time-decided-icon {
104 | margin-top: -0.2rem;
105 | margin-right: 0.5rem;
106 | color: #49de80;
107 | }
108 |
--------------------------------------------------------------------------------
/src/styles/your-polls.scss:
--------------------------------------------------------------------------------
1 | .no-poll-container {
2 | margin-top: 7rem;
3 | margin-bottom: 2rem;
4 | padding-left: 0.5rem;
5 | width: 95%;
6 | padding-right: 0.5rem;
7 | text-align: center;
8 |
9 | @include media-breakpoint-up(sm) {
10 | width: 100rem;
11 | padding-left: 0;
12 | padding-right: 0;
13 | }
14 |
15 | .icon {
16 | font-size: 4rem;
17 | background-color: #e5e7eb;
18 | border-radius: 1.5rem;
19 | padding: 1rem;
20 | }
21 |
22 | .first-line {
23 | display: block;
24 | font-family: "Cal Sans";
25 | font-size: 2rem;
26 | color: #101827;
27 | margin-top: 2rem;
28 | }
29 |
30 | .second-line {
31 | display: block;
32 | font-family: "Inter";
33 | font-size: 1rem;
34 | color: #636466;
35 | margin-top: 0.5rem;
36 | margin-bottom: 3rem;
37 | }
38 | }
39 |
40 | .poll-container {
41 | padding-left: 0.5rem;
42 | width: 95%;
43 | padding-right: 0.5rem;
44 | margin-top: 2rem;
45 | margin-bottom: 8rem;
46 |
47 | @include media-breakpoint-up(sm) {
48 | width: 75rem;
49 | margin-top: 7rem;
50 | padding-left: 0;
51 | padding-right: 0;
52 | }
53 | }
54 |
55 | .your-polls-polls-heading {
56 | font-size: 1.2rem;
57 | color: #565656;
58 | letter-spacing: 0.02rem;
59 | font-family: "Cal Sans";
60 | font-weight: 700;
61 | }
62 |
63 | .your-polls-poll-card {
64 | font-family: "Cal Sans";
65 | border-radius: 0.5rem;
66 | margin-top: 1.5rem;
67 | letter-spacing: 0.02rem;
68 | color: #101827;
69 | border: 0.1rem solid #e5e7ed;
70 | transition: 0.2s !important;
71 |
72 | a {
73 | color: #101827;
74 | text-decoration: none !important;
75 | text-decoration-color: #101827 !important;
76 |
77 | &:hover {
78 | color: #101827;
79 | text-decoration: none !important;
80 | text-decoration-color: #101827 !important;
81 | }
82 | }
83 |
84 | &:last-of-type {
85 | margin-bottom: 0rem;
86 | }
87 |
88 | .stretched-link {
89 | font-size: 1rem;
90 | }
91 |
92 | &:hover,
93 | &:active,
94 | &:focus {
95 | box-shadow: none !important;
96 | border: 0.09rem solid #f9f9f9 !important;
97 | background-color: #f9f9f9 !important;
98 | }
99 | }
100 |
101 | .card-options {
102 | float: right;
103 | }
104 |
105 | .option-button {
106 | background-color: #ffffff;
107 | padding: 0.3rem 0.8rem 0.3rem 0.8rem;
108 | border: 0.09rem solid #e5e7ed;
109 | margin-left: 0.5rem;
110 | transition: none !important;
111 |
112 | &:hover,
113 | &:active,
114 | &:focus {
115 | box-shadow: none !important;
116 | border: 0.09rem solid #e3e3e3 !important;
117 | background-color: #e3e3e3 !important;
118 | }
119 |
120 | .icon {
121 | color: #000000;
122 | margin-top: -0.3rem;
123 | font-size: 0.8rem;
124 | }
125 | }
126 |
127 | .trash-button {
128 | background-color: transparent;
129 | padding: 0.3rem;
130 | width: 2rem;
131 | height: 2rem;
132 | border: none;
133 | margin-left: 0.5rem;
134 | transition: 0.2s !important;
135 |
136 | &:hover,
137 | &:active,
138 | &:focus {
139 | box-shadow: none !important;
140 | border: none;
141 | background-color: #eaeaea !important;
142 | }
143 |
144 | .icon {
145 | color: #000000;
146 | margin-top: -0.25rem;
147 | font-size: 0.8rem;
148 | }
149 | }
150 |
151 | .modal-title {
152 | font-family: "Cal Sans";
153 | font-size: 1.4rem;
154 | letter-spacing: 0.03rem;
155 | color: #101827;
156 | }
157 |
158 | .modal-body {
159 | font-family: "Inter";
160 | font-size: 1rem;
161 | color: #555555;
162 | }
163 |
164 | .modal-header {
165 | border-bottom: 0;
166 | }
167 |
168 | .modal-footer {
169 | border-top: 0;
170 | }
171 |
172 | .modal-footer .btn {
173 | background-color: #fb2f2f;
174 | color: #ffffff;
175 | font-weight: 400;
176 | font-size: 0.9rem;
177 | font-family: "Inter";
178 | border-radius: 0.5rem;
179 | margin-top: 0.05rem;
180 | border: none;
181 | padding: 0.35rem 1rem 0.35rem 1rem;
182 | width: 100%;
183 |
184 | &:hover,
185 | &:active,
186 | &:focus {
187 | color: #ffffff !important;
188 | border: none !important;
189 | box-shadow: none !important;
190 | background-color: #ff4848 !important;
191 | }
192 |
193 | &[disabled] {
194 | opacity: 1 !important;
195 | box-shadow: none !important;
196 | color: #ffffff !important;
197 | border: none !important;
198 | background-color: #101827 !important;
199 | pointer-events: none;
200 | }
201 |
202 | @include media-breakpoint-up(sm) {
203 | width: unset;
204 | float: right;
205 | }
206 | }
207 |
208 | .modal-header .close {
209 | &:focus {
210 | outline: 0;
211 | border: 0px solid #fff;
212 | outline: none;
213 | }
214 |
215 | padding: unset;
216 | margin: unset;
217 | }
218 |
--------------------------------------------------------------------------------
/src/utils/api/server.ts:
--------------------------------------------------------------------------------
1 | import { Poll, Vote, HttpResponse, Time } from "../../models/poll";
2 |
3 | const NEXT_PUBLIC_BASE_URL =
4 | process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
5 |
6 | const httpMethod = async (
7 | endpoint: string,
8 | requestOptions: RequestInit
9 | ): Promise => {
10 | const res = await fetch(endpoint, requestOptions);
11 | const { status } = res;
12 | const responseData = await res.json();
13 | return {
14 | data: responseData,
15 | statusCode: status,
16 | };
17 | };
18 |
19 | const getPoll = (
20 | pollID: string | string[] | null | undefined
21 | ): Promise => {
22 | const endpoint = `${NEXT_PUBLIC_BASE_URL}/api/poll/${pollID}`;
23 | const requestOptions: RequestInit = {
24 | method: "GET",
25 | };
26 | return httpMethod(endpoint, requestOptions);
27 | };
28 |
29 | const createPoll = (pollArgs: { poll: Poll }): Promise => {
30 | const { poll } = pollArgs;
31 | const endpoint = `${NEXT_PUBLIC_BASE_URL}/api/poll/create`;
32 | const requestOptions: RequestInit = {
33 | method: "POST",
34 | body: JSON.stringify(poll),
35 | };
36 | return httpMethod(endpoint, requestOptions);
37 | };
38 |
39 | const markTimes = (voteArgs: {
40 | newVote: Vote;
41 | pollID: string;
42 | }): Promise => {
43 | const { newVote, pollID } = voteArgs;
44 | const endpoint = `${NEXT_PUBLIC_BASE_URL}/api/poll/${pollID}`;
45 | const requestOptions: RequestInit = {
46 | method: "PUT",
47 | body: JSON.stringify(newVote),
48 | };
49 | return httpMethod(endpoint, requestOptions);
50 | };
51 |
52 | const markFinalTime = (voteArgs: {
53 | finalTime: { finalTime: Time | undefined; open: boolean };
54 | pollID: string;
55 | secret: string;
56 | }): Promise => {
57 | const { finalTime, pollID, secret } = voteArgs;
58 | const endpoint = `${NEXT_PUBLIC_BASE_URL}/api/poll/${pollID}/${secret}`;
59 | const requestOptions: RequestInit = {
60 | method: "PUT",
61 | body: JSON.stringify(finalTime),
62 | };
63 | return httpMethod(endpoint, requestOptions);
64 | };
65 |
66 | const deletePoll = (deleteArgs: {
67 | pollID: string;
68 | secret: string;
69 | }): Promise => {
70 | const { pollID, secret } = deleteArgs;
71 | const endpoint = `${NEXT_PUBLIC_BASE_URL}/api/poll/${pollID}/${secret}`;
72 | const requestOptions: RequestInit = {
73 | method: "DELETE",
74 | };
75 | return httpMethod(endpoint, requestOptions);
76 | };
77 |
78 | export { getPoll, createPoll, markTimes, markFinalTime, deletePoll };
79 |
--------------------------------------------------------------------------------
/src/utils/db.ts:
--------------------------------------------------------------------------------
1 | import { connect, ConnectionOptions } from "mongoose";
2 |
3 | const { NEXT_MONGODB_URI } = process.env;
4 |
5 | const options: ConnectionOptions = {
6 | useUnifiedTopology: true,
7 | autoIndex: true,
8 | };
9 |
10 | const connectToDatabase = (): Promise =>
11 | connect(NEXT_MONGODB_URI, options);
12 |
13 | export default connectToDatabase;
14 |
--------------------------------------------------------------------------------
/src/utils/react-available-times.d.ts:
--------------------------------------------------------------------------------
1 | declare module "react-available-times";
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Next.js",
4 | "compilerOptions": {
5 | "target": "es5",
6 | "lib": [
7 | "dom",
8 | "dom.iterable",
9 | "esnext"
10 | ],
11 | "allowJs": true,
12 | "skipLibCheck": true,
13 | "strict": false,
14 | "forceConsistentCasingInFileNames": true,
15 | "noEmit": true,
16 | "incremental": true,
17 | "esModuleInterop": true,
18 | "module": "esnext",
19 | "moduleResolution": "node",
20 | "resolveJsonModule": true,
21 | "isolatedModules": true,
22 | "jsx": "preserve"
23 | },
24 | "include": [
25 | "next-env.d.ts",
26 | "**/*.ts",
27 | "**/*.tsx"
28 | ],
29 | "exclude": [
30 | "node_modules"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------