feat: refactor to nx

This commit is contained in:
orig
2023-09-09 23:11:34 +03:00
parent 27d1b77fff
commit 4b107486d0
152 changed files with 18843 additions and 36307 deletions

View File

@ -1,15 +1,13 @@
# https://editorconfig.org
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
indent_size = 2
insert_final_newline = true
max_line_length = 80
trim_trailing_whitespace = true
[*.md]
max_line_length = 0
max_line_length = off
trim_trailing_whitespace = false

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
vite.config.ts

View File

@ -1,9 +0,0 @@
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"parserOptions": { "ecmaVersion": 2018, "sourceType": "module" },
"rules": {
"max-len": ["error", { "code": 130 }]
}
}

35
.eslintrc.json Normal file
View File

@ -0,0 +1,35 @@
{
"root": true,
"ignorePatterns": ["**/*"],
"plugins": ["@nx"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": ["*"]
}
]
}
]
}
},
{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nx/typescript"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nx/javascript"],
"rules": {}
}
]
}

View File

@ -1,39 +0,0 @@
name: Build
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16.x
registry-url: https://registry.npmjs.org/
- name: Cache Node Modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install NPM Dependencies
run: npm install && npm run install:all
- name: Build solution
if: always()
run: npm run build:all

29
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: CI
on:
push:
branches:
- master
pull_request:
jobs:
main:
name: Continuous Integration (CI)
runs-on: ubuntu-latest
permissions:
contents: 'read'
actions: 'read'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: nrwl/nx-set-shas@v3
with:
main-branch-name: 'master'
- run: npm ci
- run: npx nx format:check
- run: npx nx affected -t lint --parallel=3
- run: npx nx affected -t test --parallel=3
- run: npx nx affected -t build --parallel=3

View File

@ -1,73 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: 'CodeQL'
on:
push:
branches: ['master']
pull_request:
# The branches below must be a subset of the branches above
branches: ['master']
schedule:
- cron: '31 11 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ['javascript']
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: '/language:${{matrix.language}}'

View File

@ -5,75 +5,34 @@ on:
branches:
- master
workflow_dispatch:
workflow_run:
workflows: [CI]
types:
- completed
env:
REGISTRY: ghcr.io
IMAGE_NAME_BACKEND: ${{ github.repository }}/backend
IMAGE_NAME_FRONTEND: ${{ github.repository }}/frontend
NX_AFFECTED: true
NX_AFFECTED_BASE: origin/master
jobs:
backend-deployment:
name: Build and publish backend image
affected-deployment:
name: Build and publish affected images
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
permissions: 'write-all'
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Log in to the Container registry
uses: docker/login-action@v1
uses: actions/checkout@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
- name: Install npm dependencies
- uses: nrwl/nx-set-shas@v3
with:
main-branch-name: 'master'
- name: Install dependencies
run: npm ci
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v1
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_BACKEND }}
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: ./apps/backend
file: ./apps/backend/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
frontend-deployment:
name: Build and publish frontend image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Log in to the Container registry
uses: docker/login-action@v1
with:
@ -81,23 +40,7 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v1
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_FRONTEND }}
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: ./apps/frontend
file: ./apps/frontend/Dockerfile.prod
platforms: linux/amd64,linux/arm64
push: true
build-args: |
API_DOMAIN=${{ secrets.API_DOMAIN }}
DOMAIN=${{ secrets.DOMAIN }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build & Push Affected Images to Registry
id: deploy-images-to-registry
run: |
npx nx affected -t "push-image-to-registry" --repository=${{ github.repository }} --github-sha="$GITHUB_SHA"

View File

@ -1,39 +0,0 @@
name: Lint
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16.x
registry-url: https://registry.npmjs.org/
- name: Cache Node Modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install NPM Dependencies
run: npm install && npm run install:all
- name: Prettier Check
if: always()
run: npm run fmt.check

View File

@ -1,39 +0,0 @@
name: Test
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16.x
registry-url: https://registry.npmjs.org/
- name: Cache Node Modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install NPM Dependencies
run: npm install && npm run install:all
- name: Run tests
if: always()
run: npm run test:all

47
.gitignore vendored
View File

@ -1,21 +1,42 @@
# Coverage reports
coverage
# See http://help.github.com/ignore-files/ for more about ignoring files.
# API keys and secrets
# Secrets
.env
# Dependency directory
# compiled output
dist
tmp
/out-tsc
# dependencies
node_modules
bower_components
# Editors
.idea
*.iml
.vscode
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# OS metadata
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
# Ignore built ts files
dist/

View File

@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install lint-staged

View File

@ -1,10 +1,3 @@
# Files Prettier should not format
**/*.log
**/.DS_Store
*.
**/dist
**/node_modules
# Ignore all build directories on frontend
apps/frontend/server
# Add files here to ignore them from prettier formatting
/dist
/coverage

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"printWidth": 140
}

View File

@ -1,8 +0,0 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"useTabs": false,
"printWidth": 130
}

8
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"recommendations": [
"nrwl.angular-console",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"firsttris.vscode-jest-runner"
]
}

View File

@ -1,131 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][mozilla coc].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][faq]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[mozilla coc]: https://github.com/mozilla/diversity
[faq]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@ -1,104 +0,0 @@
# Contributing Guidelines
## Opening an issue:
Thank you for taking the time to open an issue.
<br>
Before opening an issue, please be sure that your issue hasn't already been asked by someone.
<br><br>
Here are a few things that will help us resolve your issues:
- A descriptive title that gives an idea of what your issue refers to
- A thorough description of the issue, (one word descriptions are very hard to understand)
- Screenshots (if appropriate)
- Links (if appropriate)
<br><br>
## Submitting a pull request:
Follow the below mentioned steps to open a pull request(PR):
#### If you don't have git on your machine, [install it](https://help.github.com/articles/set-up-git/).
## Fork this repository
Fork this repository by clicking on the fork button at the top of this page.
This will create a copy of this repository in your account.
## Clone the repository
Now clone the forked repository to your machine. Go to your GitHub account, open the forked repository, click on the code button and then click the _copy to clipboard_ icon.
Open a terminal and run the following git command:
```
git clone "url you just copied"
```
where "url you just copied" (without the quotation marks) is the url to this repository (your fork of this project). See the previous steps to obtain the url.
For example:
```
git clone https://github.com/this-is-you/reduced.to.git
```
where `this-is-you` is your GitHub username. Here you're copying the contents of the first-contributions repository on GitHub to your computer.
## Create a branch
Change to the repository directory on your computer (if you are not already there):
```
cd reduced.to
```
Now create a branch using the `git switch` command:
```
git switch -c your-new-branch-name
```
For example:
```
git switch -c improved-ui
```
## Make necessary changes and commit those changes
If you go to the project directory and execute the command `git status`, you'll see there are changes.
Add those changes to the branch you just created using the `git add` command:
```
git add "filename with extension in which you have made changes"
```
Now commit those changes using the `git commit` command:
```
git commit -m "Add relevant message to the change you made"
```
## Push changes to GitHub
Push your changes using the command `git push`:
```
git push origin -u your-branch-name
```
replacing `your-branch-name` with the name of the branch you created earlier.
## Submit your changes for review
If you go to your repository on GitHub, you'll see a `Compare & pull request` button. Click on that button.
Now submit the pull request.
Soon a reviewer will be merging all your changes into the main branch of this project. You will get a notification email once the changes have been merged.
## Note:
PRs that exclusively add items to the list of domains will be classed as spam/invalid. We already grab this list from ICANN's complete list of certified registrars.

