├── src
├── schemas
│ ├── index.ts
│ └── shorten
│ │ ├── index.ts
│ │ ├── model.ts
│ │ └── input.ts
├── server
│ ├── repositories
│ │ ├── index.ts
│ │ └── shorten.repository.ts
│ ├── database
│ │ ├── index.ts
│ │ ├── types
│ │ │ └── database-client.ts
│ │ └── notion.ts
│ ├── middlewares
│ │ ├── index.ts
│ │ ├── validate.ts
│ │ └── wrap-error.ts
│ ├── models
│ │ ├── index.ts
│ │ └── shorten.model.ts
│ ├── configs
│ │ └── shorten.ts
│ └── errors.ts
├── reducers
│ ├── index.ts
│ ├── verify-token-reducer.ts
│ └── register-shorten-reducer.ts
├── utils
│ ├── index.ts
│ ├── random-integer.ts
│ ├── copy-text-to-clipboard.ts
│ └── base64.ts
├── components
│ ├── Title.tsx
│ ├── ShowItem.tsx
│ ├── Footer.tsx
│ ├── TokenAuthModal.tsx
│ └── RegisterUrlForm.tsx
├── pages
│ ├── _app.tsx
│ ├── api
│ │ ├── auth.ts
│ │ └── shortens
│ │ │ ├── [id].ts
│ │ │ └── index.ts
│ ├── [shortenUrlPath].tsx
│ └── index.tsx
└── constants
│ └── index.ts
├── tests
├── constants
│ └── index.ts
├── tsconfig.json
├── setup-env.ts
└── api
│ ├── auth.test.ts
│ └── shortens
│ ├── [id].test.ts
│ └── index.test.ts
├── cypress
├── support
│ ├── e2e.ts
│ └── commands.ts
├── fixtures
│ └── url.json
├── tsconfig.json
└── e2e
│ ├── auth.cy.ts
│ └── shorten.cy.ts
├── public
└── favicon.ico
├── next.config.cjs
├── next-env.d.ts
├── .editorconfig
├── vite.config.ts
├── .env.local.example
├── cypress.config.ts
├── tsconfig.json
├── .gitignore
├── LICENSE
├── package.json
├── .github
└── workflows
│ └── ci.yml
├── README.md
└── xo.config.cjs
/src/schemas/index.ts:
--------------------------------------------------------------------------------
1 | export * from './shorten';
2 |
--------------------------------------------------------------------------------
/tests/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const TIMEOUT = 1000 * 60 * 2;
2 |
--------------------------------------------------------------------------------
/src/server/repositories/index.ts:
--------------------------------------------------------------------------------
1 | export * from './shorten.repository';
2 |
--------------------------------------------------------------------------------
/src/schemas/shorten/index.ts:
--------------------------------------------------------------------------------
1 | export * from './input';
2 | export * from './model';
3 |
--------------------------------------------------------------------------------
/src/server/database/index.ts:
--------------------------------------------------------------------------------
1 | export {default as NotionDBClient} from './notion';
2 |
--------------------------------------------------------------------------------
/src/server/middlewares/index.ts:
--------------------------------------------------------------------------------
1 | export * from './validate';
2 | export * from './wrap-error';
3 |
--------------------------------------------------------------------------------
/cypress/support/e2e.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-unassigned-import */
2 | import './commands';
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/younho9/notion-url-shortener/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/cypress/fixtures/url.json:
--------------------------------------------------------------------------------
1 | {
2 | "repository": "https://github.com/younho9/notion-url-shortener"
3 | }
4 |
--------------------------------------------------------------------------------
/src/reducers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './register-shorten-reducer';
2 | export * from './verify-token-reducer';
3 |
--------------------------------------------------------------------------------
/next.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | module.exports = {
3 | reactStrictMode: true,
4 | };
5 |
--------------------------------------------------------------------------------
/src/server/models/index.ts:
--------------------------------------------------------------------------------
1 | export {default as ShortenModel} from './shorten.model';
2 | export * from './shorten.model';
3 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './base64';
2 | export * from './copy-text-to-clipboard';
3 | export * from './random-integer';
4 |
--------------------------------------------------------------------------------
/src/utils/random-integer.ts:
--------------------------------------------------------------------------------
1 | export const randomInteger = (minimum = 0, maximum = minimum) =>
2 | Math.floor(Math.random() * (maximum - minimum + 1) + minimum);
3 |
--------------------------------------------------------------------------------
/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "noEmit": true,
5 | "types": ["node"]
6 | },
7 | "include": ["**/*.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.yml]
11 | indent_style = space
12 | indent_size = 2
13 |
--------------------------------------------------------------------------------
/src/utils/copy-text-to-clipboard.ts:
--------------------------------------------------------------------------------
1 | export const copyTextToClipboard = async (text: string) => {
2 | try {
3 | await navigator.clipboard.writeText(text);
4 |
5 | return true;
6 | } catch {
7 | return document.execCommand('copy', true, text);
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/src/schemas/shorten/model.ts:
--------------------------------------------------------------------------------
1 | import type {ShortenType} from './input';
2 |
3 | export interface Shorten {
4 | id: number;
5 | shortenUrlPath: string;
6 | originalUrl: string;
7 | type: ShortenType;
8 | visits: number;
9 | createdAt: string;
10 | updatedAt: string;
11 | }
12 |
--------------------------------------------------------------------------------
/cypress/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "noEmit": true,
5 | // be explicit about types included
6 | // to avoid clashing with Jest types
7 | "isolatedModules": false,
8 | "types": ["cypress", "node"]
9 | },
10 | "include": ["**/*.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/tests/setup-env.ts:
--------------------------------------------------------------------------------
1 | import process from 'process';
2 |
3 | import {loadEnvConfig} from '@next/env'; // eslint-disable-line import/no-extraneous-dependencies
4 |
5 | const setupEnv = async () => {
6 | process.env.TZ = 'UTC';
7 |
8 | loadEnvConfig(process.env.PWD!);
9 | };
10 |
11 | export default setupEnv;
12 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import {fileURLToPath} from 'url';
3 |
4 | import {defineConfig} from 'vitest/config';
5 |
6 | export default defineConfig({
7 | alias: {
8 | '@': path.resolve(path.dirname(fileURLToPath(import.meta.url)), './src'),
9 | },
10 | test: {
11 | globalSetup: ['./tests/setup-env'],
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/src/components/Title.tsx:
--------------------------------------------------------------------------------
1 | import {Center, Heading, Icon} from '@chakra-ui/react';
2 | import {SiNotion} from 'react-icons/si';
3 |
4 | const Title = () => (
5 |
6 |
7 |
8 | URL Shortener
9 |
10 |
11 | );
12 |
13 | export default Title;
14 |
--------------------------------------------------------------------------------
/.env.local.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_NOTION_DATABASE_URL="https://.notion.site/" # See https://developers.notion.com/docs#step-2-share-a-database-with-your-integration
2 | NOTION_API_TOKEN="" # See https://developers.notion.com/docs/getting-started#getting-started
3 | USE_TOKEN_AUTH=false
4 | MAXIMUM_ZERO_WIDTH_SHORTEN_LENGTH=8
5 | MAXIMUM_BASE64_SHORTEN_LENGTH=7
6 | MAXIMUM_GENERATION_ATTEMPTS=5
7 |
--------------------------------------------------------------------------------
/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import process from 'process';
2 |
3 | import {loadEnvConfig} from '@next/env'; // eslint-disable-line import/no-extraneous-dependencies
4 | import {defineConfig} from 'cypress';
5 |
6 | export default defineConfig({
7 | e2e: {
8 | defaultCommandTimeout: 8000,
9 | baseUrl: 'http://localhost:3000',
10 | env: loadEnvConfig(process.cwd()).combinedEnv,
11 | video: false,
12 | screenshotOnRunFailure: false,
13 | experimentalSessionAndOrigin: true,
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/src/server/configs/shorten.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BASE64_CHARSET,
3 | MAXIMUM_BASE64_SHORTEN_LENGTH,
4 | MAXIMUM_GENERATION_ATTEMPTS,
5 | MAXIMUM_ZERO_WIDTH_SHORTEN_LENGTH,
6 | ZERO_WIDTH_CHARSET,
7 | } from '@/constants';
8 |
9 | export const shortenConfig = {
10 | zeroWidth: {
11 | charset: ZERO_WIDTH_CHARSET,
12 | length: MAXIMUM_ZERO_WIDTH_SHORTEN_LENGTH,
13 | },
14 | base64: {
15 | charset: BASE64_CHARSET,
16 | length: MAXIMUM_BASE64_SHORTEN_LENGTH,
17 | },
18 | maximumGenerationAttempts: MAXIMUM_GENERATION_ATTEMPTS,
19 | };
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@/*": ["src/*"]
20 | }
21 | },
22 | "include": ["next-env.d.ts", "cypress.config.ts", "src"],
23 | "exclude": ["node_modules"]
24 | }
25 |
--------------------------------------------------------------------------------
/.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.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # vercel
35 | .vercel
36 |
37 | # typescript
38 | *.tsbuildinfo
39 |
40 | # cypress
41 | /cypress/videos
42 | /cypress/screenshots
43 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import {ChakraProvider, extendTheme} from '@chakra-ui/react';
2 | import type {AppProps} from 'next/app';
3 | import '@fontsource/inter/400.css';
4 | import '@fontsource/inter/700.css';
5 |
6 | const theme = extendTheme({
7 | styles: {
8 | global: {
9 | 'html, body': {
10 | backgroundColor: '#f7f6f3',
11 | margin: 0,
12 | padding: 0,
13 | height: '100%',
14 | },
15 | },
16 | },
17 | fonts: {
18 | heading: 'Inter',
19 | body: 'Inter',
20 | },
21 | });
22 |
23 | const MyApp = ({Component, pageProps}: AppProps) => (
24 |
25 |
26 |
27 | );
28 |
29 | export default MyApp;
30 |
--------------------------------------------------------------------------------
/src/server/database/types/database-client.ts:
--------------------------------------------------------------------------------
1 | export interface DatabaseClient {
2 | queryOne>(
3 | parameters?: unknown,
4 | ): Promise;
5 |
6 | queryAll>(
7 | parameters?: unknown,
8 | ): Promise;
9 |
10 | findById>(
11 | id: number,
12 | ): Promise;
13 |
14 | create>(
15 | properties: unknown,
16 | ): Promise;
17 |
18 | update>(
19 | id: number,
20 | properties: unknown,
21 | ): Promise;
22 |
23 | delete(id: number): Promise;
24 | }
25 |
--------------------------------------------------------------------------------
/src/server/middlewares/validate.ts:
--------------------------------------------------------------------------------
1 | import type {NextApiHandler, NextApiRequest, NextApiResponse} from 'next';
2 | import {z} from 'zod';
3 |
4 | import {BadRequestError, InvalidInputError} from '@/server/errors';
5 |
6 | export function validate