├── .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.directory2023-02-16T22:12:18.455Zdaily0.7 4 | https://notifications.directory/social-media2023-02-16T22:12:18.455Zdaily0.7 5 | https://notifications.directory/news-and-journalism2023-02-16T22:12:18.455Zdaily0.7 6 | https://notifications.directory/e-commerce2023-02-16T22:12:18.455Zdaily0.7 7 | https://notifications.directory/online-marketplace2023-02-16T22:12:18.455Zdaily0.7 8 | https://notifications.directory/travel-and-booking2023-02-16T22:12:18.455Zdaily0.7 9 | https://notifications.directory/online-streaming2023-02-16T22:12:18.455Zdaily0.7 10 | https://notifications.directory/online-gaming2023-02-16T22:12:18.455Zdaily0.7 11 | https://notifications.directory/online-education2023-02-16T22:12:18.455Zdaily0.7 12 | https://notifications.directory/job-listing2023-02-16T22:12:18.455Zdaily0.7 13 | https://notifications.directory/real-estate2023-02-16T22:12:18.455Zdaily0.7 14 | https://notifications.directory/food-delivery2023-02-16T22:12:18.455Zdaily0.7 15 | https://notifications.directory/health-and-fitness2023-02-16T22:12:18.455Zdaily0.7 16 | https://notifications.directory/beauty-and-personal-care2023-02-16T22:12:18.455Zdaily0.7 17 | https://notifications.directory/home-and-garden2023-02-16T22:12:18.455Zdaily0.7 18 | https://notifications.directory/fashion-and-clothing2023-02-16T22:12:18.455Zdaily0.7 19 | https://notifications.directory/electronics-and-technology2023-02-16T22:12:18.455Zdaily0.7 20 | https://notifications.directory/cars-and-automotive2023-02-16T22:12:18.455Zdaily0.7 21 | https://notifications.directory/sports-and-outdoors2023-02-16T22:12:18.455Zdaily0.7 22 | https://notifications.directory/music-and-entertainment2023-02-16T22:12:18.455Zdaily0.7 23 | https://notifications.directory/art-and-design2023-02-16T22:12:18.455Zdaily0.7 24 | https://notifications.directory/photography2023-02-16T22:12:18.455Zdaily0.7 25 | https://notifications.directory/cooking-and-recipe2023-02-16T22:12:18.455Zdaily0.7 26 | https://notifications.directory/diy-and-crafting2023-02-16T22:12:18.455Zdaily0.7 27 | https://notifications.directory/gardening-and-agriculture2023-02-16T22:12:18.455Zdaily0.7 28 | https://notifications.directory/pet-care2023-02-16T22:12:18.455Zdaily0.7 29 | https://notifications.directory/parenting-and-family2023-02-16T22:12:18.455Zdaily0.7 30 | https://notifications.directory/personal-finance-and-banking2023-02-16T22:12:18.455Zdaily0.7 31 | https://notifications.directory/legal-services2023-02-16T22:12:18.455Zdaily0.7 32 | https://notifications.directory/insurance2023-02-16T22:12:18.455Zdaily0.7 33 | https://notifications.directory/health-insurance2023-02-16T22:12:18.455Zdaily0.7 34 | https://notifications.directory/life-insurance2023-02-16T22:12:18.455Zdaily0.7 35 | https://notifications.directory/pet-insurance2023-02-16T22:12:18.455Zdaily0.7 36 | https://notifications.directory/travel-insurance2023-02-16T22:12:18.455Zdaily0.7 37 | https://notifications.directory/home-insurance2023-02-16T22:12:18.455Zdaily0.7 38 | https://notifications.directory/auto-insurance2023-02-16T22:12:18.455Zdaily0.7 39 | https://notifications.directory/investment-and-trading2023-02-16T22:12:18.455Zdaily0.7 40 | https://notifications.directory/stock-market2023-02-16T22:12:18.455Zdaily0.7 41 | https://notifications.directory/cryptocurrency2023-02-16T22:12:18.455Zdaily0.7 42 | https://notifications.directory/forex2023-02-16T22:12:18.455Zdaily0.7 43 | https://notifications.directory/mutual-funds2023-02-16T22:12:18.455Zdaily0.7 44 | https://notifications.directory/retirement-planning2023-02-16T22:12:18.455Zdaily0.7 45 | https://notifications.directory/credit-card2023-02-16T22:12:18.455Zdaily0.7 46 | https://notifications.directory/personal-loans2023-02-16T22:12:18.455Zdaily0.7 47 | https://notifications.directory/mortgage-and-refinancing2023-02-16T22:12:18.455Zdaily0.7 48 | https://notifications.directory/student-loans2023-02-16T22:12:18.455Zdaily0.7 49 | https://notifications.directory/banking-and-checking2023-02-16T22:12:18.455Zdaily0.7 50 | https://notifications.directory/credit-score-and-report2023-02-16T22:12:18.455Zdaily0.7 51 | https://notifications.directory/debt-consolidation2023-02-16T22:12:18.455Zdaily0.7 52 | https://notifications.directory/tax-preparation2023-02-16T22:12:18.455Zdaily0.7 53 | https://notifications.directory/legal-documents-and-contracts2023-02-16T22:12:18.455Zdaily0.7 54 | https://notifications.directory/business-services2023-02-16T22:12:18.455Zdaily0.7 55 | https://notifications.directory/marketing-and-advertising2023-02-16T22:12:18.455Zdaily0.7 56 | https://notifications.directory/public-relations2023-02-16T22:12:18.455Zdaily0.7 57 | https://notifications.directory/human-resources2023-02-16T22:12:18.455Zdaily0.7 58 | https://notifications.directory/project-management2023-02-16T22:12:18.455Zdaily0.7 59 | https://notifications.directory/customer-relationship-management2023-02-16T22:12:18.455Zdaily0.7 60 | https://notifications.directory/data-analysis-and-reporting2023-02-16T22:12:18.455Zdaily0.7 61 | https://notifications.directory/cloud-storage-and-computing2023-02-16T22:12:18.455Zdaily0.7 62 | https://notifications.directory/virtual-events-and-webinars2023-02-16T22:12:18.455Zdaily0.7 63 | https://notifications.directory/online-meetings-and-video-conferencing2023-02-16T22:12:18.455Zdaily0.7 64 | https://notifications.directory/productivity-and-time-management2023-02-16T22:12:18.455Zdaily0.7 65 | https://notifications.directory/collaboration-and-teamwork2023-02-16T22:12:18.455Zdaily0.7 66 | https://notifications.directory/presentation-and-slide-deck2023-02-16T22:12:18.455Zdaily0.7 67 | https://notifications.directory/document-editing-and-formatting2023-02-16T22:12:18.455Zdaily0.7 68 | https://notifications.directory/online-booking-and-scheduling2023-02-16T22:12:18.455Zdaily0.7 69 | https://notifications.directory/customer-service-and-support2023-02-16T22:12:18.455Zdaily0.7 70 | https://notifications.directory/crm-and-sales2023-02-16T22:12:18.455Zdaily0.7 71 | https://notifications.directory/hr-and-payroll2023-02-16T22:12:18.455Zdaily0.7 72 | https://notifications.directory/erp-and-supply-chain-management2023-02-16T22:12:18.455Zdaily0.7 73 | https://notifications.directory/project-and-task-management2023-02-16T22:12:18.455Zdaily0.7 74 | https://notifications.directory/account-management-and-billing2023-02-16T22:12:18.455Zdaily0.7 75 | https://notifications.directory/time-tracking-and-invoicing2023-02-16T22:12:18.455Zdaily0.7 76 | https://notifications.directory/e-signature-and-document-management2023-02-16T22:12:18.455Zdaily0.7 77 | https://notifications.directory/learning-management-system2023-02-16T22:12:18.455Zdaily0.7 78 | https://notifications.directory/online-courses-and-training2023-02-16T22:12:18.455Zdaily0.7 79 | https://notifications.directory/language-learning2023-02-16T22:12:18.455Zdaily0.7 80 | https://notifications.directory/tutoring-and-coaching2023-02-16T22:12:18.455Zdaily0.7 81 | https://notifications.directory/professional-development2023-02-16T22:12:18.455Zdaily0.7 82 | https://notifications.directory/certification-and-accreditation2023-02-16T22:12:18.455Zdaily0.7 83 | https://notifications.directory/testing-and-assessment2023-02-16T22:12:18.455Zdaily0.7 84 | https://notifications.directory/student-information-system2023-02-16T22:12:18.455Zdaily0.7 85 | https://notifications.directory/library-management2023-02-16T22:12:18.455Zdaily0.7 86 | https://notifications.directory/research-and-development2023-02-16T22:12:18.455Zdaily0.7 87 | https://notifications.directory/scientific-and-technical2023-02-16T22:12:18.455Zdaily0.7 88 | https://notifications.directory/medical-and-healthcare2023-02-16T22:12:18.455Zdaily0.7 89 | https://notifications.directory/mental-health-and-therapy2023-02-16T22:12:18.455Zdaily0.7 90 | https://notifications.directory/dentistry-and-orthodontics2023-02-16T22:12:18.455Zdaily0.7 91 | https://notifications.directory/optometry-and-eye-care2023-02-16T22:12:18.455Zdaily0.7 92 | https://notifications.directory/pediatrics-and-child-care2023-02-16T22:12:18.455Zdaily0.7 93 | https://notifications.directory/geriatrics-and-senior-care2023-02-16T22:12:18.455Zdaily0.7 94 | https://notifications.directory/physical-therapy-and-rehabilitation2023-02-16T22:12:18.455Zdaily0.7 95 | https://notifications.directory/nutrition-and-diet2023-02-16T22:12:18.455Zdaily0.7 96 | https://notifications.directory/alternative-and-complementary-medicine2023-02-16T22:12:18.455Zdaily0.7 97 | https://notifications.directory/veterinary-and-pet-care2023-02-16T22:12:18.455Zdaily0.7 98 | https://notifications.directory/agriculture-and-forestry2023-02-16T22:12:18.455Zdaily0.7 99 | https://notifications.directory/environmental-science2023-02-16T22:12:18.455Zdaily0.7 100 | https://notifications.directory/geology-and-earth-science2023-02-16T22:12:18.455Zdaily0.7 101 | https://notifications.directory/astronomy-and-space2023-02-16T22:12:18.455Zdaily0.7 102 | https://notifications.directory/meteorology-and-weather2023-02-16T22:12:18.455Zdaily0.7 103 | https://notifications.directory/oceanography-and-marine-biology2023-02-16T22:12:18.455Zdaily0.7 104 | https://notifications.directory/natural-history-and-conservation2023-02-16T22:12:18.455Zdaily0.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 | 8 | 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 | 35 | 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 | 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 | 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 | 165 | 166 | 167 | 168 | Fill variables to test notification 169 | 170 | 171 |
172 | {findVariables(notification?.notification || []).map((variable, index) => ( 173 | 174 | ))} 175 |
176 | 179 |
180 | 181 |
182 |
183 | )} 184 | 185 | 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 | {/*
    21 |
    22 | 23 |
    24 |
    */} 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 |
    8 |
    9 |
    10 | 11 | 12 | 13 |

    14 | Sponsored by{' '} 15 | 16 | Novu 17 | 18 |
    19 | Built with ChatGPT 20 |

    21 |
    22 |
    23 | 30 |
    31 |
    32 |
    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 | 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 | 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 |