22
LICENSE
View File

@ -1,22 +0,0 @@
(The MIT License)
Copyright (c) 2022-2023 Ori Granot
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -13,7 +13,7 @@ LOGGER_CONSOLE_THRESHOLD=INFO # DEBUG, INFO, WARN, ERROR, FATAL
FRONT_DOMAIN=http://localhost:5173
# DATABASE
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/reduced_to_db?schema=public"
DATABASE_URL="postgresql://postgres:postgres@postgres:5432/reduced_to_db?schema=public"
# REDIS
REDIS_ENABLE=false
@ -27,4 +27,4 @@ JWT_ACCESS_SECRET=abc1234
JWT_REFRESH_SECRET=abc1234
# NOVU - You don't need this when running locally
NOVU_API_KEY= Get it from https://novu.co/
NOVU_API_KEY= Get it from https://novu.co/

View File

@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@ -1,41 +0,0 @@
# Build
/dist
/lib
/lib-types
/server
# Development
node_modules
# Cache
.cache
.mf
.vscode
.rollup.cache
tsconfig.tsbuildinfo
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Editor
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Yarn
.yarn/*
!.yarn/releases
# env
.env

View File

@ -1,36 +1,43 @@
FROM node:18 AS build
# --------------------------------------------
# Dependencies Stage
# --------------------------------------------
FROM node:19.2-alpine3.15 as dependencies
# Required for Prisma Client to work in container
RUN apt-get update && apt-get install -y openssl
WORKDIR /app
COPY package.json package-lock.json ./
WORKDIR /svr/app
RUN apk add --update python3 make g++\
&& rm -rf /var/cache/apk/*
COPY --chown=node:node package*.json ./
COPY --chown=node:node prisma ./prisma/
# Install npm dependencies
RUN npm ci
COPY --chown=node:node . .
# --------------------------------------------
# Build Stage
# --------------------------------------------
# Intermediate docker image to build the bundle in and install dependencies
FROM node:19.2-alpine3.15 as build
WORKDIR /app
# Generate prisma files
RUN npx prisma generate
COPY . .
COPY --from=dependencies /app/node_modules ./node_modules
# Run the build command which creates the production bundle
RUN npm run build
# Run prisma generate & build the bundle in production mode
RUN npx nx build backend --prod --skip-nx-cache
# Set NODE_ENV environment variable
ENV NODE_ENV production
FROM node:18-alpine3.16 AS production
# --------------------------------------------
# Production Stage
# --------------------------------------------
FROM node:19.2-alpine3.15 as production
WORKDIR /app
# Copy the bundled code from the build stage to the production image
COPY --chown=node:node --from=build /svr/app/node_modules ./node_modules
COPY --chown=node:node --from=build /svr/app/dist ./dist
COPY --chown=node:node --from=build /svr/app/package*.json ./
COPY --chown=node:node --from=build /svr/app/prisma ./prisma
COPY --from=build /app/dist/apps/backend ./backend
COPY --from=build /app/dist/libs/prisma ./prisma
COPY --from=build /app/node_modules ./node_modules
COPY ./libs/ ./libs/
COPY ./nx.json ./nx.json
EXPOSE 3000
# Start the server using the production build
CMD [ "npm", "run", "start:migrate:prod" ]
# Start the application
CMD sh -c "npx nx migrate-dev prisma --name=init && node backend/main.js"

View File

@ -0,0 +1,11 @@
/* eslint-disable */
export default {
displayName: 'backend',
preset: '../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/backend',
};

File diff suppressed because it is too large Load Diff

View File

@ -1,90 +0,0 @@
{
"name": "reduced.to-backend",
"version": "1.0.0",
"description": "",
"scripts": {
"test": "jest --silent",
"build": "nest build",
"start": "nest start",
"start:dev": "nest start --watch",
"start:prod": "node dist/main",
"start:migrate:prod": "prisma migrate deploy && npm run start:prod",
"fmt": "prettier --write .",
"fmt.check": "prettier --check ."
},
"repository": {
"type": "git",
"url": "git+https://github.com/origranot/url-shortener.git"
},
"author": "OriG",
"license": "ISC",
"bugs": {
"url": "https://github.com/origranot/url-shortener/issues"
},
"homepage": "https://github.com/origranot/url-shortener#readme",
"dependencies": {
"@nestjs/cache-manager": "^2.0.0",
"@nestjs/cli": "^10.0.3",
"@nestjs/common": "^10.0.3",
"@nestjs/config": "^3.0.0",
"@nestjs/core": "^10.0.3",
"@nestjs/jwt": "^10.1.0",
"@nestjs/passport": "^10.0.0",
"@nestjs/platform-express": "^10.0.3",
"@nestjs/schematics": "^10.0.1",
"@nestjs/testing": "^10.0.3",
"@nestjs/throttler": "^4.1.0",
"@novu/node": "^0.16.0",
"@origranot/ts-logger": "^1.12.0",
"@prisma/client": "^4.16.2",
"bcrypt": "^5.1.0",
"body-parser": "^1.20.2",
"cache-manager": "^5.2.3",
"cache-manager-redis-store": "^3.0.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"express": "^4.18.2",
"jest": "^29.5.0",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"redis": "^4.6.7",
"reflect-metadata": "^0.1.13",
"ts-jest": "^29.1.0"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/cache-manager": "^4.0.2",
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/passport-jwt": "^3.0.8",
"@types/passport-local": "^1.0.35",
"@types/supertest": "^2.0.12",
"prettier": "2.8.8",
"prisma": "^4.16.2",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"tsconfig-paths": "4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@ -1,19 +0,0 @@
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN');
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"role" "Role" NOT NULL DEFAULT 'USER',
"verified" BOOLEAN NOT NULL DEFAULT false,
"verificationToken" TEXT,
"refreshToken" TEXT,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

90
apps/backend/project.json Normal file
View File

@ -0,0 +1,90 @@
{
"name": "backend",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/backend/src",
"projectType": "application",
"targets": {
"build": {
"executor": "@nx/webpack:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"target": "node",
"compiler": "tsc",
"outputPath": "dist/apps/backend",
"main": "apps/backend/src/main.ts",
"tsConfig": "apps/backend/tsconfig.app.json",
"isolatedConfig": true,
"webpackConfig": "apps/backend/webpack.config.js"
},
"dependsOn": [
{
"dependencies": true,
"target": "build"
}
],
"configurations": {
"development": {},
"production": {}
}
},
"serve": {
"executor": "@nx/js:node",
"defaultConfiguration": "development",
"options": {
"buildTarget": "backend:build"
},
"configurations": {
"development": {
"buildTarget": "backend:build:development"
},
"production": {
"buildTarget": "backend:build:production"
}
}
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/backend/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/backend/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
},
"dependsOn": [
{
"dependencies": true,
"target": "build"
}
]
},
"docker-build": {
"dependsOn": ["build"],
"command": "docker build -f apps/backend/Dockerfile . -t backend"
},
"push-image-to-registry": {
"dependsOn": ["docker-build"],
"executor": "nx:run-commands",
"options": {
"commands": [
"docker image tag backend ghcr.io/{args.repository}/backend:{args.github-sha}",
"docker push ghcr.io/{args.repository}/backend:{args.github-sha}"
],
"parallel": false
}
}
},
"tags": []
}

View File

@ -7,7 +7,7 @@ import { AppConfigModule } from './config/config.module';
import { AppConfigService } from './config/config.service';
import { AppLoggerModule } from './logger/logger.module';
import { NovuModule } from './novu/novu.module';
import { PrismaService } from './prisma/prisma.service';
import { PrismaService } from '@reduced.to/prisma';
import { UniqueConstraint } from './shared/decorators/unique/unique.decorator';
import { CustomThrottlerGuard } from './shared/guards/custom-throttler/custom-throttler';
import { ShortenerModule } from './shortener/shortener.module';

View File

@ -4,7 +4,7 @@ import { agent as supertest, SuperAgentTest } from 'supertest';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { NovuService } from '../novu/novu.service';
import { PrismaService } from '../prisma/prisma.service';
import { PrismaService } from '@reduced.to/prisma';
import { AppConfigService } from '../config/config.service';
import { SignupDto } from './dto/signup.dto';
import { UserContext } from './interfaces/user-context';
@ -14,7 +14,6 @@ import { JwtAuthGuard } from './guards/jwt.guard';
import { VerifyAuthGuard } from './guards/verify.guard';
import { UniqueConstraint } from '../shared/decorators/unique/unique.decorator';
import { useContainer } from 'class-validator';
import { AppModule } from '../app.module';
describe('AuthController', () => {
let app: INestApplication;

View File

@ -2,7 +2,7 @@ import { Body, Controller, Get, Post, Req, Res, UnauthorizedException, UseGuards
import { Request, Response } from 'express';
import { AppConfigService } from '../config/config.service';
import { NovuService } from '../novu/novu.service';
import { PrismaService } from '../prisma/prisma.service';
import { PrismaService } from '@reduced.to/prisma';
import { AuthService } from './auth.service';
import { SignupDto } from './dto/signup.dto';
import { JwtRefreshAuthGuard } from './guards/jwt-refresh.guard';

View File

@ -4,7 +4,7 @@ import { PassportModule } from '@nestjs/passport';
import { AppConfigService } from '../config/config.service';
import { NovuModule } from '../novu/novu.module';
import { NovuService } from '../novu/novu.service';
import { PrismaModule } from '../prisma/prisma.module';
import { PrismaModule } from '@reduced.to/prisma';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy';

View File

@ -1,10 +1,10 @@
import { UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Test, TestingModule } from '@nestjs/testing';
import { Role, User } from '@prisma/client';
import * as bcrypt from 'bcrypt';
import { Role, User } from '@reduced.to/prisma';
import * as bcrypt from 'bcryptjs';
import { AppConfigModule } from '../config/config.module';
import { PrismaService } from '../prisma/prisma.service';
import { PrismaService } from '@reduced.to/prisma';
import { AuthService } from './auth.service';
describe('AuthService', () => {

View File

@ -1,9 +1,9 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Role } from '@prisma/client';
import * as bcrypt from 'bcrypt';
import { Role } from '@reduced.to/prisma';
import * as bcrypt from 'bcryptjs';
import { AppConfigService } from '../config/config.service';
import { PrismaService } from '../prisma/prisma.service';
import { PrismaService } from '@reduced.to/prisma';
import { SignupDto } from './dto/signup.dto';
import { UserContext } from './interfaces/user-context';

View File

@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { RolesGuard } from './roles.guard';
import { Role } from '@prisma/client';
import { Role } from '@reduced.to/prisma';
import { UserContext } from '../interfaces/user-context';
describe('Roles Guard', () => {

View File

@ -1,6 +1,6 @@
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from '@prisma/client';
import { Role } from '@reduced.to/prisma';
import { ROLES_KEY } from '../../shared/decorators/roles/roles.decorator';
import { UserContext } from '../interfaces/user-context';

View File

@ -1,4 +1,4 @@
import { Role } from '@prisma/client';
import { Role } from '@reduced.to/prisma';
export interface UserContext {
id: string;

View File

@ -2,7 +2,7 @@ import { ValidationPipe, VersioningType } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { useContainer } from 'class-validator';
import * as cookieParser from 'cookie-parser';
import cookieParser from 'cookie-parser';
import { AppModule } from './app.module';
import { AppConfigService } from './config/config.service';

View File

@ -1,18 +0,0 @@
import { PrismaService } from './prisma.service';
import { Test } from '@nestjs/testing';
describe('PrismaService', () => {
let service: PrismaService;
beforeEach(async () => {
const app = await Test.createTestingModule({
providers: [PrismaService],
}).compile();
service = app.get<PrismaService>(PrismaService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -1,15 +0,0 @@
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
}

View File

@ -17,7 +17,9 @@ describe('IsVerified Decorator', () => {
// Sample controller to simulate the context
class TestController {
@IsVerified()
testMethod() {}
testMethod() {
// Empty method
}
}
const testController = new TestController();

View File

@ -1,6 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Reflector } from '@nestjs/core';
import { Role } from '@prisma/client';
import { Role } from '@reduced.to/prisma';
import { Roles, ROLES_KEY } from './roles.decorator';
describe('Roles Decorator', () => {
@ -20,7 +20,9 @@ describe('Roles Decorator', () => {
// Sample controller to simulate the context
class TestController {
@Roles(...roles)
testMethod() {}
testMethod() {
// Empty method
}
}
const testController = new TestController();
@ -35,7 +37,9 @@ describe('Roles Decorator', () => {
// Sample controller to simulate the context
class TestController {
@Roles(...roles)
testMethod() {}
testMethod() {
// Empty method
}
}
const testController = new TestController();
@ -47,7 +51,9 @@ describe('Roles Decorator', () => {
it('should not set any metadata of roles', () => {
// Sample controller to simulate the context
class TestController {
testMethod() {}
testMethod() {
// Empty method
}
}
const testController = new TestController();

View File

@ -1,5 +1,5 @@
import { SetMetadata } from '@nestjs/common';
import { Role } from '@prisma/client';
import { Role } from '@reduced.to/prisma';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);

View File

@ -12,7 +12,7 @@ describe('Sortable Decorator', () => {
it('should pass validation if sort field is empty', async () => {
const dto = new TestDto();
let errors = await validate(dto);
const errors = await validate(dto);
expect(errors.length).toBe(0);
});
@ -25,7 +25,7 @@ describe('Sortable Decorator', () => {
role: SortOrder.ASCENDING,
};
let errors = await validate(dto);
const errors = await validate(dto);
expect(errors.length).toBe(0);
});

View File

@ -1,5 +1,5 @@
import { UniqueConstraint, Unique } from './unique.decorator';
import { PrismaService } from '../../../prisma/prisma.service';
import { PrismaService } from '@reduced.to/prisma';
import { Test, TestingModule } from '@nestjs/testing';
import { ValidationArguments } from 'class-validator';

View File

@ -6,7 +6,7 @@ import {
ValidationArguments,
} from 'class-validator';
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import { PrismaService } from '@reduced.to/prisma';
@ValidatorConstraint({ name: 'Unique', async: true })
@Injectable()

View File

@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { ShortenerController } from './shortener.controller';
import { ShortenerService } from './shortener.service';
import { PrismaModule } from '../prisma/prisma.module';
import { PrismaModule } from '@reduced.to/prisma';
@Module({
imports: [PrismaModule],

View File

@ -4,7 +4,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { AppCacheModule } from '../cache/cache.module';
import { AppCacheService } from '../cache/cache.service';
import { AppConfigModule } from '../config/config.module';
import { PrismaService } from '../prisma/prisma.service';
import { PrismaService } from '@reduced.to/prisma';
import { ShortenerDto } from './dto';
import { BadRequestException } from '@nestjs/common';
import { UserContext } from '../auth/interfaces/user-context';

View File

@ -1,10 +1,10 @@
import { AppCacheService } from '../cache/cache.service';
import { BadRequestException, Injectable } from '@nestjs/common';
import { AppConfigService } from '../config/config.service';
import { PrismaService } from '../prisma/prisma.service';
import { PrismaService } from '@reduced.to/prisma';
import { ShortenerDto } from './dto';
import { UserContext } from '../auth/interfaces/user-context';
import { Url } from '@prisma/client';
import { Url } from '@reduced.to/prisma';
import { calculateDateFromTtl } from '../shared/utils';
@Injectable()

View File

@ -1,9 +1,9 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import request from 'supertest';
import { UsersController } from './users.controller';
import { IFindAllOptions, UsersService } from './users.service';
import { User } from '@prisma/client';
import { User } from '@reduced.to/prisma';
import { JwtAuthGuard } from '../auth/guards/jwt.guard';
import { AppConfigModule } from '../config/config.module';
import { RolesGuard } from '../auth/guards/roles.guard';

View File

@ -1,5 +1,5 @@
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { Role, User } from '@prisma/client';
import { Role, User } from '@reduced.to/prisma';
import { JwtAuthGuard } from '../auth/guards/jwt.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../shared/decorators/roles/roles.decorator';

View File

@ -1,9 +1,8 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
import { RolesGuard } from '../auth/guards/roles.guard';
import { PrismaModule } from '../prisma/prisma.module';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { PrismaModule } from '@reduced.to/prisma';
@Module({
imports: [AuthModule, PrismaModule],

View File

@ -1,6 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { IFindAllOptions, UsersService } from './users.service';
import { PrismaService } from '../prisma/prisma.service';
import { PrismaService } from '@reduced.to/prisma';
import { SortOrder } from '../shared/enums/sort-order.enum';
describe('UsersService', () => {

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { User } from '@prisma/client';
import { PrismaService } from '@reduced.to/prisma';
import { User } from '@reduced.to/prisma';
import { IPaginationResult, orderByBuilder } from '../shared/utils';
import { SortOrder } from '../shared/enums/sort-order.enum';

View File

@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["node"],
"emitDecoratorMetadata": true,
"target": "es2021"
},
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
}

View File

@ -1,4 +0,0 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View File

@ -1,21 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
"esModuleInterop": true
}
}

View File

@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

View File

@ -0,0 +1,8 @@
const { composePlugins, withNx } = require('@nx/webpack');
// Nx plugins for webpack.
module.exports = composePlugins(withNx(), (config) => {
// Update the webpack config as needed here.
// e.g. `config.plugins.push(new MyPlugin())`
return config;
});

View File

@ -1,36 +0,0 @@
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:qwik/recommended'],
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
ecmaVersion: 2021,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
plugins: ['@typescript-eslint'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-this-alias': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'prefer-spread': 'off',
'no-case-declarations': 'off',
'no-console': 'off',
'@typescript-eslint/no-unused-vars': ['error'],
},
};

View File

@ -0,0 +1,28 @@
{
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "../../.eslintrc.json"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["apps/frontend/tsconfig.*?.json"],
"ecmaVersion": 2021,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"plugins": ["@typescript-eslint"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@ -1,2 +1,2 @@
DOMAIN=localhost
API_DOMAIN=http://localhost:3000
API_DOMAIN=http://backend:3000 # Use this for internal (Server sided) API calls

View File

@ -1,45 +0,0 @@
# Build
/dist
/lib
/lib-types
/server
# Temp
/temp
/tmp
# Development
node_modules
# Cache
.cache
.mf
.vscode
.rollup.cache
tsconfig.tsbuildinfo
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Editor
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Yarn
.yarn/*
!.yarn/releases
# env
.env

View File

@ -1,36 +1,51 @@
ARG API_DOMAIN
ARG CLIENTSIDE_API_DOMAIN
ARG DOMAIN
# --------------------------------------------
# Dependencies Stage
# --------------------------------------------
FROM node:19.2-alpine3.15 as dependencies
WORKDIR /app
COPY package.json package-lock.json ./
RUN apk add --update python3 make g++\
&& rm -rf /var/cache/apk/*
RUN npm ci
# --------------------------------------------
# Build Stage
# --------------------------------------------
# Intermediate docker image to build the bundle in and install dependencies
FROM node:19.2-alpine3.15 as build
WORKDIR /app
# Set the working directory to /usr/src/app
WORKDIR /usr/src/app
COPY . .
COPY --from=dependencies /app/node_modules ./node_modules
# Copy the package.json and package-lock.json over in the intermedate "build" image
COPY ./package.json ./
COPY ./package-lock.json ./
# Run prisma generate & build the bundle in production mode
RUN npx nx build frontend --prod --skip-nx-cache
# Clean install because we want to install the exact versions (Include dev dependencies)
RUN npm ci --include=dev
# Copy the source code into the build image
COPY ./ ./
# Build the project
RUN npm run build
# Pull the same Node image and use it as the final (production image)
# --------------------------------------------
# Production Stage
# --------------------------------------------
FROM node:19.2-alpine3.15 as production
WORKDIR /app
# Set the working directory to /usr/src/app
WORKDIR /usr/src/app
# Only copy the results from the build over to the final image
# We do this to keep the final image as small as possible
COPY --from=build /usr/src/app/node_modules ./node_modules
COPY --from=build /usr/src/app/server ./server
COPY --from=build /usr/src/app/dist ./dist
COPY --from=build /app/dist/apps/frontend ./frontend
COPY --from=build /app/node_modules ./node_modules
EXPOSE 5000
# Start the application
CMD [ "node", "server/entry.express"]
CMD [ "node", "frontend/server/entry.express.js"]

View File

@ -1,41 +0,0 @@
FROM node:18-alpine3.15 AS build
# Set NODE_ENV environment variable
ENV NODE_ENV production
# Create app directory
WORKDIR /app
# Install app dependencies
COPY *.json ./
# Copy all files to build stage
COPY . .
# Define Buildtime vairables from workflow
ARG API_DOMAIN=${API_DOMAIN}
ARG DOMAIN=${DOMAIN}
# Install dependencies for production
RUN npm ci --include=dev
# Building code for production
RUN npm run build
# Build static files
RUN npm run build.server
# Final stage
FROM node:18-alpine3.15 as production
# Create app directory
WORKDIR /app
# Copy build
COPY --from=build /app/node_modules/ ./node_modules/
COPY --from=build /app/dist/ ./dist/
COPY --from=build /app/server/ ./server/
EXPOSE 5000
CMD ["sh", "-c", "ORIGIN=https://reduced.to node server/entry.express.js" ]

View File

@ -1,18 +1,17 @@
# Qwik App ⚡️
# Qwik City App ⚡️
- [Qwik Docs](https://qwik.builder.io/)
- [Discord](https://qwik.builder.io/chat)
- [Qwik GitHub](https://github.com/BuilderIO/qwik)
- [@QwikDev](https://twitter.com/QwikDev)
- [Vite](https://vitejs.dev/)
- [Partytown](https://partytown.builder.io/)
- [Mitosis](https://github.com/BuilderIO/mitosis)
- [Builder.io](https://www.builder.io/)
---
## Project Structure
This project is using Qwik with [QwikCity](https://qwik.builder.io/qwikcity/overview/). QwikCity is just a extra set of tools on top of Qwik to make it easier to build a full site, including directory-based routing, layouts, and more.
Inside your project, you'll see the following directory structure:
```
@ -31,12 +30,12 @@ Inside your project, you'll see the following directory structure:
- `public`: Any static assets, like images, can be placed in the public directory. Please see the [Vite public directory](https://vitejs.dev/guide/assets.html#the-public-directory) for more info.
## Add Integrations
## Add Integrations and deployment
Use the `npm run qwik add` command to add additional integrations. Some examples of integrations include: Cloudflare, Netlify or Vercel server, and the [Static Site Generator (SSG)](https://qwik.builder.io/qwikcity/static-site-generation/static-site-config/).
Use the `pnpm qwik add` command to add additional integrations. Some examples of integrations include: Cloudflare, Netlify or Express server, and the [Static Site Generator (SSG)](https://qwik.builder.io/qwikcity/static-site-generation/static-site-config/).
```shell
npm run qwik add # or `yarn qwik add`
pnpm qwik add # or `yarn qwik add`
```
## Development
@ -44,7 +43,7 @@ npm run qwik add # or `yarn qwik add`
Development mode uses [Vite's development server](https://vitejs.dev/). During development, the `dev` command will server-side render (SSR) the output.
```shell
npm run dev # or `yarn dev`
npm start # or `yarn start`
```
> Note: during dev mode, Vite may request a significant number of `.js` files. This does not represent a Qwik production build.
@ -54,7 +53,7 @@ npm run dev # or `yarn dev`
The preview command will create a production build of the client modules, a production build of `src/entry.preview.tsx`, and run a local server. The preview server is only for convenience to locally preview a production build, and it should not be used as a production server.
```shell
npm run preview # or `yarn preview`
pnpm preview # or `yarn preview`
```
## Production
@ -62,27 +61,5 @@ npm run preview # or `yarn preview`
The production build will generate client and server modules by running both client and server build commands. Additionally, the build command will use Typescript to run a type check on the source code.
```shell
npm run build # or `yarn build`
pnpm build # or `yarn build`
```
## Static Site Generator (Node.js)
```
npm run ssg
```
## Static Site Generator (Node.js)
```
npm run ssg
```
## Express Server
This app has a minimal [Express server](https://expressjs.com/) implementation. After running a full build, you can preview the build using the command:
```
npm run serve
```
Then visit [http://localhost:8080/](http://localhost:8080/)

View File

@ -7,7 +7,7 @@ export default extendConfig(baseConfig, () => {
build: {
ssr: true,
rollupOptions: {
input: ['src/entry.express.tsx', '@qwik-city-plan'],
input: ['apps/frontend/src/entry.express.tsx', '@qwik-city-plan'],
},
},
plugins: [

File diff suppressed because it is too large Load Diff

View File

@ -1,55 +0,0 @@
{
"name": "reduced.to-frontend",
"description": "",
"engines": {
"node": ">=15.0.0"
},
"private": true,
"scripts": {
"build": "qwik build",
"build.client": "vite build",
"build.preview": "vite build --ssr src/entry.preview.tsx",
"build.server": "vite build -c adapters/express/vite.config.ts",
"build.types": "tsc --incremental --noEmit",
"dev": "vite --mode ssr",
"dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force",
"fmt": "prettier --write .",
"fmt.check": "prettier --check .",
"lint": "eslint \"src/**/*.ts*\"",
"preview": "qwik build preview && vite preview --open",
"serve": "node server/entry.express",
"start": "vite --open --mode ssr",
"qwik": "qwik"
},
"devDependencies": {
"@builder.io/qwik": "^1.2.3",
"@builder.io/qwik-city": "^1.2.3",
"@tailwindcss/typography": "^0.5.9",
"@types/canvas-confetti": "^1.4.3",
"@types/eslint": "8.4.6",
"@types/express": "4.17.13",
"@types/node": "latest",
"@types/qrcode": "^1.5.0",
"@typescript-eslint/eslint-plugin": "5.37.0",
"@typescript-eslint/parser": "5.37.0",
"autoprefixer": "^10.4.12",
"daisyui": "^2.51.5",
"dotenv": "^16.0.3",
"eslint": "8.23.1",
"eslint-plugin-qwik": "^1.2.3",
"express": "^4.18.2",
"node-fetch": "^3.2.10",
"nodemon": "^2.0.20",
"postcss": "^8.4.17",
"tailwindcss": "^3.3.2",
"typescript": "4.8.3",
"vite": "^4.3.9",
"vite-tsconfig-paths": "3.5.0"
},
"dependencies": {
"axios": "^1.1.2",
"canvas-confetti": "^1.5.1",
"jwt-decode": "^3.1.2",
"qrcode": "^1.5.1"
}
}

View File

@ -1,6 +1,11 @@
const { join } = require('path');
module.exports = {
plugins: {
tailwindcss: {},
'tailwindcss/nesting': {},
autoprefixer: {},
tailwindcss: {
config: join(__dirname, 'tailwind.config.js'),
},
},
};

View File

@ -0,0 +1,86 @@
{
"name": "frontend",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"sourceRoot": "apps/frontend/src",
"targets": {
"build": {
"executor": "qwik-nx:build",
"options": {
"runSequence": ["frontend:build.client", "frontend:build.ssr"],
"outputPath": "dist/apps/frontend"
},
"configurations": {
"preview": {}
}
},
"build.client": {
"executor": "@nx/vite:build",
"options": {
"outputPath": "dist/apps/frontend",
"configFile": "apps/frontend/vite.config.ts"
}
},
"build.ssr": {
"executor": "@nx/vite:build",
"defaultConfiguration": "express",
"options": {
"outputPath": "dist/apps/frontend",
"configFile": "apps/frontend/adapters/express/vite.config.ts"
}
},
"preview": {
"executor": "@nx/vite:preview-server",
"options": {
"buildTarget": "frontend:build",
"port": 4300
}
},
"test": {
"executor": "@nx/vite:test",
"outputs": ["../../coverage/apps/frontend"],
"options": {
"passWithNoTests": true,
"reportsDirectory": "../../coverage/apps/frontend"
}
},
"serve": {
"executor": "@nx/vite:dev-server",
"options": {
"buildTarget": "frontend:build.client",
"mode": "ssr",
"port": 4200
}
},
"serve.debug": {
"executor": "nx:run-commands",
"options": {
"command": "node --inspect-brk ../../node_modules/vite/bin/vite.js --mode ssr --force",
"cwd": "apps/frontend"
}
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/frontend/**/*.{ts,tsx,js,jsx}"]
}
},
"docker-build": {
"dependsOn": ["build"],
"command": "docker build -f apps/frontend/Dockerfile . -t frontend"
},
"push-image-to-registry": {
"dependsOn": ["docker-build"],
"executor": "nx:run-commands",
"options": {
"commands": [
"docker image tag frontend ghcr.io/{args.repository}/frontend:{args.github-sha}",
"docker push ghcr.io/{args.repository}/frontend:{args.github-sha}"
],
"parallel": false
}
}
},
"tags": []
}

View File

@ -1,13 +1,13 @@
import { confettiAnimate } from '~/lib/confetti';
import { Store } from '~/routes';
import { copyToClipboard, normalizeUrl } from '~/utils';
import { confettiAnimate } from '../../lib/confetti';
import { Store } from '../../routes';
import { copyToClipboard, normalizeUrl } from '../../utils';
/**
* Returns the shorter link from the server.
* @param {string} originalUrl - The original url we want to shorten.
*/
const getShortenUrl = async (originalUrl: string, ttl: number) => {
const result = await fetch(`${process.env.API_DOMAIN}/api/v1/shortener`, {
const result = await fetch(`${process.env.CLIENTSIDE_API_DOMAIN}/api/v1/shortener`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },

View File

@ -1,10 +1,10 @@
import { component$, useContext, $, useOnDocument, useSignal, QwikKeyboardEvent } from '@builder.io/qwik';
import { InputContext } from '~/routes';
import { InputContext } from '../../routes';
import { ShortenerInputBtn } from './shortener-input-btn';
import { TIME_FRAME_DIR } from './constants';
import { useGetCurrentUser } from '~/routes/layout';
import { Select } from '~/components/select/select';
import { ArrowDoodle } from '~/components/arrow-doodle/arrow-doodle';
import { useGetCurrentUser } from '../../routes/layout';
import { Select } from '../../components/select/select';
import { ArrowDoodle } from '../../components/arrow-doodle/arrow-doodle';
export interface ShortenerInputProps {
onKeyUp$: (event: QwikKeyboardEvent<HTMLInputElement>) => void;

View File

@ -1,5 +1,5 @@
import { $, component$, useContext, useStylesScoped$ } from '@builder.io/qwik';
import { GlobalStore } from '~/context';
import { GlobalStore } from '../../context';
import { themeStorageKey } from '../router-head/theme-script';
import styles from './theme-switcher.css?inline';
@ -7,13 +7,20 @@ export const DARK_THEME = 'dracula';
export const LIGHT_THEME = 'light';
export type ThemePreference = typeof DARK_THEME | typeof LIGHT_THEME;
export const colorSchemeChangeListener = (onColorSchemeChange: (isDark: boolean) => void) => {
export const colorSchemeChangeListener = (
onColorSchemeChange: (isDark: boolean) => void
) => {
const listener = ({ matches: isDark }: MediaQueryListEvent) => {
onColorSchemeChange(isDark);
};
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => listener(event));
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (event) => listener(event));
return () => window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', listener);
return () =>
window
.matchMedia('(prefers-color-scheme: dark)')
.removeEventListener('change', listener);
};
export const setPreference = (theme: ThemePreference) => {
@ -30,7 +37,9 @@ export const getColorPreference = (): ThemePreference => {
if (localStorage.getItem(themeStorageKey)) {
return localStorage.getItem(themeStorageKey) as ThemePreference;
} else {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? DARK_THEME : LIGHT_THEME;
return window.matchMedia('(prefers-color-scheme: dark)').matches
? DARK_THEME
: LIGHT_THEME;
}
};

View File

@ -15,15 +15,15 @@ import express from 'express';
import { fileURLToPath } from 'node:url';
import { join } from 'node:path';
import { IncomingMessage } from 'node:http';
import { Http2ServerRequest } from 'node:http2';
declare global {
// eslint-disable-next-line
interface QwikCityPlatform extends PlatformNode {}
}
// import compression from 'compression';
// Directories where the static assets are located
const distDir = join(fileURLToPath(import.meta.url), '..', '..', 'dist');
const distDir = join(fileURLToPath(import.meta.url), '..', '..', 'client');
const buildDir = join(distDir, 'build');
// Allow for dynamic port
@ -34,7 +34,7 @@ const { router, notFound } = createQwikCity({
render,
qwikCityPlan,
manifest,
getClientConn: (request: IncomingMessage) => {
getClientConn: (request: IncomingMessage | Http2ServerRequest) => {
// We need to override the default getClientConn function to get the client IP address from the request headers (x-forwarded-for or x-real-ip)
return {
ip: (request.headers['x-forwarded-for'] as string) || (request.headers['x-real-ip'] as string) || undefined,

View File

@ -1,6 +1,18 @@
/*
* WHAT IS THIS FILE?
*
* It's the bundle entry point for `npm run preview`.
* That is, serving your app built in production mode.
*
* Feel free to modify this file, but don't remove it!
*
* Learn more about Vite's preview command:
* - https://vitejs.dev/config/preview-options.html#preview-options
*
*/
import { createQwikCity } from '@builder.io/qwik-city/middleware/node';
import qwikCityPlan from '@qwik-city-plan';
import render from './entry.ssr';
import qwikCityPlan from '@qwik-city-plan';
/**
* The default export is the QwikCity adaptor used by Vite preview.

View File

@ -1,18 +1,18 @@
import { component$, createContextId, useContextProvider, useSignal, useStore, useStylesScoped$ } from '@builder.io/qwik';
import { DocumentHead } from '@builder.io/qwik-city';
import animations from '~/assets/css/animations.css?inline';
import { handleShareOnTwitter } from '~/components/home-buttons/actions';
import { TwitterButton } from '~/components/home-buttons/twitter-button/twitter-button';
import { Loader } from '~/components/loader/loader';
import { generateQRCode } from '~/components/qr-code/handleQRCode';
import { QRCode } from '~/components/qr-code/qr-code';
import { handleShortener } from '~/components/shortener-input/handleShortener';
import { ShortenerInput } from '~/components/shortener-input/shortener-input';
import { Tooltip } from '~/components/tooltip/tooltip';
import { Waves } from '~/components/waves/waves';
import { copyToClipboard, openUrl } from '~/utils';
import animations from '../assets/css/animations.css?inline';
import { handleShareOnTwitter } from '../components/home-buttons/actions';
import { TwitterButton } from '../components/home-buttons/twitter-button/twitter-button';
import { Loader } from '../components/loader/loader';
import { generateQRCode } from '../components/qr-code/handleQRCode';
import { QRCode } from '../components/qr-code/qr-code';
import { handleShortener } from '../components/shortener-input/handleShortener';
import { ShortenerInput } from '../components/shortener-input/shortener-input';
import { Tooltip } from '../components/tooltip/tooltip';
import { Waves } from '../components/waves/waves';
import { copyToClipboard, openUrl } from '../utils';
import styles from './index.css?inline';
import { TIME_FRAME_DIR } from '~/components/shortener-input/constants';
import { TIME_FRAME_DIR } from '../components/shortener-input/constants';
export const InputContext = createContextId<Store>('input');

View File

@ -13,7 +13,7 @@ export default component$(() => {
useVisibleTask$(() => {
store.token = params.token;
fetch(`${process.env.API_DOMAIN}/api/v1/auth/verify?token=${store.token}`, {
fetch(`${process.env.CLIENTSIDE_API_DOMAIN}/api/v1/auth/verify?token=${store.token}`, {
method: 'GET',
headers: {
token: store.token,

View File

@ -1,7 +1,7 @@
import { component$, useStore, useVisibleTask$ } from '@builder.io/qwik';
import { Link, RequestHandler } from '@builder.io/qwik-city';
import { Loader } from '~/components/loader/loader';
import { authorizedFetch, validateAccessToken } from '~/shared/auth.service';
import { Loader } from '../../../components/loader/loader';
import { authorizedFetch, validateAccessToken } from '../../../shared/auth.service';
export interface Store {
isVerified: boolean;
@ -24,7 +24,7 @@ export default component$(() => {
});
useVisibleTask$(() => {
authorizedFetch(`${process.env.API_DOMAIN}/api/v1/auth/verified`).then(async (response) => {
authorizedFetch(`${process.env.CLIENTSIDE_API_DOMAIN}/api/v1/auth/verified`).then(async (response) => {
const { verified } = await response.json();
store.isVerified = verified;
store.loading = false;
@ -54,7 +54,7 @@ export default component$(() => {
<button
class="btn btn-primary"
onClick$={async () => {
authorizedFetch(`${process.env.API_DOMAIN}/api/v1/auth/resend`).then(() => {
authorizedFetch(`${process.env.CLIENTSIDE_API_DOMAIN}/api/v1/auth/resend`).then(() => {
store.resent = true;
});
}}

View File

@ -1,5 +1,5 @@
import { component$, Slot } from '@builder.io/qwik';
import NotFoundLayout from '~/layouts/NotFoundLayout';
import NotFoundLayout from '../../layouts/NotFoundLayout';
export default component$(() => {
return (

View File

@ -75,7 +75,7 @@ export const authorizedFetch = async (url: string, options = {}) => {
};
export const refreshTokens = async (refreshToken: string): Promise<{ accessToken: string; refreshToken: string }> => {
const res = await fetch(`${process.env.API_DOMAIN}/api/v1/auth/refresh`, {
const res = await fetch(`${process.env.CLIENTSIDE_API_DOMAIN}/api/v1/auth/refresh`, {
method: 'POST',
credentials: 'include',
headers: {

View File

@ -2,12 +2,12 @@ export const copyToClipboard = async (data: string): Promise<void> => {
return navigator.clipboard.writeText(data);
};
export const openUrl = (url: string | URL, target: string = '_blank'): void => {
export const openUrl = (url: string | URL, target = '_blank'): void => {
window.open(url, target);
};
/**
* Normalize input url
* Normalize url input
* - add protocol 'http' if missing.
* - correct protocol http/https if mistyped one character.
* @param {string} url

View File

@ -1,6 +1,8 @@
const { join } = require('path');
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
content: [join(__dirname, 'src/**/*.{js,ts,jsx,tsx,mdx}')],
darkMode: 'class',
theme: {
extend: {

View File

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc"
},
"files": [],
"exclude": [],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", ".eslintrc.json"]
}

View File

@ -1,4 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"allowJs": true,
"target": "ES2017",
@ -7,7 +8,6 @@
"jsx": "react-jsx",
"jsxImportSource": "@builder.io/qwik",
"strict": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"esModuleInterop": true,
@ -16,11 +16,15 @@
"isolatedModules": true,
"outDir": "tmp",
"noEmit": true,
"types": ["node", "vite/client"],
"paths": {
"~/*": ["./src/*"]
}
"types": ["node", "vite/client", "vitest"]
},
"files": ["./.eslintrc.cjs"],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"include": ["src"]
}

View File

@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["vitest/globals", "node"]
},
"include": [
"vite.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}

View File

@ -1,20 +1,60 @@
import { qwikCity } from '@builder.io/qwik-city/vite';
import { qwikVite } from '@builder.io/qwik/optimizer';
import { qwikCity } from '@builder.io/qwik-city/vite';
import { defineConfig, loadEnv } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import { qwikNxVite } from 'qwik-nx/plugins';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
console.log('env', {
DOMAIN: env.DOMAIN,
CLIENTSIDE_API_DOMAIN: env.CLIENTSIDE_API_DOMAIN,
API_DOMAIN: env.API_DOMAIN,
});
return {
plugins: [qwikCity(), qwikVite(), tsconfigPaths()],
cacheDir: '../../node_modules/.vite/apps/frontend',
plugins: [
qwikNxVite(),
qwikCity(),
qwikVite({
client: {
outDir: '../../dist/apps/frontend/client',
},
ssr: {
outDir: '../../dist/apps/frontend/server',
},
}),
tsconfigPaths({ root: '../../' }),
],
define: {
'process.env.API_DOMAIN': JSON.stringify(env.API_DOMAIN),
'process.env.DOMAIN': JSON.stringify(env.DOMAIN),
'process.env.API_DOMAIN': JSON.stringify(env.API_DOMAIN),
'process.env.CLIENTSIDE_API_DOMAIN': JSON.stringify(env.CLIENTSIDE_API_DOMAIN),
'process.env.NODE_ENV': JSON.stringify(mode),
},
optimizeDeps: {
include: ['canvas-confetti'],
},
server: {
fs: {
// Allow serving files from the project root
allow: ['../../'],
},
},
preview: {
headers: {
'Cache-Control': 'public, max-age=600',
},
},
test: {
globals: true,
cache: {
dir: '../../node_modules/.vitest',
},
environment: 'node',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
},
};
});

View File

@ -10,6 +10,8 @@ services:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=reduced_to_db
volumes:
- ../../tmp/data/postgresql:/var/lib/postgresql/data
ports:
- '5432:5432'
@ -27,25 +29,26 @@ services:
frontend:
container_name: frontend
build:
context: ../../apps/frontend
dockerfile: Dockerfile
context: ../..
dockerfile: apps/frontend/Dockerfile
args:
- DOMAIN=${DOMAIN}
- API_DOMAIN=${API_DOMAIN}
- CLIENTSIDE_API_DOMAIN=${CLIENTSIDE_API_DOMAIN}
restart: always
ports:
- '5000:5000'
env_file: ../../apps/frontend/.env
volumes:
- /app
backend:
container_name: backend
platform: linux/amd64
build:
context: ../../apps/backend
dockerfile: Dockerfile
context: ../..
dockerfile: apps/backend/Dockerfile
restart: always
volumes:
- ../../apps/backend:/svr/app/
- /svr/app/node_modules
- ../../apps/backend:/app/apps/backend/
ports:
- '3000:3000'
env_file: ../../apps/backend/.env

20
docs/.gitignore vendored
View File

@ -1,20 +0,0 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -1,8 +0,0 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"useTabs": false,
"printWidth": 130
}

View File

@ -1,41 +0,0 @@
# Website
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
### Installation
```
$ yarn
```
### Local Development
```
$ yarn start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
### Build
```
$ yarn build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
### Deployment
Using SSH:
```
$ USE_SSH=true yarn deploy
```
Not using SSH:
```
$ GIT_USER=<Your GitHub username> yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.

View File

@ -1,3 +0,0 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};

View File

@ -1,5 +0,0 @@
# Soon
```js
// TODO: Add inforamtion about the api
```

View File

@ -1,94 +0,0 @@
# API Usage
The reduced.to URL Shortener API allows you to shorten long URLs into shorter, more manageable ones. This documentation provides details on how to use the API effectively.
## API Base URL
The base URL for the API is `https://reduced.to/api/v1/shortener`.
## API Usage
| Method | Endpoint | Content-Type | Request Body | Description |
| ------ | ----------------- | ---------------- | --------------------------------------- | -------------------------------------- |
| POST | /api/v1/shortener | application/json | `{"originalUrl": "https://google.com"}` | Shortens a URL and returns the result. |
To shorten a URL, make a `POST` request to the base URL with the following JSON payload:
POST /api/v1/shortener
Content-Type: application/json
```json
{
"originalUrl": "https://google.com"
}
```
Upon successful execution of the request, you will receive a JSON response containing the shortened URL. The response object will have the following structure:
```json
{
"newUrl": "vv9ip"
}
```
## Javascript Example
```javascript
// Define the API endpoint
const url = 'https://reduced.to/api/v1/shortener';
// Define the original URL to be shortened
const originalUrl = 'https://google.com';
// Create the request payload
const payload = {
originalUrl: originalUrl,
};
// Send the POST request to shorten the URL
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
.then((response) => response.json())
.then((data) => {
// Extract the shortened URL from the response
const shortenedUrl = data.newUrl;
// Print the shortened URL
console.log(`Shortened URL: https://reduced.to/${shortenedUrl}`);
})
.catch((error) => {
console.error('Error:', error.message);
});
```
# Python Example
```python
import requests
import json
# Define the API endpoint
url = 'https://reduced.to/api/v1/shortener'
# Define the original URL to be shortened
original_url = 'https://google.com'
# Create the request payload
payload = {
'originalUrl': original_url
}
# Send the POST request to shorten the URL
response = requests.post(url, json=payload)
# Extract the shortened URL from the response
shortened_url = response.json()['newUrl']
# Print the shortened URL
print(f'Shortened URL: https://reduced.to/{shortened_url}')
```

View File

@ -1,31 +0,0 @@
---
title: Overview
description: Overview of the project
hide_table_of_contents: true
---
### What is Redcued.to?
Reduced.to is a modern web application designed to shorten link URLs, making them easier to remember, share, and track. The project is developed using cutting-edge technologies and services, including NestJS, Redis, Novu, PostgreSQL, and Qwik.
### How does it work?
Simply paste a long URL into the input field and click the "Shorten" button. The application will generate a shortened URL that you can copy and share with others. When a user clicks on the shortened URL, they will be redirected to the original long URL. There is also a dashboard for registered users to view their shortened URLs and track their performance.
### Technologies and Services
To ensure optimal performance, the project uses Redis as a cache storage mechanism. Redis is an efficient, in-memory data structure store that enables fast retrieval of key-value pairs. By leveraging Redis, we can store and retrieve mappings between short links and their corresponding long URLs in a highly efficient manner.
We uses NestJS as the backend framework for the project. NestJS is a progressive Node.js framework that offers a modular architecture and a powerful CLI. By leveraging NestJS, we can develop a robust backend that is easy to maintain and scale.
For data storage, we utilize PostgreSQL, a reliable and robust open-source relational database. PostgreSQL offers excellent data integrity and powerful querying capabilities, ensuring secure storage of user-related information while enabling efficient retrieval and manipulation as needed.
The frontend of the project is developed using Qwik, a cutting-edge framework known for its enhanced performance and scalability. By leveraging Qwik, we deliver a smooth and seamless user interface for an exceptional user experience.
In summary, the reduced.to project is a comprehensive URL shortener that incorporates various technologies and services to ensure optimal performance, security, and user experience. With the integration of Redis, Novu, PostgreSQL, and Qwik, we provide a reliable, efficient, and user-friendly platform for generating shortened URLs.
### Project Architecture Overview
![Project Architecture](/images/architecture.png)
Feel free to explore and contribute to this open-source project. We welcome new contributors and encourage you to join our community!

View File

@ -1,87 +0,0 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
const lightCodeTheme = require('prism-react-renderer/themes/github');
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'Reduced.to',
staticDirectories: ['public', 'static'],
tagline:
"Reduced.to is a modern web application that reduces the length of link URL, so it's easier to remember, share and track.",
favicon: 'images/favicon.png',
url: 'https://reduced.to/',
baseUrl: '/',
projectName: 'reduced.to',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
i18n: {
defaultLocale: 'en',
locales: ['en'],
},
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
sidebarPath: require.resolve('./sidebars.js'),
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
routeBasePath: '/',
editUrl: '/',
},
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
}),
],
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
navbar: {
logo: {
alt: 'Reduced.to Logo',
src: `images/logo.svg`,
//srcDark: `img/logo.svg`,
href: '/',
target: '_self',
},
items: [
{
type: 'search',
position: 'right',
},
{
position: 'right',
href: 'https://reduced.to',
label: 'Visit App',
},
{
position: 'right',
href: 'https://github.com/origranot/reduced.to',
html: `
<a aria-label="GitHub" class="navbar-github-link">
<img src="images/github-logo.png" alt="GitHub Logo" class="navbar-github-logo" />
</a>
`,
},
],
},
footer: {
copyright: `Copyright © ${new Date().getFullYear()} Reduced.to, Inc. Built with Docusaurus.`,
},
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
},
}),
};
module.exports = config;

12668
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +0,0 @@
{
"name": "my-website",
"version": "0.0.0",
"private": true,
"scripts": {
"fmt.check": "prettier --check .",
"fmt": "prettier --write .",
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc",
"lint": "prettier --list-different \"./src/**/*.{ts,tsx,json,md}\""
},
"dependencies": {
"@docusaurus/core": "2.4.1",
"@docusaurus/preset-classic": "2.4.1",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.4.1",
"@tsconfig/docusaurus": "^1.0.5",
"prettier": "^2.8.8",
"typescript": "^4.7.4"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"engines": {
"node": ">=16.14"
}
}

View File

@ -1,26 +0,0 @@
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
*/
// @ts-check
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
tutorialSidebar: [
'overview',
{
type: 'category',
label: 'API',
items: ['api/soon'],
},
],
};
module.exports = sidebars;

View File

@ -1,67 +0,0 @@
import React from 'react';
import clsx from 'clsx';
import styles from './styles.module.css';
type FeatureItem = {
title: string;
Svg: React.ComponentType<React.ComponentProps<'svg'>>;
description: JSX.Element;
};
const FeatureList: FeatureItem[] = [
{
title: 'Easy to Use',
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
description: (
<>Docusaurus was designed from the ground up to be easily installed and used to get your website up and running quickly.</>
),
},
{
title: 'Focus on What Matters',
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
description: (
<>
Docusaurus lets you focus on your docs, and we&apos;ll do the chores. Go ahead and move your docs into the{' '}
<code>docs</code> directory.
</>
),
},
{
title: 'Powered by React',
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
description: (
<>
Extend or customize your website layout by reusing React. Docusaurus can be extended while reusing the same header and
footer.
</>
),
},
];
function Feature({ title, Svg, description }: FeatureItem) {
return (
<div className={clsx('col col--4')}>
<div className="text--center">
<Svg className={styles.featureSvg} role="img" />
</div>
<div className="text--center padding-horiz--md">
<h3>{title}</h3>
<p>{description}</p>
</div>
</div>
);
}
export default function HomepageFeatures(): JSX.Element {
return (
<section className={styles.features}>
<div className="container">
<div className="row">
{FeatureList.map((props, idx) => (
<Feature key={idx} {...props} />
))}
</div>
</div>
</section>
);
}

Some files were not shown because too many files have changed in this diff Show More