├── .nvmrc ├── .dockerignore ├── apps ├── web │ ├── hooks │ │ ├── useGoogleLogin.tsx │ │ └── use-mobile.tsx │ ├── types │ │ ├── react-open-weather.d.ts │ │ └── react-weather-widget.d.ts │ ├── app │ │ ├── favicon.ico │ │ ├── login │ │ │ └── page.tsx │ │ ├── signup │ │ │ └── page.tsx │ │ ├── profile │ │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── verify-email │ │ │ ├── page.tsx │ │ │ ├── VerifyEmail.tsx │ │ │ └── ShadcnVerifyEmail.tsx │ │ ├── forgot-password │ │ │ ├── page.tsx │ │ │ ├── ForgotPassword.tsx │ │ │ └── ShadcnForgotPassword.tsx │ │ ├── test │ │ │ ├── page.tsx │ │ │ └── Test.tsx │ │ ├── posts │ │ │ ├── [...params] │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── roles │ │ │ │ └── [id] │ │ │ │ │ ├── page.tsx │ │ │ │ │ ├── DashboardRoleView.tsx │ │ │ │ │ └── DashboardRoleCreateModal.tsx │ │ │ ├── posts │ │ │ │ └── [id] │ │ │ │ │ └── page.tsx │ │ │ ├── users │ │ │ │ └── [id] │ │ │ │ │ └── page.tsx │ │ │ ├── categories │ │ │ │ └── [id] │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── DashboardCategoryCreateModal.tsx │ │ │ ├── Dashboard.tsx │ │ │ └── components │ │ │ │ ├── recent-sales.tsx │ │ │ │ └── overview.tsx │ │ ├── auth │ │ │ └── google │ │ │ │ └── callback │ │ │ │ ├── page.tsx │ │ │ │ └── AuthGoogle.tsx │ │ ├── logout │ │ │ └── page.tsx │ │ ├── home │ │ │ └── Home.tsx │ │ ├── layout.tsx │ │ ├── user │ │ │ └── UserContext.tsx │ │ └── globals.css │ ├── public │ │ ├── favicon.ico │ │ ├── images │ │ │ └── new.gif │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── vercel.svg │ │ └── next.svg │ ├── next.config.mjs │ ├── postcss.config.js │ ├── .eslintrc.json │ ├── lib │ │ └── utils.ts │ ├── .env.development │ ├── components │ │ ├── ui │ │ │ ├── skeleton.tsx │ │ │ ├── label.tsx │ │ │ ├── input.tsx │ │ │ ├── textarea.tsx │ │ │ ├── separator.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── tooltip.tsx │ │ │ ├── badge.tsx │ │ │ ├── avatar.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── table.tsx │ │ │ ├── dialog.tsx │ │ │ └── sheet.tsx │ │ ├── common │ │ │ ├── preline │ │ │ │ └── PrelineScript.tsx │ │ │ ├── auth │ │ │ │ └── WithAuth.tsx │ │ │ ├── preloaders │ │ │ │ └── CircularProgress.tsx │ │ │ └── alerts │ │ │ │ └── Toast.tsx │ │ ├── home │ │ │ ├── layouts │ │ │ │ └── PlainLayout.tsx │ │ │ ├── Header.tsx │ │ │ ├── menus │ │ │ │ ├── NavMenu.tsx │ │ │ │ └── MainMenu.tsx │ │ │ └── WeatherWidget.tsx │ │ └── dashboard │ │ │ └── Dialog.tsx │ ├── .env.production │ ├── .gitignore │ ├── components.json │ ├── tsconfig.json │ ├── contexts │ │ └── TrpcContext.tsx │ ├── package.json │ └── tailwind.config.ts ├── server │ ├── .prettierrc │ ├── src │ │ ├── config.ts │ │ ├── auth │ │ │ ├── auth.types.ts │ │ │ ├── auth.controller.ts │ │ │ ├── auth.module.ts │ │ │ ├── google.strategy.ts │ │ │ └── auth.service.ts │ │ ├── config │ │ │ ├── config.module.ts │ │ │ └── config.service.ts │ │ ├── app.service.ts │ │ ├── email │ │ │ ├── templates │ │ │ │ ├── welcome.ejs │ │ │ │ └── reset-password.ejs │ │ │ ├── email.service.ts │ │ │ └── email.module.ts │ │ ├── user │ │ │ ├── user.types.ts │ │ │ ├── user.module.ts │ │ │ ├── user.service.test.ts │ │ │ ├── user.dto.ts │ │ │ └── user.router.ts │ │ ├── openai │ │ │ └── openai.module.ts │ │ ├── prisma │ │ │ ├── prisma.module.ts │ │ │ └── prisma.service.ts │ │ ├── upload │ │ │ ├── upload.module.ts │ │ │ ├── upload.router.ts │ │ │ └── upload.service.ts │ │ ├── app.controller.ts │ │ ├── trpc │ │ │ ├── trpc.exception-handler.ts │ │ │ ├── trpc.module.ts │ │ │ ├── trpc.router.ts │ │ │ └── trpc.service.ts │ │ ├── post │ │ │ ├── post.module.ts │ │ │ ├── post.dto.ts │ │ │ └── post.router.ts │ │ ├── role │ │ │ ├── role.module.ts │ │ │ ├── role.dto.ts │ │ │ ├── role.router.ts │ │ │ └── role.service.ts │ │ ├── category │ │ │ ├── category.module.ts │ │ │ ├── category.dto.ts │ │ │ ├── category.router.ts │ │ │ └── category.service.ts │ │ ├── app.e2e-spec.ts │ │ ├── main.ts │ │ ├── scripts │ │ │ └── configure-cors.ts │ │ └── app.module.ts │ ├── .gitignore │ ├── tsconfig.build.json │ ├── prisma │ │ ├── migrations │ │ │ ├── migration_lock.toml │ │ │ └── 20241028065201_init │ │ │ │ └── migration.sql │ │ ├── seed.ts │ │ └── schema.prisma │ ├── nest-cli.json │ ├── tsconfig.json │ ├── jest.config.ts │ ├── .env.example │ ├── .eslintrc.js │ ├── .pnpm-debug.log │ └── package.json ├── shared │ ├── tsconfig.build.json │ ├── package.json │ ├── tsconfig.json │ ├── interfaces.ts │ └── transformer.ts └── .pnpm-debug.log ├── .vscode └── settings.json ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── deploy-with-ssh.yml ├── Dockerfile ├── pnpm-workspace.yaml ├── docker-compose.yml ├── tsconfig.json ├── LICENSE ├── docker-compose.prod.yml ├── scripts └── configure-s3-cors.js ├── package.json └── CLAUDE.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.17.0 -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /apps/web/hooks/useGoogleLogin.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/server/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } -------------------------------------------------------------------------------- /apps/server/src/config.ts: -------------------------------------------------------------------------------- 1 | import 'jest-ts-auto-mock'; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "trpc" 4 | ] 5 | } -------------------------------------------------------------------------------- /apps/web/types/react-open-weather.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-open-weather'; -------------------------------------------------------------------------------- /apps/web/types/react-weather-widget.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-weather-widget'; -------------------------------------------------------------------------------- /apps/server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .log 3 | .env 4 | .env.* 5 | !.env.example 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | .DS_Store 5 | postgres-data 6 | .pnpm-store 7 | -------------------------------------------------------------------------------- /apps/web/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaequery/ult/HEAD/apps/web/app/favicon.ico -------------------------------------------------------------------------------- /apps/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaequery/ult/HEAD/apps/web/public/favicon.ico -------------------------------------------------------------------------------- /apps/web/public/images/new.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaequery/ult/HEAD/apps/web/public/images/new.gif -------------------------------------------------------------------------------- /apps/web/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaequery/ult/HEAD/apps/web/public/favicon-16x16.png -------------------------------------------------------------------------------- /apps/web/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaequery/ult/HEAD/apps/web/public/favicon-32x32.png -------------------------------------------------------------------------------- /apps/server/src/auth/auth.types.ts: -------------------------------------------------------------------------------- 1 | export interface AuthJwt { 2 | accessToken: string; 3 | expiresIn: string; 4 | } 5 | -------------------------------------------------------------------------------- /apps/web/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /apps/server/src/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | @Module({}) 4 | export class ConfigModule {} 5 | -------------------------------------------------------------------------------- /apps/server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /apps/shared/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /apps/web/app/login/page.tsx: -------------------------------------------------------------------------------- 1 | import ShadcnLogin from "./ShadcnLogin"; 2 | 3 | export default function LoginPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/signup/page.tsx: -------------------------------------------------------------------------------- 1 | import ShadcnSignup from "./ShadcnSignup"; 2 | 3 | export default function SignupPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import ShadcnProfile from "./ShadcnProfile"; 2 | 3 | export default function ProfilePage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'tailwindcss/nesting': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /apps/server/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /apps/web/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Home from "./home/Home"; 2 | 3 | export default function RootPage() { 4 | return ( 5 | <> 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/app/verify-email/page.tsx: -------------------------------------------------------------------------------- 1 | import ShadcnVerifyEmail from "./ShadcnVerifyEmail"; 2 | 3 | export default function VerifyEmailPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "react/no-unescaped-entities": "off", 5 | "@next/next/no-img-element": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/app/forgot-password/page.tsx: -------------------------------------------------------------------------------- 1 | import ShadcnForgotPassword from "./ShadcnForgotPassword"; 2 | 3 | export default function ForgotPasswordPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /apps/server/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/server/src/email/templates/welcome.ejs: -------------------------------------------------------------------------------- 1 |

Hey <%= firstName %>,

2 |

Thanks for signing up to Hanmi News!

3 |

4 | Confirm you account 5 |

6 | -------------------------------------------------------------------------------- /apps/web/app/test/page.tsx: -------------------------------------------------------------------------------- 1 | // import Test from "@web/app/test/Test"; 2 | 3 | export default function TestPage() { 4 | return ( 5 | <> 6 | {/* */} 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /apps/server/src/user/user.types.ts: -------------------------------------------------------------------------------- 1 | import { AuthJwt } from '@server/auth/auth.types'; 2 | import { UserById } from '@shared/interfaces'; 3 | 4 | export interface UserLoginResponse { 5 | jwt: AuthJwt; 6 | user: UserById; 7 | } 8 | -------------------------------------------------------------------------------- /apps/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "build": "tsc", 6 | "dev": "tsc -w" 7 | }, 8 | "devDependencies": { 9 | "typescript": "^5.3.3" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | RUN apk add --no-cache g++ make py3-pip libc6-compat 3 | RUN npm install -g pnpm 4 | WORKDIR /app 5 | COPY . . 6 | RUN pnpm install 7 | RUN cd /app/apps/server && npx prisma generate && cd /app 8 | RUN pnpm build -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - apps/* 3 | 4 | onlyBuiltDependencies: 5 | - '@nestjs/core' 6 | - '@prisma/client' 7 | - '@prisma/engines' 8 | - '@swc/core' 9 | - bcrypt 10 | - esbuild 11 | - nice-napi 12 | - prisma 13 | -------------------------------------------------------------------------------- /apps/server/src/email/templates/reset-password.ejs: -------------------------------------------------------------------------------- 1 |

Hey <%= firstName %>,

2 |

Your account password has been reset.

3 |

Temporary password: <%= password %>

4 |

5 | Login to my account 6 |

7 |

8 | -------------------------------------------------------------------------------- /apps/server/src/openai/openai.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { OpenaiService } from './openai.service'; 3 | 4 | @Module({ 5 | imports: [], 6 | providers: [OpenaiService], 7 | exports: [OpenaiService], 8 | }) 9 | export class OpenaiModule {} 10 | -------------------------------------------------------------------------------- /apps/web/app/posts/[...params]/page.tsx: -------------------------------------------------------------------------------- 1 | import HomeLayout from "@web/components/home/layouts/HomeLayout"; 2 | import PostView from "./PostView"; 3 | 4 | export default function PostViewPage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/server/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true, 7 | "builder": "swc", 8 | "assets": ["email/templates/**/*"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/server/src/prisma/prisma.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PrismaService } from './prisma.service'; 3 | 4 | @Module({ 5 | imports: [], 6 | controllers: [], 7 | providers: [PrismaService], 8 | exports: [PrismaService], 9 | }) 10 | export class PrismaModule {} 11 | -------------------------------------------------------------------------------- /apps/web/app/posts/page.tsx: -------------------------------------------------------------------------------- 1 | import HomeLayout from "@web/components/home/layouts/HomeLayout"; 2 | import PostList from "./PostList"; 3 | 4 | export default function PostListPage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/server/src/upload/upload.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UploadService } from './upload.service'; 3 | import { UploadRouter } from './upload.router'; 4 | 5 | @Module({ 6 | providers: [UploadService, UploadRouter], 7 | exports: [UploadService, UploadRouter], 8 | }) 9 | export class UploadModule {} -------------------------------------------------------------------------------- /apps/web/.env.development: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_URL="http://localhost:3001" 2 | NEXT_PUBLIC_TRPC_URL="http://localhost:3001/trpc" 3 | NEXT_PUBLIC_TRANSLOADIT_TOKEN="0f2f33e1b37a419c3cefedf3a34363fc" 4 | NEXT_PUBLIC_TRANSLOADIT_TEMPLATE_ID="22122b17dd9449d6899b81caa5298414" 5 | NEXT_PUBLIC_OPENWEATHERMAP_API_KEY="617be6615bc5240a442bef8651742d8e" 6 | -------------------------------------------------------------------------------- /apps/web/app/dashboard/roles/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import DashboardSidebar from "../../DashboardSidebar"; 2 | import DashboardRoleView from "./DashboardRoleView"; 3 | 4 | export default function DashboardRoleViewPage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/app/auth/google/callback/page.tsx: -------------------------------------------------------------------------------- 1 | import PlainLayout from "@web/components/home/layouts/PlainLayout"; 2 | import React from "react"; 3 | import AuthGoogle from "./AuthGoogle"; 4 | 5 | export default function AuthGooglePage() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/app/dashboard/posts/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import DashboardSidebar from "../../DashboardSidebar"; 2 | import ShadcnDashboardPostView from "./ShadcnDashboardPostView"; 3 | 4 | export default function DashboardPostViewPage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/app/dashboard/users/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import DashboardSidebar from "../../DashboardSidebar"; 2 | import ShadcnDashboardUserView from "./ShadcnDashboardUserView"; 3 | 4 | export default function DashboardUserViewPage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/server/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/app/dashboard/categories/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import DashboardSidebar from "../../DashboardSidebar"; 2 | import DashboardCategoryView from "./DashboardCategoryView"; 3 | 4 | export default function DashboardCategoryViewPage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@web/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } -------------------------------------------------------------------------------- /apps/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "declaration": true, 7 | "removeComments": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "declaration": true, 7 | "removeComments": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/.env.production: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_URL="https://api.hanminews.com" 2 | NEXT_PUBLIC_TRPC_URL="https://api.hanminews.com/trpc" 3 | NEXT_PUBLIC_TRANSLOADIT_TOKEN="0f2f33e1b37a419c3cefedf3a34363fc" 4 | NEXT_PUBLIC_TRANSLOADIT_TEMPLATE_ID="22122b17dd9449d6899b81caa5298414" 5 | NEXT_PUBLIC_OPENWEATHERMAP_API_KEY="617be6615bc5240a442bef8651742d8e" 6 | NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID="GTM-KPJRGHRM" -------------------------------------------------------------------------------- /apps/server/src/config/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService as ConfigServiceInternal } from '@nestjs/config'; 3 | 4 | @Injectable() 5 | export class ConfigService { 6 | constructor(private configService: ConfigServiceInternal) {} 7 | 8 | get(key: string) { 9 | return this.configService.get(key); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/app/logout/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { useEffect } from "react"; 5 | import { useUserContext } from "../user/UserContext"; 6 | 7 | export default function LogoutPage() { 8 | const { logout } = useUserContext(); 9 | const router = useRouter(); 10 | useEffect(() => { 11 | logout(); 12 | router.push("/"); 13 | }, [logout, router]); 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/app/home/Home.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import ShadcnHomeLayout from "@web/components/home/layouts/ShadcnHomeLayout"; 4 | import ShadcnPostList from "../posts/ShadcnPostList"; 5 | 6 | export default function Home() { 7 | return ( 8 | 9 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /apps/server/src/trpc/trpc.exception-handler.ts: -------------------------------------------------------------------------------- 1 | import { Catch } from '@nestjs/common'; 2 | import { BaseExceptionFilter } from '@nestjs/core'; 3 | 4 | /** 5 | * TODO: Can't get this to work, see https://github.com/jaequery/ult-stack/issues/1 6 | */ 7 | @Catch() 8 | export class TrpcExceptionFilter extends BaseExceptionFilter { 9 | catch(e: any) { 10 | console.log('TRPC error', e.message); 11 | // super.catch(exception, host); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/server/src/prisma/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; 2 | import { PrismaClient } from '@prisma/client'; 3 | 4 | @Injectable() 5 | export class PrismaService 6 | extends PrismaClient 7 | implements OnModuleInit, OnModuleDestroy 8 | { 9 | async onModuleInit() { 10 | await this.$connect(); 11 | } 12 | 13 | async onModuleDestroy() { 14 | await this.$disconnect(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/server/src/post/post.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EmailModule } from '@server/email/email.module'; 3 | import { PrismaModule } from '@server/prisma/prisma.module'; 4 | import { AuthModule } from '../auth/auth.module'; 5 | import { PostRouter } from './post.router'; 6 | import { PostService } from './post.service'; 7 | 8 | @Module({ 9 | imports: [AuthModule, PrismaModule, EmailModule], 10 | providers: [PostService, PostRouter], 11 | exports: [PostService, PostRouter], 12 | }) 13 | export class PostModule {} 14 | -------------------------------------------------------------------------------- /apps/server/src/role/role.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EmailModule } from '@server/email/email.module'; 3 | import { PrismaModule } from '@server/prisma/prisma.module'; 4 | import { AuthModule } from '../auth/auth.module'; 5 | import { RoleRouter } from './role.router'; 6 | import { RoleService } from './role.service'; 7 | 8 | @Module({ 9 | imports: [AuthModule, PrismaModule, EmailModule], 10 | providers: [RoleService, RoleRouter], 11 | exports: [RoleService, RoleRouter], 12 | }) 13 | export class RoleModule {} 14 | -------------------------------------------------------------------------------- /apps/server/src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EmailModule } from '@server/email/email.module'; 3 | import { PrismaModule } from '@server/prisma/prisma.module'; 4 | import { AuthModule } from '../auth/auth.module'; 5 | import { UserRouter } from './user.router'; 6 | import { UserService } from './user.service'; 7 | 8 | @Module({ 9 | imports: [AuthModule, PrismaModule, AuthModule, EmailModule], 10 | providers: [UserService, UserRouter], 11 | exports: [UserService, UserRouter], 12 | }) 13 | export class UserModule {} 14 | -------------------------------------------------------------------------------- /apps/web/.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /apps/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "iconLibrary": "lucide", 14 | "aliases": { 15 | "components": "@web/components", 16 | "utils": "@web/lib/utils", 17 | "ui": "@web/components/ui", 18 | "lib": "@web/lib", 19 | "hooks": "@web/hooks" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/server/jest.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | export default { 4 | moduleFileExtensions: ['js', 'json', 'ts'], 5 | moduleNameMapper: { 6 | '^@server/(.*)$': '/$1', 7 | '^@shared/(.*)$': path.join(__dirname, '../shared/$1'), 8 | }, 9 | rootDir: 'src', 10 | testMatch: ['**/**.test.ts', '**/**.e2e-spec.ts'], 11 | transform: { 12 | '^.+\\.(t|j)s$': 'ts-jest', 13 | }, 14 | collectCoverageFrom: ['**/*.(t|j)s'], 15 | coverageDirectory: '../coverage', 16 | testEnvironment: 'node', 17 | setupFiles: ['config.ts'], 18 | }; 19 | -------------------------------------------------------------------------------- /apps/server/src/category/category.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EmailModule } from '@server/email/email.module'; 3 | import { PrismaModule } from '@server/prisma/prisma.module'; 4 | import { AuthModule } from '../auth/auth.module'; 5 | import { CategoryRouter } from './category.router'; 6 | import { CategoryService } from './category.service'; 7 | 8 | @Module({ 9 | imports: [AuthModule, PrismaModule, EmailModule], 10 | providers: [CategoryService, CategoryRouter], 11 | exports: [CategoryService, CategoryRouter], 12 | }) 13 | export class CategoryModule {} 14 | -------------------------------------------------------------------------------- /apps/server/src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { AuthService } from './auth.service'; 4 | 5 | @Controller('auth') 6 | export class AuthController { 7 | constructor(private readonly authService: AuthService) {} 8 | 9 | @Get('google') 10 | @UseGuards(AuthGuard('google')) 11 | async googleAuth() {} 12 | 13 | @Get('google/callback') 14 | @UseGuards(AuthGuard('google')) 15 | async googleAuthRedirect(@Req() req: any) { 16 | return this.authService.loginFromGoogle(req); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/hooks/use-mobile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } -------------------------------------------------------------------------------- /apps/web/app/dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Roles } from "@shared/interfaces"; 4 | import _ from "lodash"; 5 | import { useUserContext } from "../user/UserContext"; 6 | import WithAuth from "@web/components/common/auth/WithAuth"; 7 | 8 | const Dashboard = () => { 9 | const { currentUser } = useUserContext(); 10 | return ( 11 | <> 12 |
13 | Welcome to dashboard, {_.startCase(currentUser?.firstName || "")}! 14 |
15 | 16 | ); 17 | }; 18 | 19 | export default WithAuth(Dashboard, [Roles.Admin, Roles.User]); 20 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "../..", 5 | "target": "es5", 6 | "lib": ["dom", "dom.iterable", "esnext"], 7 | "allowJs": true, 8 | "strict": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/components/common/preline/PrelineScript.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { usePathname } from "next/navigation"; 4 | import { useEffect } from "react"; 5 | import { IStaticMethods } from "preline/preline"; 6 | 7 | declare global { 8 | interface Window { 9 | HSStaticMethods: IStaticMethods; 10 | } 11 | } 12 | 13 | export default function PrelineScript() { 14 | const path = usePathname(); 15 | 16 | useEffect(() => { 17 | import("preline/preline"); 18 | }, []); 19 | 20 | useEffect(() => { 21 | setTimeout(() => { 22 | if (window.HSStaticMethods) { 23 | window?.HSStaticMethods?.autoInit(); 24 | } 25 | }, 100); 26 | }, [path]); 27 | 28 | return null; 29 | } 30 | -------------------------------------------------------------------------------- /apps/server/src/trpc/trpc.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { PostModule } from '@server/post/post.module'; 3 | import { RoleModule } from '@server/role/role.module'; 4 | import { UploadModule } from '@server/upload/upload.module'; 5 | import { UserModule } from '@server/user/user.module'; 6 | import { TrpcRouter } from './trpc.router'; 7 | import { TrpcService } from './trpc.service'; 8 | import { CategoryModule } from '@server/category/category.module'; 9 | 10 | @Global() 11 | @Module({ 12 | imports: [UserModule, PostModule, RoleModule, CategoryModule, UploadModule], 13 | controllers: [], 14 | providers: [TrpcService, TrpcRouter], 15 | exports: [TrpcService], 16 | }) 17 | export class TrpcModule {} 18 | -------------------------------------------------------------------------------- /apps/server/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import request from 'supertest'; 4 | import { AppModule } from '@server/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | image: postgres:15-alpine 4 | restart: always 5 | env_file: 6 | - ./apps/server/.env.development 7 | ports: 8 | - 5432:5432 9 | volumes: 10 | - ./postgres-data:/var/lib/postgresql/data 11 | networks: 12 | - app-network 13 | localstack: 14 | image: localstack/localstack:0.12.17.3 15 | # volumes: 16 | # - ./localstack:/tmp/localstack 17 | ports: 18 | - 4566:4566 19 | environment: 20 | - SERVICES=s3 21 | - USE_SSL=false 22 | - DEFAULT_REGION=us-west-2 23 | # - DATA_DIR=/tmp/localstack/data 24 | # - DEBUG=1 25 | networks: 26 | - app-network 27 | 28 | networks: 29 | app-network: 30 | driver: bridge 31 | -------------------------------------------------------------------------------- /apps/shared/interfaces.ts: -------------------------------------------------------------------------------- 1 | // you can import this from any of the apps 2 | // eg; import { SomeInterface } from '@shared/interfaces'; 3 | import { Post, PostComment, PostReaction, Role, User, Category } from "@prisma/client"; 4 | 5 | export interface UserById extends User { 6 | roles?: Role[]; 7 | } 8 | 9 | export enum UserStatus { 10 | active, 11 | inactive, 12 | } 13 | 14 | export enum Roles { 15 | Admin = "Admin", 16 | User = "User", 17 | } 18 | 19 | export interface PostCommentWithUser extends PostComment { 20 | user: User; // Add the user field of type User 21 | } 22 | 23 | export interface PostById extends Post { 24 | user: UserById; 25 | category: Category | null; 26 | postReactions: PostReaction[]; 27 | postComments: PostCommentWithUser[]; 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "esModuleInterop": true, 5 | "experimentalDecorators": true, 6 | "emitDecoratorMetadata": true, 7 | "incremental": true, 8 | "skipLibCheck": true, 9 | "strictNullChecks": true, 10 | "noImplicitAny": true, 11 | "strictBindCallApply": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "paths": { 15 | "@server/*": ["./apps/server/src/*"], 16 | "@web/*": ["./apps/web/*"], 17 | "@shared/*": ["./apps/shared/dist/*"], 18 | "@prisma/client": ["./apps/server/node_modules/@prisma/client"] 19 | }, 20 | "plugins": [ 21 | { "transform" : "ts-auto-mock/transformer", "cacheBetweenTests": false } 22 | ] 23 | } 24 | } -------------------------------------------------------------------------------- /apps/web/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@web/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } -------------------------------------------------------------------------------- /apps/web/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@web/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } -------------------------------------------------------------------------------- /apps/server/src/role/role.dto.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const RoleCreateDto = z.object({ 4 | name: z.string(), 5 | }); 6 | export type RoleCreateDtoType = z.infer; 7 | 8 | export const RoleUpdateDto = z.object({ 9 | id: z.number(), 10 | name: z.string(), 11 | }); 12 | export type RoleUpdateDtoType = z.infer; 13 | 14 | export const RoleRemoveDto = z.object({ 15 | id: z.array(z.number()), 16 | }); 17 | export type RoleRemoveDtoType = z.infer; 18 | 19 | export const RoleFindAllDto = z.object({ 20 | page: z.number().default(1), 21 | perPage: z.number().default(10), 22 | }); 23 | export type RoleFindAllDtoType = z.infer; 24 | 25 | export const RoleFindByIdDto = z.object({ 26 | id: z.number(), 27 | }); 28 | export type RoleFindByIdDtoType = z.infer; 29 | -------------------------------------------------------------------------------- /apps/web/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@web/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |