├── .commitlintrc.json
├── .env.example
├── .eslintrc.js
├── .gitignore
├── .husky
├── .gitignore
└── commit-msg
├── .lintstagedrc
├── .markdownlint.json
├── .prettierignore
├── .prettierrc
├── README.md
├── jsconfig.json
├── next-sitemap.config.js
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── favicon.png
├── favicon.svg
├── fonts
│ ├── brother-1816
│ │ ├── brother-1816-book.woff2
│ │ ├── brother-1816-light.woff2
│ │ ├── brother-1816-medium.woff2
│ │ └── brother-1816-regular.woff2
│ └── ibm-plex-mono
│ │ └── ibm-plex-mono-regular.woff2
├── images
│ ├── hero.jpeg
│ ├── mobile-1.svg
│ ├── noise.png
│ └── social-preview.jpg
├── robots.txt
├── sitemap-0.xml
└── sitemap.xml
├── src
├── app
│ ├── (category)
│ │ └── [category-slug]
│ │ │ ├── (sub-category)
│ │ │ └── [sub-category-slug]
│ │ │ │ ├── head.jsx
│ │ │ │ ├── layout.jsx
│ │ │ │ └── page.jsx
│ │ │ ├── head.jsx
│ │ │ └── page.jsx
│ ├── head.jsx
│ ├── layout.jsx
│ └── page.jsx
├── components
│ ├── pages
│ │ └── sub-category
│ │ │ ├── mobile
│ │ │ ├── arrow-left.inline.svg
│ │ │ ├── arrow-right.inline.svg
│ │ │ ├── card.inline.svg
│ │ │ ├── close.inline.svg
│ │ │ ├── index.js
│ │ │ └── mobile.jsx
│ │ │ └── template-info
│ │ │ ├── index.js
│ │ │ └── template-info.jsx
│ └── shared
│ │ ├── button
│ │ ├── button.jsx
│ │ └── index.js
│ │ ├── category-card
│ │ ├── category-card.jsx
│ │ └── index.js
│ │ ├── dialogue
│ │ ├── close.inline.svg
│ │ ├── dialogue.jsx
│ │ └── index.js
│ │ ├── footer
│ │ ├── footer.jsx
│ │ └── index.js
│ │ ├── head-meta-tags
│ │ ├── head-meta-tags.jsx
│ │ └── index.js
│ │ ├── header
│ │ ├── header.jsx
│ │ └── index.js
│ │ ├── link
│ │ ├── index.js
│ │ └── link.jsx
│ │ └── mobile-menu
│ │ ├── index.js
│ │ └── mobile-menu.jsx
├── constants
│ ├── links.js
│ ├── menus.js
│ └── seo.js
├── hooks
│ └── .gitkeep
├── images
│ ├── arrow.inline.svg
│ ├── cards
│ │ └── airplane.inline.svg
│ ├── chatgpt.inline.svg
│ └── logo.inline.svg
├── layouts
│ └── layout-main
│ │ ├── index.js
│ │ └── layout-main.jsx
├── styles
│ ├── container.css
│ ├── fonts.css
│ ├── global.css
│ ├── grid-gap.css
│ ├── main.css
│ ├── remove-autocomplete-styles.css
│ ├── safe-paddings.css
│ └── scrollbar-hidden.css
└── utils
│ ├── .gitkeep
│ ├── api
│ └── queries.js
│ └── index.js
└── tailwind.config.js
/.commitlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@commitlint/config-conventional"],
3 | "rules": {
4 | "scope-enum": [
5 | 2,
6 | "always",
7 | [
8 | "components",
9 | "constants",
10 | "hooks",
11 | "icons",
12 | "images",
13 | "pages",
14 | "styles",
15 | "templates",
16 | "utils"
17 | ]
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | SITE_URL=
2 | REACT_APP_API=
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es6: true,
5 | },
6 | extends: [
7 | 'airbnb',
8 | 'airbnb/hooks',
9 | 'airbnb/whitespace',
10 | 'prettier',
11 | 'plugin:@next/next/recommended',
12 | ],
13 | globals: {
14 | Atomics: 'readonly',
15 | SharedArrayBuffer: 'readonly',
16 | },
17 | parserOptions: {
18 | ecmaFeatures: {
19 | jsx: true,
20 | },
21 | ecmaVersion: 2020,
22 | sourceType: 'module',
23 | },
24 | plugins: ['react'],
25 | rules: {
26 | // Removes "default" from "restrictedNamedExports", original rule setup — https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb-base/rules/es6.js#L65
27 | 'no-restricted-exports': ['error', { restrictedNamedExports: ['then'] }],
28 | 'no-unused-vars': 'error',
29 | 'no-shadow': 'off',
30 | 'no-undef': 'error',
31 | 'react/prop-types': 'error',
32 | 'react/no-array-index-key': 'off',
33 | 'react/jsx-props-no-spreading': 'off',
34 | 'react/no-danger': 'off',
35 | 'react/react-in-jsx-scope': 'off',
36 | 'react/forbid-prop-types': 'off',
37 | // Changes values from "function-expression" to "arrow-function", original rule setup — https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb/rules/react.js#L528
38 | 'react/function-component-definition': [
39 | 'error',
40 | {
41 | namedComponents: 'arrow-function',
42 | unnamedComponents: 'arrow-function',
43 | },
44 | ],
45 | 'react/jsx-sort-props': [
46 | 'error',
47 | {
48 | callbacksLast: true,
49 | shorthandLast: true,
50 | noSortAlphabetically: true,
51 | },
52 | ],
53 | 'import/order': [
54 | 'error',
55 | {
56 | groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object'],
57 | 'newlines-between': 'always',
58 | alphabetize: {
59 | order: 'asc',
60 | caseInsensitive: true,
61 | },
62 | },
63 | ],
64 | 'jsx-a11y/label-has-associated-control': [
65 | 'error',
66 | {
67 | required: {
68 | some: ['nesting', 'id'],
69 | },
70 | },
71 | ],
72 | 'jsx-a11y/label-has-for': [
73 | 'error',
74 | {
75 | required: {
76 | some: ['nesting', 'id'],
77 | },
78 | },
79 | ],
80 | },
81 | settings: {
82 | 'import/resolver': {
83 | node: {
84 | paths: ['src'],
85 | },
86 | },
87 | },
88 | };
89 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*
29 | !.env.example
30 |
31 | # vercel
32 | .vercel
33 | .vscode/snipsnap.code-snippets
34 |
35 | #linters
36 | .stylelintcache
37 | .eslintcache
38 |
39 | .idea/
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit ""
5 |
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
1 | {
2 | "*.{js,jsx,html,css,md}": "prettier --write",
3 | "*.{js,jsx}": "eslint --cache --fix",
4 | }
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "line-length": false,
3 | "ol-prefix": false
4 | }
5 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | package.json
2 | package-lock.json
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "quoteProps": "as-needed",
8 | "jsxSingleQuote": false,
9 | "trailingComma": "es5",
10 | "bracketSpacing": true,
11 | "jsxBracketSameLine": false,
12 | "arrowParens": "always",
13 | "endOfLine": "lf"
14 | }
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pixel Point Next.js Tailwind Starter
2 |
3 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
4 |
5 | ## Table of Contents
6 |
7 | - [Getting Started](#getting-started)
8 | - [Usage](#usage)
9 | - [Learn more](#learn-more)
10 | - [Build the website](#deploy-on-vercel)
11 | - [Project Structure](#project-structure)
12 | - [Code Style](#code-style)
13 | - [ESLint](#eslint)
14 | - [Prettier](#prettier)
15 | - [VS Code](#vs-code)
16 |
17 | ## Getting Started
18 |
19 | 1. Clone this repository or hit "Use this template" button
20 |
21 | ```bash
22 | git clone git@github.com:pixel-point/nextjs-tailwind-starter.git
23 | ```
24 |
25 | 2. Install dependencies
26 |
27 | ```bash
28 | npm install
29 | ```
30 |
31 | ## Usage
32 |
33 | ```bash
34 | npm run dev
35 | ```
36 |
37 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
38 |
39 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
40 |
41 | ### Learn More
42 |
43 | To learn more about Next.js, take a look at the following resources:
44 |
45 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
46 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
47 |
48 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
49 |
50 | ### Deploy on Vercel
51 |
52 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
53 |
54 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
55 |
56 | ## Project Structure
57 |
58 | ```text
59 | ├── public
60 | ├── src
61 | │ ├── components
62 | │ │ ├── pages — React components that are being used specifically on a certain page
63 | │ │ └── shared — React components that are being used across the whole website
64 | │ ├── hooks
65 | │ ├── images
66 | │ ├── pages
67 | │ ├── styles
68 | │ ├── utils
69 | ├── next.config.js — Main configuration file for a Next.js site. Read more about it [here](https://nextjs.org/docs/api-reference/next.config.js/introduction)
70 | ├── postcss.config.js — Main configuration file of PostCSS. [Read more about it here](https://tailwindcss.com/docs/configuration#generating-a-post-css-configuration-file)
71 | └── tailwind.config.js — Main configuration file for Tailwind CSS [Read more about it here](https://tailwindcss.com/docs/configuration)
72 | ```
73 |
74 | ## Component Folder Structure
75 |
76 | ### Each component includes
77 |
78 | 1. Main JavaScript File
79 | 2. Index File
80 |
81 | ### Each component optionally may include
82 |
83 | 1. Folder with images and icons
84 | 2. Folder with data
85 |
86 | Also, each component may include another component that follows all above listed rules.
87 |
88 | ### Example structure
89 |
90 | ```bash
91 | component
92 | ├── nested-component
93 | │ ├── data
94 | │ │ └── nested-component-lottie-data.json
95 | │ ├── images
96 | │ │ ├── nested-component-image.jpg
97 | │ │ ├── nested-component-inline-svg.inline.svg
98 | │ │ └── nested-component-url-svg.url.svg
99 | │ ├── nested-component.js
100 | │ └── index.js
101 | ├── data
102 | │ └── component-lottie-data.json
103 | ├── images
104 | │ ├── component-image.jpg
105 | │ ├── component-inline-svg.inline.svg
106 | │ └── component-url-svg.url.svg
107 | ├── component.js
108 | └── index.js
109 | ```
110 |
111 | ## Code Style
112 |
113 | ### ESLint
114 |
115 | [ESLint](https://eslint.org/) helps find and fix code style issues and force developers to follow same rules. Current configuration is based on [Airbnb style guide](https://github.com/airbnb/javascript).
116 |
117 | Additional commands:
118 |
119 | ```bash
120 | npm run lint
121 | ```
122 |
123 | Run it to check the current status of eslint issues across project.
124 |
125 | ```bash
126 | npm run lint:fix
127 | ```
128 |
129 | Run it to fix all possible issues.
130 |
131 | ### Prettier
132 |
133 | [Prettier](https://prettier.io/) helps to format code based on defined rules. [Difference between Prettier and ESLint](https://prettier.io/docs/en/comparison.html).
134 |
135 | Additional commands:
136 |
137 | ```bash
138 | npm run format
139 | ```
140 |
141 | Run it to format all files across the project.
142 |
143 | ### VS Code
144 |
145 | Following extensions required to simplify the process of keeping the same code style across the project:
146 |
147 | - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
148 | - [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
149 |
150 | After installation enable "ESLint on save" by adding to your VS Code settings.json the following line:
151 |
152 | ```json
153 | "editor.codeActionsOnSave": {
154 | "source.fixAll.eslint": true
155 | }
156 | ```
157 |
158 | You can navigate to settings.json by using Command Pallete (CMD+Shift+P) and then type "Open settings.json".
159 |
160 | To enable Prettier go to Preferences -> Settings -> type "Format". Then check that you have esbenp.prettier-vscode as default formatter, and also enable "Format On Save".
161 |
162 | Reload VS Code and auto-format will work for you.
163 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "jsx": "react"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/next-sitemap.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | siteUrl: process.env.SITE_URL || 'https://notifications.directory',
3 | generateRobotsTxt: true, // (optional)\
4 | sitemapSize: 7000,
5 | // ...other options
6 | };
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | eslint: {
4 | ignoreDuringBuilds: true,
5 | },
6 | experimental: {
7 | appDir: true,
8 | },
9 | webpack(config) {
10 | // https://github.com/vercel/next.js/issues/25950#issuecomment-863298702
11 | const fileLoaderRule = config.module.rules.find((rule) => {
12 | if (rule.test instanceof RegExp) {
13 | return rule.test.test('.svg');
14 | }
15 | return null;
16 | });
17 |
18 | fileLoaderRule.exclude = /\.svg$/;
19 |
20 | config.module.rules.push({
21 | test: /\.inline.svg$/,
22 | use: [
23 | {
24 | loader: '@svgr/webpack',
25 | options: {
26 | svgo: true,
27 | svgoConfig: {
28 | plugins: [
29 | {
30 | name: 'preset-default',
31 | params: {
32 | overrides: {
33 | removeViewBox: false,
34 | },
35 | },
36 | },
37 | 'prefixIds',
38 | 'removeDimensions',
39 | ],
40 | },
41 | },
42 | },
43 | ],
44 | });
45 |
46 | return config;
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-starter",
3 | "version": "0.1.0",
4 | "private": true,
5 | "engines": {
6 | "node": ">=16.8.0",
7 | "npm": ">=8.6.0"
8 | },
9 | "scripts": {
10 | "dev": "next dev",
11 | "build": "next build",
12 | "postbuild": "next-sitemap",
13 | "start": "next start",
14 | "format": "prettier --write .",
15 | "lint": "npm run lint:js && npm run lint:md",
16 | "lint:fix": "npm run lint:js:fix",
17 | "lint:js": "eslint --ext .js,.jsx --ignore-path .gitignore .",
18 | "lint:js:fix": "eslint --fix --ext .js,.jsx --ignore-path .gitignore .",
19 | "lint:md": "markdownlint --ignore-path .gitignore .",
20 | "lint:md:fix": "markdownlint --fix --ignore-path .gitignore .",
21 | "prepare": "husky install"
22 | },
23 | "dependencies": {
24 | "@radix-ui/react-dialog": "^1.0.2",
25 | "clsx": "^1.2.1",
26 | "file-loader": "^6.2.0",
27 | "formik": "^2.2.9",
28 | "framer-motion": "^8.5.0",
29 | "markdownlint-cli": "^0.33.0",
30 | "next": "^13.1.1",
31 | "next-sitemap": "^3.1.52",
32 | "nextjs-google-analytics": "^2.3.0",
33 | "prop-types": "^15.8.1",
34 | "react": "^18.2.0",
35 | "react-dom": "^18.2.0",
36 | "slugify": "^1.6.5",
37 | "tailwindcss": "^3.2.4",
38 | "tailwindcss-safe-area": "^0.2.2"
39 | },
40 | "devDependencies": {
41 | "@commitlint/cli": "^17.4.1",
42 | "@commitlint/config-conventional": "^17.4.0",
43 | "@next/eslint-plugin-next": "^13.1.1",
44 | "@svgr/webpack": "^6.5.1",
45 | "autoprefixer": "^10.4.13",
46 | "eslint": "^8.31.0",
47 | "eslint-config-airbnb": "^19.0.4",
48 | "eslint-config-prettier": "^8.6.0",
49 | "eslint-plugin-import": "^2.27.4",
50 | "eslint-plugin-jsx-a11y": "^6.7.1",
51 | "eslint-plugin-react": "^7.32.0",
52 | "eslint-plugin-react-hooks": "^4.6.0",
53 | "husky": "^8.0.3",
54 | "lint-staged": "^13.1.0",
55 | "postcss": "^8.4.21",
56 | "postcss-import": "^15.1.0",
57 | "prettier": "^2.8.2",
58 | "prettier-plugin-tailwindcss": "^0.2.1",
59 | "svgo-loader": "^3.0.3",
60 | "url-loader": "^4.1.1"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: ['postcss-import', 'tailwindcss/nesting', 'tailwindcss', 'autoprefixer'],
3 | };
4 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novuhq/notification-directory-react/ae92a575c63aaa158cf11dc83e9fad77b54779f4/public/favicon.png
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
27 |
28 |
--------------------------------------------------------------------------------
/public/fonts/brother-1816/brother-1816-book.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novuhq/notification-directory-react/ae92a575c63aaa158cf11dc83e9fad77b54779f4/public/fonts/brother-1816/brother-1816-book.woff2
--------------------------------------------------------------------------------
/public/fonts/brother-1816/brother-1816-light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novuhq/notification-directory-react/ae92a575c63aaa158cf11dc83e9fad77b54779f4/public/fonts/brother-1816/brother-1816-light.woff2
--------------------------------------------------------------------------------
/public/fonts/brother-1816/brother-1816-medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novuhq/notification-directory-react/ae92a575c63aaa158cf11dc83e9fad77b54779f4/public/fonts/brother-1816/brother-1816-medium.woff2
--------------------------------------------------------------------------------
/public/fonts/brother-1816/brother-1816-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novuhq/notification-directory-react/ae92a575c63aaa158cf11dc83e9fad77b54779f4/public/fonts/brother-1816/brother-1816-regular.woff2
--------------------------------------------------------------------------------
/public/fonts/ibm-plex-mono/ibm-plex-mono-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novuhq/notification-directory-react/ae92a575c63aaa158cf11dc83e9fad77b54779f4/public/fonts/ibm-plex-mono/ibm-plex-mono-regular.woff2
--------------------------------------------------------------------------------
/public/images/hero.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novuhq/notification-directory-react/ae92a575c63aaa158cf11dc83e9fad77b54779f4/public/images/hero.jpeg
--------------------------------------------------------------------------------
/public/images/mobile-1.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
23 |
28 |
29 |
35 |
41 |
42 |
51 |
52 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/public/images/noise.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novuhq/notification-directory-react/ae92a575c63aaa158cf11dc83e9fad77b54779f4/public/images/noise.png
--------------------------------------------------------------------------------
/public/images/social-preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novuhq/notification-directory-react/ae92a575c63aaa158cf11dc83e9fad77b54779f4/public/images/social-preview.jpg
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # *
2 | User-agent: *
3 | Allow: /
4 |
5 | # Host
6 | Host: https://notifications.directory
7 |
8 | # Sitemaps
9 | Sitemap: https://notifications.directory/sitemap.xml
10 |
--------------------------------------------------------------------------------
/public/sitemap-0.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://notifications.directory 2023-02-16T22:12:18.455Z daily 0.7
4 | https://notifications.directory/social-media 2023-02-16T22:12:18.455Z daily 0.7
5 | https://notifications.directory/news-and-journalism 2023-02-16T22:12:18.455Z daily 0.7
6 | https://notifications.directory/e-commerce 2023-02-16T22:12:18.455Z daily 0.7
7 | https://notifications.directory/online-marketplace 2023-02-16T22:12:18.455Z daily 0.7
8 | https://notifications.directory/travel-and-booking 2023-02-16T22:12:18.455Z daily 0.7
9 | https://notifications.directory/online-streaming 2023-02-16T22:12:18.455Z daily 0.7
10 | https://notifications.directory/online-gaming 2023-02-16T22:12:18.455Z daily 0.7
11 | https://notifications.directory/online-education 2023-02-16T22:12:18.455Z daily 0.7
12 | https://notifications.directory/job-listing 2023-02-16T22:12:18.455Z daily 0.7
13 | https://notifications.directory/real-estate 2023-02-16T22:12:18.455Z daily 0.7
14 | https://notifications.directory/food-delivery 2023-02-16T22:12:18.455Z daily 0.7
15 | https://notifications.directory/health-and-fitness 2023-02-16T22:12:18.455Z daily 0.7
16 | https://notifications.directory/beauty-and-personal-care 2023-02-16T22:12:18.455Z daily 0.7
17 | https://notifications.directory/home-and-garden 2023-02-16T22:12:18.455Z daily 0.7
18 | https://notifications.directory/fashion-and-clothing 2023-02-16T22:12:18.455Z daily 0.7
19 | https://notifications.directory/electronics-and-technology 2023-02-16T22:12:18.455Z daily 0.7
20 | https://notifications.directory/cars-and-automotive 2023-02-16T22:12:18.455Z daily 0.7
21 | https://notifications.directory/sports-and-outdoors 2023-02-16T22:12:18.455Z daily 0.7
22 | https://notifications.directory/music-and-entertainment 2023-02-16T22:12:18.455Z daily 0.7
23 | https://notifications.directory/art-and-design 2023-02-16T22:12:18.455Z daily 0.7
24 | https://notifications.directory/photography 2023-02-16T22:12:18.455Z daily 0.7
25 | https://notifications.directory/cooking-and-recipe 2023-02-16T22:12:18.455Z daily 0.7
26 | https://notifications.directory/diy-and-crafting 2023-02-16T22:12:18.455Z daily 0.7
27 | https://notifications.directory/gardening-and-agriculture 2023-02-16T22:12:18.455Z daily 0.7
28 | https://notifications.directory/pet-care 2023-02-16T22:12:18.455Z daily 0.7
29 | https://notifications.directory/parenting-and-family 2023-02-16T22:12:18.455Z daily 0.7
30 | https://notifications.directory/personal-finance-and-banking 2023-02-16T22:12:18.455Z daily 0.7
31 | https://notifications.directory/legal-services 2023-02-16T22:12:18.455Z daily 0.7
32 | https://notifications.directory/insurance 2023-02-16T22:12:18.455Z daily 0.7
33 | https://notifications.directory/health-insurance 2023-02-16T22:12:18.455Z daily 0.7
34 | https://notifications.directory/life-insurance 2023-02-16T22:12:18.455Z daily 0.7
35 | https://notifications.directory/pet-insurance 2023-02-16T22:12:18.455Z daily 0.7
36 | https://notifications.directory/travel-insurance 2023-02-16T22:12:18.455Z daily 0.7
37 | https://notifications.directory/home-insurance 2023-02-16T22:12:18.455Z daily 0.7
38 | https://notifications.directory/auto-insurance 2023-02-16T22:12:18.455Z daily 0.7
39 | https://notifications.directory/investment-and-trading 2023-02-16T22:12:18.455Z daily 0.7
40 | https://notifications.directory/stock-market 2023-02-16T22:12:18.455Z daily 0.7
41 | https://notifications.directory/cryptocurrency 2023-02-16T22:12:18.455Z daily 0.7
42 | https://notifications.directory/forex 2023-02-16T22:12:18.455Z daily 0.7
43 | https://notifications.directory/mutual-funds 2023-02-16T22:12:18.455Z daily 0.7
44 | https://notifications.directory/retirement-planning 2023-02-16T22:12:18.455Z daily 0.7
45 | https://notifications.directory/credit-card 2023-02-16T22:12:18.455Z daily 0.7
46 | https://notifications.directory/personal-loans 2023-02-16T22:12:18.455Z daily 0.7
47 | https://notifications.directory/mortgage-and-refinancing 2023-02-16T22:12:18.455Z daily 0.7
48 | https://notifications.directory/student-loans 2023-02-16T22:12:18.455Z daily 0.7
49 | https://notifications.directory/banking-and-checking 2023-02-16T22:12:18.455Z daily 0.7
50 | https://notifications.directory/credit-score-and-report 2023-02-16T22:12:18.455Z daily 0.7
51 | https://notifications.directory/debt-consolidation 2023-02-16T22:12:18.455Z daily 0.7
52 | https://notifications.directory/tax-preparation 2023-02-16T22:12:18.455Z daily 0.7
53 | https://notifications.directory/legal-documents-and-contracts 2023-02-16T22:12:18.455Z daily 0.7
54 | https://notifications.directory/business-services 2023-02-16T22:12:18.455Z daily 0.7
55 | https://notifications.directory/marketing-and-advertising 2023-02-16T22:12:18.455Z daily 0.7
56 | https://notifications.directory/public-relations 2023-02-16T22:12:18.455Z daily 0.7
57 | https://notifications.directory/human-resources 2023-02-16T22:12:18.455Z daily 0.7
58 | https://notifications.directory/project-management 2023-02-16T22:12:18.455Z daily 0.7
59 | https://notifications.directory/customer-relationship-management 2023-02-16T22:12:18.455Z daily 0.7
60 | https://notifications.directory/data-analysis-and-reporting 2023-02-16T22:12:18.455Z daily 0.7
61 | https://notifications.directory/cloud-storage-and-computing 2023-02-16T22:12:18.455Z daily 0.7
62 | https://notifications.directory/virtual-events-and-webinars 2023-02-16T22:12:18.455Z daily 0.7
63 | https://notifications.directory/online-meetings-and-video-conferencing 2023-02-16T22:12:18.455Z daily 0.7
64 | https://notifications.directory/productivity-and-time-management 2023-02-16T22:12:18.455Z daily 0.7
65 | https://notifications.directory/collaboration-and-teamwork 2023-02-16T22:12:18.455Z daily 0.7
66 | https://notifications.directory/presentation-and-slide-deck 2023-02-16T22:12:18.455Z daily 0.7
67 | https://notifications.directory/document-editing-and-formatting 2023-02-16T22:12:18.455Z daily 0.7
68 | https://notifications.directory/online-booking-and-scheduling 2023-02-16T22:12:18.455Z daily 0.7
69 | https://notifications.directory/customer-service-and-support 2023-02-16T22:12:18.455Z daily 0.7
70 | https://notifications.directory/crm-and-sales 2023-02-16T22:12:18.455Z daily 0.7
71 | https://notifications.directory/hr-and-payroll 2023-02-16T22:12:18.455Z daily 0.7
72 | https://notifications.directory/erp-and-supply-chain-management 2023-02-16T22:12:18.455Z daily 0.7
73 | https://notifications.directory/project-and-task-management 2023-02-16T22:12:18.455Z daily 0.7
74 | https://notifications.directory/account-management-and-billing 2023-02-16T22:12:18.455Z daily 0.7
75 | https://notifications.directory/time-tracking-and-invoicing 2023-02-16T22:12:18.455Z daily 0.7
76 | https://notifications.directory/e-signature-and-document-management 2023-02-16T22:12:18.455Z daily 0.7
77 | https://notifications.directory/learning-management-system 2023-02-16T22:12:18.455Z daily 0.7
78 | https://notifications.directory/online-courses-and-training 2023-02-16T22:12:18.455Z daily 0.7
79 | https://notifications.directory/language-learning 2023-02-16T22:12:18.455Z daily 0.7
80 | https://notifications.directory/tutoring-and-coaching 2023-02-16T22:12:18.455Z daily 0.7
81 | https://notifications.directory/professional-development 2023-02-16T22:12:18.455Z daily 0.7
82 | https://notifications.directory/certification-and-accreditation 2023-02-16T22:12:18.455Z daily 0.7
83 | https://notifications.directory/testing-and-assessment 2023-02-16T22:12:18.455Z daily 0.7
84 | https://notifications.directory/student-information-system 2023-02-16T22:12:18.455Z daily 0.7
85 | https://notifications.directory/library-management 2023-02-16T22:12:18.455Z daily 0.7
86 | https://notifications.directory/research-and-development 2023-02-16T22:12:18.455Z daily 0.7
87 | https://notifications.directory/scientific-and-technical 2023-02-16T22:12:18.455Z daily 0.7
88 | https://notifications.directory/medical-and-healthcare 2023-02-16T22:12:18.455Z daily 0.7
89 | https://notifications.directory/mental-health-and-therapy 2023-02-16T22:12:18.455Z daily 0.7
90 | https://notifications.directory/dentistry-and-orthodontics 2023-02-16T22:12:18.455Z daily 0.7
91 | https://notifications.directory/optometry-and-eye-care 2023-02-16T22:12:18.455Z daily 0.7
92 | https://notifications.directory/pediatrics-and-child-care 2023-02-16T22:12:18.455Z daily 0.7
93 | https://notifications.directory/geriatrics-and-senior-care 2023-02-16T22:12:18.455Z daily 0.7
94 | https://notifications.directory/physical-therapy-and-rehabilitation 2023-02-16T22:12:18.455Z daily 0.7
95 | https://notifications.directory/nutrition-and-diet 2023-02-16T22:12:18.455Z daily 0.7
96 | https://notifications.directory/alternative-and-complementary-medicine 2023-02-16T22:12:18.455Z daily 0.7
97 | https://notifications.directory/veterinary-and-pet-care 2023-02-16T22:12:18.455Z daily 0.7
98 | https://notifications.directory/agriculture-and-forestry 2023-02-16T22:12:18.455Z daily 0.7
99 | https://notifications.directory/environmental-science 2023-02-16T22:12:18.455Z daily 0.7
100 | https://notifications.directory/geology-and-earth-science 2023-02-16T22:12:18.455Z daily 0.7
101 | https://notifications.directory/astronomy-and-space 2023-02-16T22:12:18.455Z daily 0.7
102 | https://notifications.directory/meteorology-and-weather 2023-02-16T22:12:18.455Z daily 0.7
103 | https://notifications.directory/oceanography-and-marine-biology 2023-02-16T22:12:18.455Z daily 0.7
104 | https://notifications.directory/natural-history-and-conservation 2023-02-16T22:12:18.455Z daily 0.7
105 |
--------------------------------------------------------------------------------
/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://notifications.directory/sitemap-0.xml
4 |
--------------------------------------------------------------------------------
/src/app/(category)/[category-slug]/(sub-category)/[sub-category-slug]/head.jsx:
--------------------------------------------------------------------------------
1 | import HeadMetaTags from 'components/shared/head-meta-tags';
2 | import { MAIN_TITLE, SEPARATOR } from 'constants/seo';
3 | import {
4 | addSlugToCategories,
5 | addSlugToSubCategories,
6 | findCategoryBySlug,
7 | findSubCategoryBySlug,
8 | } from 'utils';
9 | import { getCategories, getSubCategories } from 'utils/api/queries';
10 |
11 | const Head = async ({
12 | params: { 'category-slug': categorySlug, 'sub-category-slug': subCategorySlug },
13 | }) => {
14 | let categories = await getCategories();
15 | categories = addSlugToCategories(categories);
16 | const matchingCategory = findCategoryBySlug(categories, categorySlug);
17 | if (!matchingCategory) {
18 | return <> >;
19 | }
20 | // eslint-disable-next-line no-underscore-dangle
21 | let subCategories = await getSubCategories(matchingCategory?._id);
22 |
23 | subCategories = addSlugToSubCategories(subCategories);
24 |
25 | const matchingSubCategory = findSubCategoryBySlug(subCategories, subCategorySlug);
26 |
27 | const title = `${MAIN_TITLE}${SEPARATOR}${matchingCategory.category}${SEPARATOR}${matchingSubCategory.subCategory}`;
28 | return ;
29 | };
30 |
31 | export default Head;
32 |
--------------------------------------------------------------------------------
/src/app/(category)/[category-slug]/(sub-category)/[sub-category-slug]/layout.jsx:
--------------------------------------------------------------------------------
1 | import Link from 'components/shared/link';
2 | import {
3 | addSlugToCategories,
4 | addSlugToSubCategories,
5 | findCategoryBySlug,
6 | findSubCategoryBySlug,
7 | } from 'utils';
8 | import { getCategories, getSubCategories } from 'utils/api/queries';
9 |
10 | const SubCategoryLayout = async ({
11 | children,
12 | params: { 'category-slug': categorySlug, 'sub-category-slug': subCategorySlug },
13 | }) => {
14 | let categories = await getCategories();
15 | categories = addSlugToCategories(categories);
16 | const matchingCategory = findCategoryBySlug(categories, categorySlug);
17 | if (!matchingCategory) {
18 | return <> >;
19 | }
20 | // eslint-disable-next-line no-underscore-dangle
21 | let subCategories = await getSubCategories(matchingCategory?._id);
22 |
23 | subCategories = addSlugToSubCategories(subCategories);
24 |
25 | const matchingSubCategory = findSubCategoryBySlug(subCategories, subCategorySlug);
26 | if (!matchingSubCategory) {
27 | return <> >;
28 | }
29 |
30 | // Exclude from subCategories the one that is currently being displayed
31 | const otherSubCategories = subCategories.filter(
32 | (subCategory) => subCategory.slug !== subCategorySlug
33 | );
34 |
35 | return (
36 |
37 |
38 |
39 | {children}
40 |
41 |
42 | Other notification types for Social Media
43 |
44 |
45 | {otherSubCategories.map((subCategory, index) => (
46 |
47 | {subCategory.subCategory}
48 |
49 | ))}
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export default SubCategoryLayout;
57 |
58 | export const revalidate = 60;
59 |
--------------------------------------------------------------------------------
/src/app/(category)/[category-slug]/(sub-category)/[sub-category-slug]/page.jsx:
--------------------------------------------------------------------------------
1 | import slugify from 'slugify';
2 |
3 | import TemplateInfo from 'components/pages/sub-category/template-info';
4 | import {
5 | addSlugToCategories,
6 | addSlugToSubCategories,
7 | findCategoryBySlug,
8 | findSubCategoryBySlug,
9 | } from 'utils';
10 | import { getCategories, getSubCategories, getNotifications } from 'utils/api/queries';
11 |
12 | const SubCategorySlug = async ({ params }) => {
13 | if (!params['category-slug'] || !params['sub-category-slug']) {
14 | return <> >;
15 | }
16 |
17 | const { 'category-slug': categorySlug, 'sub-category-slug': subCategorySlug } = params;
18 |
19 | let categories = await getCategories();
20 | categories = addSlugToCategories(categories);
21 | const matchingCategory = findCategoryBySlug(categories, categorySlug);
22 | if (!matchingCategory) {
23 | return <> >;
24 | }
25 | // eslint-disable-next-line no-underscore-dangle
26 | let subCategories = await getSubCategories(matchingCategory._id);
27 |
28 | subCategories = addSlugToSubCategories(subCategories);
29 |
30 | const matchingSubCategory = findSubCategoryBySlug(subCategories, subCategorySlug);
31 |
32 | const notifications = await getNotifications(matchingSubCategory._id);
33 |
34 | return (
35 |
40 | );
41 | };
42 |
43 | export async function generateStaticParams() {
44 | const categories = (await getCategories()).filter((f) => f._id && f.category);
45 | const getAll = (
46 | await Promise.all(
47 | categories
48 | .filter((f) => f.category && typeof f.category === 'string')
49 | .map(async (c) => {
50 | const sub = await getSubCategories(c._id);
51 | if (!Array.isArray(sub)) {
52 | return [];
53 | }
54 | return (
55 | await Promise.all(
56 | sub
57 | .filter((f) => f.subCategory)
58 | .map(async (current) => {
59 | const notifications = await getNotifications(current._id);
60 | if (!Array.isArray(notifications) || notifications?.length === 0) {
61 | return [];
62 | }
63 | if (notifications.some((f) => !f.notification)) {
64 | return [];
65 | }
66 | const cat = slugify(c.category, { lower: true });
67 | const subd = slugify(current.subCategory, { lower: true });
68 | if (!cat || !subd || typeof cat !== 'string' || typeof subd !== 'string') {
69 | return [];
70 | }
71 | return [
72 | {
73 | 'category-slug': cat,
74 | 'sub-category-slug': subd,
75 | },
76 | ];
77 | }, [])
78 | )
79 | ).reduce((all, current) => [...all, ...current], []);
80 | })
81 | )
82 | ).reduce((all, current) => [...all, ...current], []);
83 |
84 | return getAll;
85 | }
86 |
87 | export default SubCategorySlug;
88 |
89 | export const revalidate = 60;
90 |
--------------------------------------------------------------------------------
/src/app/(category)/[category-slug]/head.jsx:
--------------------------------------------------------------------------------
1 | import HeadMetaTags from 'components/shared/head-meta-tags';
2 | import { MAIN_TITLE, SEPARATOR } from 'constants/seo';
3 | import {
4 | addSlugToCategories,
5 | addSlugToSubCategories,
6 | findCategoryBySlug,
7 | findSubCategoryBySlug,
8 | } from 'utils';
9 | import { getCategories, getSubCategories } from 'utils/api/queries';
10 |
11 | const Head = async ({ params: { 'category-slug': categorySlug } }) => {
12 | let categories = await getCategories();
13 | categories = addSlugToCategories(categories);
14 | const matchingCategory = findCategoryBySlug(categories, categorySlug);
15 | if (!matchingCategory) {
16 | return <>>
17 | }
18 | const title = `${MAIN_TITLE}${SEPARATOR}${matchingCategory.category}`;
19 |
20 | return ;
21 | };
22 |
23 | export default Head;
24 |
--------------------------------------------------------------------------------
/src/app/(category)/[category-slug]/page.jsx:
--------------------------------------------------------------------------------
1 | import slugify from 'slugify';
2 |
3 | import Link from 'components/shared/link';
4 | import { getCategories, getSubCategories } from 'utils/api/queries';
5 |
6 | const CategoryPage = async ({ params: { 'category-slug': categorySlug } }) => {
7 | const categories = await getCategories();
8 | categories.forEach((category, index) => {
9 | categories[index].slug = slugify(category.category, { lower: true });
10 | });
11 |
12 | const matchingCategory = categories.find((category) => category.slug === categorySlug);
13 | if (!matchingCategory) {
14 | return <>>;
15 | }
16 | // eslint-disable-next-line no-underscore-dangle
17 |
18 | const subCategories = await getSubCategories(matchingCategory?._id);
19 |
20 | subCategories.forEach((subCategory, index) => {
21 | subCategories[index].slug = slugify(subCategory.subCategory, { lower: true });
22 | });
23 |
24 | return (
25 |
26 |
27 |
28 | List of all categories
29 |
30 |
31 |
32 |
{matchingCategory.category}
33 |
34 |
35 |
36 | {subCategories.length > 0 &&
37 | subCategories.map((subCategory, index) => (
38 |
43 | {subCategory.subCategory}
44 |
45 | ))}
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default CategoryPage;
53 |
54 | export const revalidate = 60;
55 |
56 | export async function generateStaticParams() {
57 | const categories = (await getCategories()).filter((f) => f._id && f.category);
58 |
59 | categories.forEach((category, index) => {
60 | categories[index].slug = slugify(category.category, { lower: true });
61 | });
62 |
63 | return categories.map((category) => ({
64 | 'category-slug': category.slug,
65 | }));
66 | }
67 |
--------------------------------------------------------------------------------
/src/app/head.jsx:
--------------------------------------------------------------------------------
1 | import HeadMetaTags from 'components/shared/head-meta-tags';
2 |
3 | const title = 'Notification Generator - Generate notifications for your website';
4 | const description = 'Generate notifications for your website';
5 |
6 | const Head = async () => ;
7 |
8 | export default Head;
9 |
--------------------------------------------------------------------------------
/src/app/layout.jsx:
--------------------------------------------------------------------------------
1 | import 'styles/main.css';
2 | import Footer from 'components/shared/footer';
3 | import Header from 'components/shared/header';
4 |
5 | const RootLayout = ({ children }) => (
6 |
7 |
9 |
10 |
11 | {children}
12 |
13 |
14 |
15 |
16 | );
17 |
18 | export default RootLayout;
19 |
--------------------------------------------------------------------------------
/src/app/page.jsx:
--------------------------------------------------------------------------------
1 | import NextImage from 'next/image';
2 |
3 | import Button from 'components/shared/button';
4 | import CategoryCard from 'components/shared/category-card';
5 | import ButtonLogo from 'images/chatgpt.inline.svg';
6 | import { getCategoriesWithAllSubCategories } from 'utils';
7 |
8 | const Home = async () => {
9 | const categories = await getCategoriesWithAllSubCategories();
10 | return (
11 | <>
12 |
13 |
14 |
15 |
16 | Notification inspirations for your App made{' '}
17 |
18 | with ChatGPT
19 |
20 |
21 | Discover an extensive library of customizable notifications for your users. Choose
22 | from various options to keep your audience engaged and informed. Focus on programming
23 | rather than creativity.
24 |
25 |
26 |
32 |
33 | Show me
34 |
35 |
40 | Discover Novu
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | Discover the Power of Notifications
54 |
55 |
56 | Unlock the full potential of your notifications with our diverse category library.
57 | Discover the power of personalized notifications, focusing on code rather than copy.
58 |
59 |
60 |
61 | {categories.map((category, index) => (
62 |
67 | ))}
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | >
79 | );
80 | };
81 |
82 | export default Home;
83 |
84 | export const revalidate = 60;
85 |
--------------------------------------------------------------------------------
/src/components/pages/sub-category/mobile/arrow-left.inline.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/components/pages/sub-category/mobile/arrow-right.inline.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/components/pages/sub-category/mobile/close.inline.svg:
--------------------------------------------------------------------------------
1 |
2 |
9 |
16 |
17 |
--------------------------------------------------------------------------------
/src/components/pages/sub-category/mobile/index.js:
--------------------------------------------------------------------------------
1 | import Mobile from './mobile';
2 | export default Mobile;
--------------------------------------------------------------------------------
/src/components/pages/sub-category/mobile/mobile.jsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import clsx from 'clsx';
4 | import { motion, AnimatePresence } from 'framer-motion';
5 | import Link from 'next/link';
6 | import { usePathname } from 'next/navigation';
7 | import { useEffect, useState } from 'react';
8 |
9 | import ArrowLeft from './arrow-left.inline.svg';
10 | import ArrowRight from './arrow-right.inline.svg';
11 | import CardSvg from './card.inline.svg';
12 | import CloseSvg from './close.inline.svg';
13 |
14 | const Mobile = ({
15 | notificationId,
16 | notificationMsg,
17 | nextNotificationIndex,
18 | previousNotificationIndex,
19 | next,
20 | previous,
21 | }) => {
22 | const [currentDate, setCurrentDate] = useState(false);
23 | const [currentTime, setCurrentTime] = useState(false);
24 | // get current path of the page
25 | const pathname = usePathname();
26 |
27 | const truncatedMessage = (msg) => (msg.length > 57 ? `${msg.slice(0, 57)}...` : msg);
28 |
29 | // if pathName has this pattern /e-commerce/order-confirmation/2 then we need to remove the last part
30 | // and get the pathName as /e-commerce/order-confirmation
31 | const prevPath = pathname.split('/').slice(0, -1).join('/');
32 |
33 | useEffect(() => {
34 | // assign to a variable current date in this format January 10, Tuesday
35 | const currentDate = new Date().toLocaleString('en-us', {
36 | month: 'long',
37 | day: 'numeric',
38 | weekday: 'long',
39 | });
40 | // assign to a variable current date in this format 14:51 without AM/PM
41 |
42 | const currentTime = new Date().toLocaleString('en-us', {
43 | hour: 'numeric',
44 | minute: 'numeric',
45 | hour12: false,
46 | });
47 |
48 | setCurrentTime(currentTime);
49 | setCurrentDate(currentDate);
50 | }, []);
51 | return (
52 |
53 |
57 |
58 |
59 |
63 |
64 |
65 |
66 |
71 |
72 |
77 | {currentDate || 'Friday 1, January'}
78 |
79 |
85 | {currentTime || '00:00'}
86 |
87 |
88 |
89 |
90 |
Notification Center
91 |
92 |
93 |
94 |
95 |
102 |
103 |
104 |
105 |
106 | A
107 |
108 |
109 |
110 |
Acme.corp
111 |
1 second ago
112 |
113 |
114 | {truncatedMessage(notificationMsg)}
115 |
116 |
117 |
118 |
slide to view
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | );
129 | };
130 |
131 | export default Mobile;
132 |
--------------------------------------------------------------------------------
/src/components/pages/sub-category/template-info/index.js:
--------------------------------------------------------------------------------
1 | import TemplateInfo from './template-info';
2 | export default TemplateInfo;
--------------------------------------------------------------------------------
/src/components/pages/sub-category/template-info/template-info.jsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as DialogPrimitive from '@radix-ui/react-dialog';
4 | import { useFormik } from 'formik';
5 | import NextLink from 'next/link';
6 | import { usePathname } from 'next/navigation';
7 | import { useState } from 'react';
8 |
9 | import Mobile from 'components/pages/sub-category/mobile';
10 | import Button from 'components/shared/button';
11 | import {
12 | Dialog,
13 | DialogContent,
14 | DialogDescription,
15 | DialogHeader,
16 | DialogTitle,
17 | DialogTrigger,
18 | } from 'components/shared/dialogue';
19 | import Link from 'components/shared/link';
20 | import LINKS from 'constants/links';
21 |
22 | const colors = ['rgba(255, 179, 204, 1)', 'rgba(255, 230, 179, 1)'];
23 | const variables = [
24 | {
25 | name: '{{play_name}}',
26 | description: 'The name of the play',
27 | },
28 | {
29 | name: '{{theater_name}}',
30 | description: 'The name of the theater',
31 | },
32 | ];
33 | // function that will accept notificationTemp string and replace items in {{}} with span tags containing the variable name and that uses style color from a colors variables based on the index of the variable in the variables array
34 | const parseNotification = (notification) => {
35 | let enhancedNotification = notification;
36 | const regex = /{{(.*?)}}/g;
37 | const matches = typeof notification === 'string' ? notification?.match(regex) || [] : [];
38 |
39 | matches?.forEach((match, index) => {
40 | enhancedNotification = enhancedNotification.replace(
41 | match,
42 | `${match} `
43 | );
44 | });
45 | return enhancedNotification;
46 | };
47 |
48 | // // function that will accept notificationTemp string and find variables inside {{}} and return an array of objects with the variable name, without the {{}}
49 |
50 | const findVariables = (notification) => {
51 | const regex = /{{(.*?)}}/g;
52 | const matches = typeof notification === 'string' ? notification?.match(regex) || [] : [];
53 |
54 | return matches?.map((match) => match.replace(/{{|}}/g, ''));
55 | };
56 |
57 | const InputGroup = ({ variable, formik }) => (
58 |
59 | {variable}
60 |
67 |
68 | );
69 |
70 | const TemplateInfo = ({ matchingCategory, matchingSubCategory, notifications }) => {
71 | const [open, setOpen] = useState(false);
72 | const [customNotification, setCustomNotification] = useState(undefined);
73 | const [currentNotificationIndex, setCurrentNotificationIndex] = useState(0);
74 | const notification = notifications[currentNotificationIndex];
75 |
76 | // Calculate index of the next notification, if it's the last notification, go back to the first one
77 | const nextNotificationIndex =
78 | currentNotificationIndex + 1 > notifications.length - 1 ? 0 : currentNotificationIndex + 1;
79 | // Calculate index of the previous notification, if it's the first notification, go to the last one
80 |
81 | const previousNotificationIndex =
82 | currentNotificationIndex === 0 ? notifications.length - 1 : currentNotificationIndex - 1;
83 |
84 | const formik = useFormik({
85 | initialValues: {},
86 | onSubmit: (values) => {
87 | setOpen(false);
88 | // clone the notification object
89 | const customNotificationTemp = { ...notification };
90 | // generate random _id for the notification
91 | customNotificationTemp._id = Math.random().toString(36).substr(2, 9);
92 | customNotificationTemp.notification = notification?.notification?.replace(
93 | /{{(.*?)}}/g,
94 | (match) => values[match.replace(/{{|}}/g, '')]
95 | );
96 |
97 | setCustomNotification(customNotificationTemp);
98 | },
99 | });
100 |
101 | return (
102 |
103 |
104 |
105 |
106 | List of all categories
107 |
108 | /
109 |
110 | {matchingCategory.category}
111 |
112 |
113 |
114 |
{matchingSubCategory.subCategory}
115 |
{matchingSubCategory.description}
116 |
117 |
118 |
119 | Notification {currentNotificationIndex + 1} of {notifications.length}
120 |
121 |
135 |
136 |
140 |
141 | {/*
142 |
Variables
143 |
144 | {variables.map(({ name, description }, index) => (
145 |
149 |
150 | {name}
151 |
152 | {description}
153 |
154 | ))}
155 |
156 |
*/}
157 |
158 |
159 | {findVariables(notification?.notification || []) && (
160 |
161 |
162 |
163 | Send a test notification
164 |
165 |
166 |
167 |
168 | Fill variables to test notification
169 |
170 |
171 |
181 |
182 |
183 | )}
184 |
185 |
186 | Go to Novu
187 |
188 |
189 |
190 |
191 | setCurrentNotificationIndex(nextNotificationIndex)}
193 | previous={() => setCurrentNotificationIndex(previousNotificationIndex)}
194 | notificationId={customNotification?._id || notification?._id}
195 | notificationMsg={customNotification?.notification || notification?.notification}
196 | nextNotificationIndex={nextNotificationIndex}
197 | previousNotificationIndex={previousNotificationIndex}
198 | />
199 |
200 |
201 | );
202 | };
203 |
204 | export default TemplateInfo;
205 |
--------------------------------------------------------------------------------
/src/components/shared/button/button.jsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import PropTypes from 'prop-types';
3 | import React from 'react';
4 |
5 | import Link from 'components/shared/link';
6 |
7 | // Example of the code — https://user-images.githubusercontent.com/20713191/144215307-35538500-b9f0-486d-abed-1a14825bb75c.png
8 | const styles = {
9 | base: 'inline-flex items-center justify-center !leading-none text-center whitespace-nowrap rounded transition-[colors, opacity] duration-200 outline-none uppercase font-medium',
10 | size: {
11 | lg: 'h-12 px-6 text-sm sm:h-10 sm:px-5 sm:text-xs',
12 | sm: 'h-10 px-5 text-xs',
13 | },
14 | // TODO: Add themes. Better to name the theme using this pattern: "${color-name}-${theme-type}", e.g. "black-filled"
15 | // If there is no dividing between theme types, then feel free to use just color names, e.g. "black"
16 | // Check out an example by a link above for better understanding
17 | theme: {
18 | 'purple-filled': 'bg-purple-2 text-white hover:bg-purple-3',
19 | 'gray-filled': 'bg-gray-2 text-white hover:bg-gray-3',
20 | },
21 | };
22 |
23 | const Button = ({
24 | className: additionalClassName = null,
25 | to = null,
26 | size,
27 | theme,
28 | children,
29 | ...otherProps
30 | }) => {
31 | const className = clsx(styles.base, styles.size[size], styles.theme[theme], additionalClassName);
32 |
33 | const Tag = to ? Link : 'button';
34 |
35 | return (
36 |
37 | {children}
38 |
39 | );
40 | };
41 |
42 | Button.propTypes = {
43 | className: PropTypes.string,
44 | to: PropTypes.string,
45 | size: PropTypes.oneOf(Object.keys(styles.size)).isRequired,
46 | theme: PropTypes.oneOf(Object.keys(styles.theme)).isRequired,
47 | children: PropTypes.node.isRequired,
48 | };
49 |
50 | export default Button;
51 |
--------------------------------------------------------------------------------
/src/components/shared/button/index.js:
--------------------------------------------------------------------------------
1 | import Button from './button';
2 |
3 | export default Button;
4 |
--------------------------------------------------------------------------------
/src/components/shared/category-card/category-card.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import slugify from 'slugify';
4 |
5 | import Link from 'components/shared/link';
6 | import ArrowIcon from 'images/arrow.inline.svg';
7 | import AirplaneIcon from 'images/cards/airplane.inline.svg';
8 | // const icons = {
9 | // airplane: AirplaneIcon,
10 | // cart: CartIcon,
11 | // person: PersonIcon,
12 | // shop: ShopIcon,
13 | // };
14 |
15 | const CategoryCard = ({ title, subCategories, categoryId }, index) => (
16 | // const Icon = icons[iconName];
17 |
18 |
19 |
20 | {/*
*/}
25 |
{title}
26 |
27 |
28 |
29 | {subCategories &&
30 | subCategories.length > 0 &&
31 | subCategories.slice(0, 5).map((item, index) => (
32 |
33 |
39 | {item.subCategory}
40 |
41 |
42 | ))}
43 |
44 |
48 | show more
49 |
50 |
51 | );
52 | CategoryCard.propTypes = {
53 | title: PropTypes.string.isRequired,
54 | description: PropTypes.string.isRequired,
55 | };
56 |
57 | export default CategoryCard;
58 |
--------------------------------------------------------------------------------
/src/components/shared/category-card/index.js:
--------------------------------------------------------------------------------
1 | import CategoryCard from './category-card';
2 | export default CategoryCard;
--------------------------------------------------------------------------------
/src/components/shared/dialogue/close.inline.svg:
--------------------------------------------------------------------------------
1 |
2 |
9 |
16 |
17 |
--------------------------------------------------------------------------------
/src/components/shared/dialogue/dialogue.jsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as DialogPrimitive from '@radix-ui/react-dialog';
4 | import clsx from 'clsx';
5 | import * as React from 'react';
6 |
7 | import CloseSvg from './close.inline.svg';
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = ({ className, children, ...props }) => (
14 |
15 |
16 | {children}
17 |
18 |
19 | );
20 | DialogPortal.displayName = DialogPrimitive.Portal.displayName;
21 |
22 | const DialogOverlay = React.forwardRef(({ className, children, ...props }, ref) => (
23 |
31 | ));
32 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
33 |
34 | const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
35 |
36 |
37 |
46 | {children}
47 |
48 | Close
49 |
50 |
51 |
52 |
53 | ));
54 | DialogContent.displayName = DialogPrimitive.Content.displayName;
55 |
56 | const DialogHeader = ({ className, ...props }) => (
57 |
58 | );
59 | DialogHeader.displayName = 'DialogHeader';
60 |
61 | const DialogFooter = ({ className, ...props }) => (
62 |
63 | );
64 | DialogFooter.displayName = 'DialogFooter';
65 |
66 | const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
67 |
72 | ));
73 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
74 |
75 | const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
76 |
81 | ));
82 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
83 |
84 | export {
85 | Dialog,
86 | DialogTrigger,
87 | DialogContent,
88 | DialogHeader,
89 | DialogFooter,
90 | DialogTitle,
91 | DialogDescription,
92 | };
93 |
--------------------------------------------------------------------------------
/src/components/shared/dialogue/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | Dialog,
3 | DialogContent,
4 | DialogFooter,
5 | DialogDescription,
6 | DialogHeader,
7 | DialogTitle,
8 | DialogTrigger,
9 | } from './dialogue';
10 |
11 | export {
12 | Dialog,
13 | DialogTrigger,
14 | DialogContent,
15 | DialogHeader,
16 | DialogFooter,
17 | DialogTitle,
18 | DialogDescription,
19 | };
20 |
--------------------------------------------------------------------------------
/src/components/shared/footer/footer.jsx:
--------------------------------------------------------------------------------
1 | import MENUS from 'constants/menus';
2 | import LogoSVG from 'images/logo.inline.svg';
3 |
4 | import Link from '../link';
5 |
6 | const Footer = () => (
7 |
33 | );
34 |
35 | export default Footer;
36 |
--------------------------------------------------------------------------------
/src/components/shared/footer/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './footer';
2 |
--------------------------------------------------------------------------------
/src/components/shared/head-meta-tags/head-meta-tags.jsx:
--------------------------------------------------------------------------------
1 | // import prop types
2 | import Script from 'next/script';
3 | import PropTypes from 'prop-types';
4 |
5 | const defaultTitle = 'Notification Generator';
6 | const defaultDescription = 'Generate notifications for your website';
7 | const defaultImagePath = '/images/social-preview.jpg';
8 |
9 | const { SITE_URL } = process.env;
10 |
11 | const HeadMetaTags = ({ title, description, imagePath }) => (
12 | <>
13 | {title}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
34 | >
35 | );
36 |
37 | // Write prop types validation based on default parameters
38 |
39 | HeadMetaTags.propTypes = {
40 | title: PropTypes.string,
41 | description: PropTypes.string,
42 | imagePath: PropTypes.string,
43 | };
44 |
45 | // Write default props based on default parameters
46 |
47 | HeadMetaTags.defaultProps = {
48 | title: defaultTitle,
49 | description: defaultDescription,
50 | imagePath: defaultImagePath,
51 | };
52 |
53 | export default HeadMetaTags;
54 |
--------------------------------------------------------------------------------
/src/components/shared/head-meta-tags/index.js:
--------------------------------------------------------------------------------
1 | import HeadMetaTags from './head-meta-tags';
2 |
3 | export default HeadMetaTags;
4 |
--------------------------------------------------------------------------------
/src/components/shared/header/header.jsx:
--------------------------------------------------------------------------------
1 | import MENUS from 'constants/menus';
2 | import LogoSVG from 'images/logo.inline.svg';
3 |
4 | import Link from '../link';
5 |
6 | // TODO: Implement mobile menu functionality and delete eslint comment below, example — https://user-images.githubusercontent.com/20713191/144221747-70dc933e-a5bd-4586-9019-08117afc13e0.png
7 | // eslint-disable-next-line no-unused-vars
8 | const Header = () => (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | {MENUS.header.map(({ to, text, target }, index) => (
17 |
18 | {text}
19 |
20 | ))}
21 |
22 |
23 |
24 | );
25 |
26 | export default Header;
27 |
--------------------------------------------------------------------------------
/src/components/shared/header/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './header';
2 |
--------------------------------------------------------------------------------
/src/components/shared/link/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './link';
2 |
--------------------------------------------------------------------------------
/src/components/shared/link/link.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/anchor-is-valid */
2 | import clsx from 'clsx';
3 | import NextLink from 'next/link';
4 | import PropTypes from 'prop-types';
5 |
6 | // Example of the code — https://user-images.githubusercontent.com/20713191/144221096-1939c382-4ab8-4d28-b0e6-7bbe3a8f8556.png
7 | const styles = {
8 | base: 'inline-block leading-none',
9 | // TODO: Add sizes. Better to write down all sizes and go from higher to lower, e.g. "xl", "lg", "md", "sm", "xs"
10 | // The name of the size cannot be lower than the font size that being used, e.g. "sm" size cannot have font-size "xs"
11 | // Check out an example by a link above for better understanding
12 | size: {
13 | base: 'text-base',
14 | sm: 'text-sm',
15 | }, // TODO: Add themes. Better to name the theme using this pattern: "${color-name}-${theme-type}", e.g. "black-filled"
16 | // If there is no dividing between theme types, then feel free to use just color names, e.g. "black"
17 | // Check out an example by a link above for better understanding
18 | theme: {
19 | purple: 'text-purple-1 hover:text-white',
20 | white: 'text-white hover:text-purple-1',
21 | },
22 | };
23 |
24 | const Link = ({
25 | className: additionalClassName = null,
26 | size = null,
27 | theme = null,
28 | to = null,
29 | children,
30 | ...props
31 | }) => {
32 | const className = clsx(
33 | size && theme && styles.base,
34 | styles.size[size],
35 | styles.theme[theme],
36 | additionalClassName
37 | );
38 |
39 | if (to.startsWith('/')) {
40 | return (
41 |
42 | {children}
43 |
44 | );
45 | }
46 |
47 | return (
48 |
49 | {children}
50 |
51 | );
52 | };
53 |
54 | Link.propTypes = {
55 | className: PropTypes.string,
56 | to: PropTypes.string,
57 | size: PropTypes.oneOf(Object.keys(styles.size)),
58 | theme: PropTypes.oneOf(Object.keys(styles.theme)),
59 | children: PropTypes.node.isRequired,
60 | };
61 |
62 | export default Link;
63 |
--------------------------------------------------------------------------------
/src/components/shared/mobile-menu/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './mobile-menu';
2 |
--------------------------------------------------------------------------------
/src/components/shared/mobile-menu/mobile-menu.jsx:
--------------------------------------------------------------------------------
1 | import { motion, useAnimation } from 'framer-motion';
2 | import PropTypes from 'prop-types';
3 | import React, { useEffect } from 'react';
4 |
5 | import Link from 'components/shared/link';
6 |
7 | const ANIMATION_DURATION = 0.2;
8 |
9 | const variants = {
10 | from: {
11 | opacity: 0,
12 | translateY: 30,
13 | transition: {
14 | duration: ANIMATION_DURATION,
15 | },
16 | transitionEnd: {
17 | zIndex: -1,
18 | },
19 | },
20 | to: {
21 | zIndex: 999,
22 | opacity: 1,
23 | translateY: 0,
24 | transition: {
25 | duration: ANIMATION_DURATION,
26 | },
27 | },
28 | };
29 |
30 | // TODO: Add links
31 | const links = [
32 | {
33 | text: '',
34 | to: '',
35 | },
36 | ];
37 |
38 | const MobileMenu = ({ isOpen }) => {
39 | const controls = useAnimation();
40 |
41 | useEffect(() => {
42 | if (isOpen) {
43 | controls.start('to');
44 | } else {
45 | controls.start('from');
46 | }
47 | }, [isOpen, controls]);
48 |
49 | return (
50 |
61 |
62 | {links.map(({ text, to }, index) => (
63 |
64 | {/* TODO: Add needed props so the link would have styles */}
65 |
66 | {text}
67 |
68 |
69 | ))}
70 |
71 | {/* TODO: Add a button if needed */}
72 |
73 | );
74 | };
75 |
76 | MobileMenu.propTypes = {
77 | isOpen: PropTypes.bool,
78 | };
79 |
80 | MobileMenu.defaultProps = {
81 | isOpen: false,
82 | };
83 |
84 | export default MobileMenu;
85 |
--------------------------------------------------------------------------------
/src/constants/links.js:
--------------------------------------------------------------------------------
1 | export default {
2 | // Pages
3 | home: {
4 | to: '/',
5 | },
6 | novu: {
7 | to: 'https://novu.co/?utm_campaign=not-dir',
8 | },
9 | // Social
10 | discord: {
11 | to: 'https://discord.novu.co',
12 | target: '_blank',
13 | },
14 | twitter: {
15 | to: 'https://twitter.com/novuhq',
16 | target: '_blank',
17 | },
18 | github: {
19 | to: 'https://github.com/novuhq/novu',
20 | target: '_blank',
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/src/constants/menus.js:
--------------------------------------------------------------------------------
1 | import LINKS from 'constants/links.js';
2 |
3 | const MENUS = {
4 | header: [
5 | { text: 'Discord', ...LINKS.discord },
6 | { text: 'Twitter', ...LINKS.twitter },
7 | { text: 'GitHub', ...LINKS.github },
8 | ],
9 | footer: [
10 | { text: 'Discord', ...LINKS.discord },
11 | { text: 'Twitter', ...LINKS.twitter },
12 | { text: 'GitHub', ...LINKS.github },
13 | ],
14 | mobile: [
15 | {
16 | text: 'Documentation',
17 | ...LINKS.documentation,
18 | },
19 | { text: 'Blog', ...LINKS.blog },
20 | { text: 'Contributors', ...LINKS.contributors },
21 | { text: 'Careers', ...LINKS.careers },
22 | ],
23 | };
24 |
25 | export default MENUS;
26 |
--------------------------------------------------------------------------------
/src/constants/seo.js:
--------------------------------------------------------------------------------
1 | const MAIN_TITLE = 'Notification Generator';
2 | const SEPARATOR = ' - ';
3 |
4 | export { MAIN_TITLE, SEPARATOR };
5 |
--------------------------------------------------------------------------------
/src/hooks/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novuhq/notification-directory-react/ae92a575c63aaa158cf11dc83e9fad77b54779f4/src/hooks/.gitkeep
--------------------------------------------------------------------------------
/src/images/arrow.inline.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/images/cards/airplane.inline.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
17 |
24 |
31 |
35 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/images/chatgpt.inline.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/src/images/logo.inline.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/layouts/layout-main/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './layout-main';
2 |
--------------------------------------------------------------------------------
/src/layouts/layout-main/layout-main.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { useState } from 'react';
3 |
4 | import Footer from 'components/shared/footer';
5 | import Header from 'components/shared/header';
6 | import MobileMenu from 'components/shared/mobile-menu';
7 | import SEO from 'components/shared/seo';
8 | // import LINKS from 'constants/links';
9 | import MENUS from 'constants/menus';
10 |
11 | const LayoutMain = ({ children }) => {
12 | const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
13 |
14 | const handleHeaderBurgerClick = () => setIsMobileMenuOpen(!isMobileMenuOpen);
15 | return (
16 | <>
17 |
18 |
23 | {children}
24 |
25 |
26 | >
27 | );
28 | };
29 |
30 | LayoutMain.propTypes = {
31 | children: PropTypes.node.isRequired,
32 | };
33 |
34 | export default LayoutMain;
35 |
--------------------------------------------------------------------------------
/src/styles/container.css:
--------------------------------------------------------------------------------
1 | @layer components {
2 | .container {
3 | @apply mx-auto max-w-[1216px] xl:px-10 lg:max-w-full md:px-7 sm:px-4;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/styles/fonts.css:
--------------------------------------------------------------------------------
1 | @layer base {
2 | @font-face {
3 | font-family: 'Brother-1816';
4 | src: url('/fonts/brother-1816/brother-1816-light.woff2') format('woff2');
5 | font-weight: 300;
6 | font-style: normal;
7 | font-display: fallback;
8 | }
9 |
10 | @font-face {
11 | font-family: 'Brother-1816';
12 | src: url('/fonts/brother-1816/brother-1816-book.woff2') format('woff2');
13 | font-weight: 350;
14 | font-style: normal;
15 | font-display: fallback;
16 | }
17 |
18 | @font-face {
19 | font-family: 'Brother-1816';
20 | src: url('/fonts/brother-1816/brother-1816-regular.woff2') format('woff2');
21 | font-weight: normal;
22 | font-style: normal;
23 | font-display: fallback;
24 | }
25 |
26 | @font-face {
27 | font-family: 'Brother-1816';
28 | src: url('/fonts/brother-1816/brother-1816-medium.woff2') format('woff2');
29 | font-weight: 500;
30 | font-style: normal;
31 | font-display: fallback;
32 | }
33 |
34 | @font-face {
35 | font-family: 'IBM Plex Mono';
36 | src: url('/fonts/ibm-plex-mono/ibm-plex-mono-regular.woff2') format('woff2');
37 | font-weight: 400;
38 | font-style: normal;
39 | font-display: fallback;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/styles/global.css:
--------------------------------------------------------------------------------
1 | /* TODO: Add global styles: font-size, color, and other if needed */
2 | @layer base {
3 | body {
4 | @apply min-w-[320px] font-sans antialiased;
5 | -webkit-tap-highlight-color: transparent;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/styles/grid-gap.css:
--------------------------------------------------------------------------------
1 | @layer base {
2 | .grid-gap-x {
3 | @apply gap-x-8 xl:gap-x-6 lg:gap-x-4;
4 | }
5 |
6 | .grid-gap-y {
7 | @apply gap-y-8 xl:gap-y-6 lg:gap-y-4;
8 | }
9 |
10 | .grid-gap {
11 | @apply grid-gap-x grid-gap-y;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/styles/main.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss/base';
2 | @import 'tailwindcss/components';
3 | @import 'tailwindcss/utilities';
4 |
5 | /* Base */
6 | @import './fonts.css';
7 | @import './global.css';
8 | @import './grid-gap.css';
9 |
10 | /* Components */
11 | @import './container.css';
12 |
13 | /* Utilities */
14 | @import './remove-autocomplete-styles.css';
15 | @import './safe-paddings.css';
16 | @import './scrollbar-hidden.css';
17 |
--------------------------------------------------------------------------------
/src/styles/remove-autocomplete-styles.css:
--------------------------------------------------------------------------------
1 | @layer utilities {
2 | .remove-autocomplete-styles {
3 | &:-webkit-autofill,
4 | &:-webkit-autofill:hover,
5 | &:-webkit-autofill:focus {
6 | transition: background-color 5000s;
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/styles/safe-paddings.css:
--------------------------------------------------------------------------------
1 | @layer base {
2 | .safe-paddings {
3 | @apply px-safe;
4 |
5 | header& {
6 | @apply pt-safe;
7 | }
8 |
9 | footer& {
10 | @apply pb-safe;
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/styles/scrollbar-hidden.css:
--------------------------------------------------------------------------------
1 | @layer utilities {
2 | .scrollbar-hidden {
3 | -ms-overflow-style: none;
4 | scrollbar-width: none;
5 |
6 | &::-webkit-scrollbar {
7 | display: none;
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/novuhq/notification-directory-react/ae92a575c63aaa158cf11dc83e9fad77b54779f4/src/utils/.gitkeep
--------------------------------------------------------------------------------
/src/utils/api/queries.js:
--------------------------------------------------------------------------------
1 | let categories;
2 | const getCategories = async () => {
3 | try {
4 | if (categories) {
5 | return categories;
6 | }
7 | const categoriesResponse = await fetch(`https://api.notifications.directory/categories/`);
8 | categories = await categoriesResponse.json();
9 | if (!Array.isArray(categories)) {
10 | categories = undefined;
11 | return [];
12 | }
13 | return categories;
14 | } catch (e) {
15 | return [];
16 | }
17 | };
18 |
19 | const getSubCategories = async (categoryId) => {
20 | try {
21 | const subCategoriesResponse = await fetch(
22 | `https://api.notifications.directory/categories/${categoryId}/sub`
23 | );
24 | const subCategories = await subCategoriesResponse.json();
25 | if (!Array.isArray(subCategories)) {
26 | return [];
27 | }
28 | return subCategories;
29 | } catch (err) {
30 | return [];
31 | }
32 | };
33 |
34 | const getNotifications = async (subCategoryId) => {
35 | try {
36 | const notificationsResponse = await fetch(
37 | `https://api.notifications.directory/sub/${subCategoryId}/notifications`
38 | );
39 | const notifications = await notificationsResponse.json();
40 | if (!Array.isArray(notifications)) {
41 | return [];
42 | }
43 | return notifications;
44 | } catch (err) {
45 | return [];
46 | }
47 | };
48 |
49 | export { getCategories, getSubCategories, getNotifications };
50 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import slugify from 'slugify';
2 |
3 | import { getCategories, getSubCategories } from './api/queries';
4 |
5 | const addSlugToCategories = (categories) => {
6 | const newCategories = categories;
7 | newCategories.forEach((category, index) => {
8 | newCategories[index].slug = slugify(category.category, { lower: true });
9 | });
10 | return newCategories;
11 | };
12 |
13 | const findCategoryBySlug = (categories, slug) =>
14 | categories.find((category) => category.slug === slug);
15 |
16 | const addSlugToSubCategories = (subCategories) => {
17 | const newSubCategories = subCategories;
18 | newSubCategories.forEach((subCategory, index) => {
19 | newSubCategories[index].slug = slugify(subCategory.subCategory, { lower: true });
20 | });
21 | return newSubCategories;
22 | };
23 |
24 | const findSubCategoryBySlug = (subCategories, slug) =>
25 | subCategories.find((subCategory) => subCategory.slug === slug);
26 |
27 | const getCategoriesWithAllSubCategories = async () => {
28 | const categories = await getCategories();
29 | // For each category, make a request to get the subcategories, queries can be async so we need to use Promise.all
30 | const promises = categories.map((category) => getSubCategories(category._id));
31 | // Once all the promises are resolved, we need to merge categories and subcategories into one array based matching _id from a category to category field in a subcategory
32 | const subCategories = await Promise.all(promises);
33 | const categoriesWithSubCategories = categories.map((category) => {
34 | // const subCategoriesForCategory = subCategories.find((subCategory) =>
35 | // subCategory ? subCategory[0].category === category._id : false
36 | // );
37 | const subCategoriesForCategory = subCategories.find((subCategory) => {
38 | if (subCategory.length > 0) {
39 | return subCategory[0].category === category._id;
40 | }
41 | return false;
42 | });
43 | return {
44 | ...category,
45 | subCategories: subCategoriesForCategory,
46 | };
47 | });
48 | return categoriesWithSubCategories;
49 | };
50 |
51 | export {
52 | addSlugToCategories,
53 | findCategoryBySlug,
54 | addSlugToSubCategories,
55 | findSubCategoryBySlug,
56 | getCategoriesWithAllSubCategories,
57 | };
58 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | const defaultTheme = require('tailwindcss/defaultTheme');
3 |
4 | module.exports = {
5 | content: ['./src/**/*.{js,jsx,ts,tsx}'],
6 | corePlugins: {
7 | container: false,
8 | },
9 | theme: {
10 | // TODO: Uncomment this part of the code and the import of "defaultTheme" above, and complete TODOs
11 | fontFamily: {
12 | sans: ['Brother-1816', ...defaultTheme.fontFamily.sans],
13 | mono: ['IBM Plex Mono', ...defaultTheme.fontFamily.mono],
14 | },
15 | fontSize: {
16 | xs: ['12px'],
17 | sm: ['14px', { lineHeight: 1.5 }],
18 | base: ['16px'],
19 | lg: ['18px'],
20 | xl: ['20px'],
21 | '2xl': ['24px'],
22 | '3xl': ['30px'],
23 | '4xl': ['36px'],
24 | '5xl': ['48px'],
25 | '6xl': ['60px'],
26 | '7xl': ['72px'],
27 | '8xl': ['96px'],
28 | },
29 | colors: ({ colors }) => ({
30 | inherit: colors.inherit,
31 | current: colors.current,
32 | transparent: colors.transparent,
33 | black: '#000000',
34 | white: '#ffffff',
35 | // TODO: Add colors
36 | // Make sure that they are prepared in the Figma and follow the naming primary/secondary/gray-${number}
37 | // Example of correctly prepared colors in Figma — https://user-images.githubusercontent.com/20713191/143586876-5e834233-9639-4166-9811-b00e63820d98.png
38 | // Example of incorrectly prepared colors in Figma — https://user-images.githubusercontent.com/20713191/143586974-6986149f-aee3-450c-a1dd-26e73e3aca02.png
39 | // black: '',
40 | // white: '',
41 | // primary: {
42 | // 1: '',
43 | // },
44 | // secondary: {
45 | // 1: '',
46 | // },
47 | gray: {
48 | 1: '#0D0D0D',
49 | 2: '#1A1A1A',
50 | 3: '#262626',
51 | 4: '#333333',
52 | 5: '#4D4D4D',
53 | 6: '#666666',
54 | 7: '#808080',
55 | 8: '#999999',
56 | 9: '#CCCCCC',
57 | 10: '#E6E6E6',
58 | },
59 | purple: {
60 | 1: '#B3B3FF',
61 | 2: '#8080FF',
62 | // make the previous color darker by 20%
63 | 3: '#7C3BED',
64 | DEFAULT: '#B3B3FF',
65 | },
66 | }),
67 |
68 | screens: {
69 | '2xl': { max: '1919px' },
70 | xl: { max: '1535px' },
71 | lg: { max: '1279px' },
72 | md: { max: '1023px' },
73 | sm: { max: '767px' },
74 | xs: { max: '359px' },
75 | },
76 | extend: {
77 | lineHeight: {
78 | denser: '1.125',
79 | },
80 | backgroundImage: {
81 | 'mobile-gradient':
82 | 'radial-gradient(100% 100% at 100% 0%, rgba(255, 216, 77, 0.9) 0%, rgba(255, 216, 77, 0) 88.98%), linear-gradient(111.83deg, #8080FF -0.43%, #B3B3FF 53.44%, #7C3BED 100%)',
83 | 'light-gradient':
84 | 'linear-gradient(119.61deg, rgba(151, 77, 255, 0.5) 18.23%, #4C00E3 86.99%)',
85 | noise: "url('/images/noise.png')",
86 | 'text-gradient': 'linear-gradient(180deg, #FFFFFF 24.06%, rgba(255, 255, 255, 0.6) 100%)',
87 | 'warm-gradient': 'linear-gradient(192.71deg, #FFD84D 0%, #6400E3 93.66%)',
88 | },
89 | },
90 | },
91 | plugins: [require('tailwindcss-safe-area')],
92 | };
93 |
--------------------------------------------------------------------------------