├── apps
├── server
│ ├── .gitignore
│ ├── .prettierrc
│ ├── tsconfig.build.json
│ ├── src
│ │ ├── app.service.ts
│ │ ├── core
│ │ │ ├── auth
│ │ │ │ ├── dto
│ │ │ │ │ └── requestUserDto.ts
│ │ │ │ ├── auth.module.ts
│ │ │ │ ├── auth.controller.ts
│ │ │ │ ├── guards
│ │ │ │ │ └── auth.guard.ts
│ │ │ │ └── auth.service.ts
│ │ │ ├── drizzle
│ │ │ │ └── drizzle.module.ts
│ │ │ ├── core.module.ts
│ │ │ └── posthog
│ │ │ │ └── posthog.service.ts
│ │ ├── post
│ │ │ ├── dto
│ │ │ │ └── create-post.dto.ts
│ │ │ ├── post.module.ts
│ │ │ ├── post.service.ts
│ │ │ └── post.controller.ts
│ │ ├── app.controller.ts
│ │ ├── main.ts
│ │ ├── app.controller.spec.ts
│ │ └── app.module.ts
│ ├── nest-cli.json
│ ├── test
│ │ ├── jest-e2e.json
│ │ └── app.e2e-spec.ts
│ ├── Dockerfile.dev
│ ├── .env.example
│ ├── tsconfig.json
│ ├── .eslintrc.js
│ ├── Dockerfile
│ ├── package.json
│ └── README.md
└── web
│ ├── .dockerignore
│ ├── .eslintrc.json
│ ├── src
│ ├── app
│ │ ├── favicon.ico
│ │ ├── api
│ │ │ └── auth
│ │ │ │ └── [...nextauth]
│ │ │ │ └── route.ts
│ │ ├── dashboard
│ │ │ ├── notes
│ │ │ │ └── page.tsx
│ │ │ ├── analytics
│ │ │ │ └── page.tsx
│ │ │ ├── page.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── posts
│ │ │ │ └── page.tsx
│ │ │ └── profile
│ │ │ │ └── page.tsx
│ │ ├── blog
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ └── [slug]
│ │ │ │ └── page.tsx
│ │ ├── not-found.tsx
│ │ ├── page.tsx
│ │ ├── layout.tsx
│ │ └── globals.css
│ ├── lib
│ │ ├── utils.ts
│ │ ├── api-client.ts
│ │ └── auth.ts
│ ├── types
│ │ └── next-auth-d.ts
│ ├── components
│ │ ├── ui
│ │ │ ├── skeleton.tsx
│ │ │ ├── Subtitle.tsx
│ │ │ ├── social-button.tsx
│ │ │ ├── Title.tsx
│ │ │ ├── textarea.tsx
│ │ │ ├── label.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── input.tsx
│ │ │ ├── animated-shiny-text.tsx
│ │ │ ├── checkbox.tsx
│ │ │ ├── tooltip.tsx
│ │ │ ├── copy-button.tsx
│ │ │ ├── avatar.tsx
│ │ │ ├── border-beam.tsx
│ │ │ ├── ripple.tsx
│ │ │ ├── feature.tsx
│ │ │ ├── custom-accordion-item.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── card.tsx
│ │ │ ├── accordion.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── link-preview.tsx
│ │ │ ├── sheet.tsx
│ │ │ ├── form.tsx
│ │ │ └── hero-video-dialog.tsx
│ │ ├── blog
│ │ │ ├── image-with-blur.tsx
│ │ │ ├── blog-quote.tsx
│ │ │ ├── frame.tsx
│ │ │ ├── blog-list.tsx
│ │ │ ├── article.tsx
│ │ │ ├── article-toc.tsx
│ │ │ ├── blog-card.tsx
│ │ │ ├── dark-theme.ts
│ │ │ └── mdx-content.tsx
│ │ ├── dashboard
│ │ │ ├── sign-out-button.tsx
│ │ │ ├── mobile-navbar.tsx
│ │ │ ├── app-sidebar.tsx
│ │ │ └── user-button.tsx
│ │ ├── landing-page
│ │ │ ├── background.tsx
│ │ │ ├── footer.tsx
│ │ │ ├── guide.tsx
│ │ │ ├── features.tsx
│ │ │ ├── code-selection.tsx
│ │ │ ├── tech-stack.tsx
│ │ │ └── hero.tsx
│ │ └── posts
│ │ │ ├── post-list.tsx
│ │ │ └── create-post.tsx
│ ├── middleware.ts
│ ├── providers
│ │ ├── theme-provider.tsx
│ │ └── api-client-provider.tsx
│ ├── hooks
│ │ ├── use-mobile.tsx
│ │ └── use-window-scroll.ts
│ └── config
│ │ ├── dashboard-navitems.ts
│ │ ├── social-button-names-config.ts
│ │ ├── landing-page-nav-items.ts
│ │ └── code-selection-items-config.ts
│ ├── public
│ └── images
│ │ ├── blog
│ │ ├── mit.jpg
│ │ └── blog-1.png
│ │ └── TechIcons
│ │ ├── docker.png
│ │ ├── nestjs.png
│ │ ├── nextjs.png
│ │ ├── drizzle.png
│ │ ├── nextauth.png
│ │ ├── postgres.png
│ │ ├── posthog.png
│ │ └── ts-rest.png
│ ├── .prettierrc.json
│ ├── postcss.config.mjs
│ ├── components.json
│ ├── Dockerfile.dev
│ ├── .gitignore
│ ├── .env.example
│ ├── next.config.ts
│ ├── tsconfig.json
│ ├── content-collections.ts
│ ├── Dockerfile
│ ├── README.md
│ ├── package.json
│ ├── content
│ └── blog
│ │ └── guide-to-setup-the-kit.mdx
│ └── tailwind.config.ts
├── packages
└── shared
│ ├── .gitignore
│ ├── src
│ ├── index.ts
│ ├── db
│ │ ├── index.ts
│ │ ├── types.ts
│ │ └── schema.ts
│ ├── main.ts
│ └── routers
│ │ ├── users
│ │ └── index.ts
│ │ └── posts
│ │ └── index.ts
│ ├── drizzle.config.ts
│ ├── .env.example
│ ├── Dockerfile.dev
│ ├── tsconfig.json
│ └── package.json
├── .npmrc
├── pnpm-workspace.yaml
├── .vscode
└── settings.json
├── .env.example
├── .dockerignore
├── turbo.json
├── docker-compose.postgres.yml
├── .gitignore
├── package.json
├── LICENSE
├── docker-compose.dev.yml
├── docker-compose.prod.yml
└── README.md
/apps/server/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/shared/.gitignore:
--------------------------------------------------------------------------------
1 | /drizzle
--------------------------------------------------------------------------------
/packages/shared/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './main';
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | node-linker=hoisted
2 | package-import-method=clone-or-copy
3 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "apps/*"
3 | - "packages/*"
4 |
--------------------------------------------------------------------------------
/apps/server/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/apps/web/.dockerignore:
--------------------------------------------------------------------------------
1 | .next
2 | node_modules
3 | Dockerfile
4 | .git
5 | .dockerignore
--------------------------------------------------------------------------------
/apps/web/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals","next/typescript","prettier"]
3 | }
4 |
--------------------------------------------------------------------------------
/apps/web/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-27/god-tier-saas/HEAD/apps/web/src/app/favicon.ico
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.workingDirectories": [
3 | {
4 | "mode": "auto"
5 | }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/apps/web/public/images/blog/mit.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-27/god-tier-saas/HEAD/apps/web/public/images/blog/mit.jpg
--------------------------------------------------------------------------------
/apps/web/public/images/blog/blog-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-27/god-tier-saas/HEAD/apps/web/public/images/blog/blog-1.png
--------------------------------------------------------------------------------
/apps/web/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "singleQuote": false,
4 | "tabWidth": 2,
5 | "trailingComma": "es5"
6 | }
--------------------------------------------------------------------------------
/apps/web/public/images/TechIcons/docker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-27/god-tier-saas/HEAD/apps/web/public/images/TechIcons/docker.png
--------------------------------------------------------------------------------
/apps/web/public/images/TechIcons/nestjs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-27/god-tier-saas/HEAD/apps/web/public/images/TechIcons/nestjs.png
--------------------------------------------------------------------------------
/apps/web/public/images/TechIcons/nextjs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-27/god-tier-saas/HEAD/apps/web/public/images/TechIcons/nextjs.png
--------------------------------------------------------------------------------
/apps/server/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/apps/web/public/images/TechIcons/drizzle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-27/god-tier-saas/HEAD/apps/web/public/images/TechIcons/drizzle.png
--------------------------------------------------------------------------------
/apps/web/public/images/TechIcons/nextauth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-27/god-tier-saas/HEAD/apps/web/public/images/TechIcons/nextauth.png
--------------------------------------------------------------------------------
/apps/web/public/images/TechIcons/postgres.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-27/god-tier-saas/HEAD/apps/web/public/images/TechIcons/postgres.png
--------------------------------------------------------------------------------
/apps/web/public/images/TechIcons/posthog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-27/god-tier-saas/HEAD/apps/web/public/images/TechIcons/posthog.png
--------------------------------------------------------------------------------
/apps/web/public/images/TechIcons/ts-rest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mit-27/god-tier-saas/HEAD/apps/web/public/images/TechIcons/ts-rest.png
--------------------------------------------------------------------------------
/apps/web/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import { handlers } from "@/lib/auth"; // Referring to the auth.ts we just created
2 | export const { GET, POST } = handlers;
3 |
--------------------------------------------------------------------------------
/apps/web/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/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/core/auth/dto/requestUserDto.ts:
--------------------------------------------------------------------------------
1 | import { NewUser } from '@template/shared';
2 | import { Request } from 'express';
3 |
4 | export interface RequestUserDto extends Request {
5 | user: NewUser;
6 | }
--------------------------------------------------------------------------------
/apps/web/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Web env variables
2 |
3 | # Env
4 | ENV="dev" # dev or prod
5 |
6 | # Database
7 | POSTGRES_USER=my_user
8 | POSTGRES_DB=gts_db
9 | POSTGRES_HOST=postgres
10 | POSTGRES_PASSWORD=my_password
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 | }
8 | }
9 |
--------------------------------------------------------------------------------
/apps/web/src/app/dashboard/notes/page.tsx:
--------------------------------------------------------------------------------
1 | const NotesPage = () => {
2 | return (
3 |
4 |
Notes
5 |
6 | );
7 | };
8 |
9 | export default NotesPage;
10 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | pg_data/
2 |
3 | node_modules/
4 |
5 | apps/web/node_modules
6 | apps/web/dist
7 | apps/web/.next
8 |
9 | apps/server/node_modules
10 | apps/server/dist
11 |
12 | packages/shared/node_modules
13 | packages/shared/dist
14 |
--------------------------------------------------------------------------------
/apps/server/src/post/dto/create-post.dto.ts:
--------------------------------------------------------------------------------
1 | // import { IsString } from 'class-validator';
2 |
3 | // export class CreatePostDto {
4 |
5 |
6 | // @IsString()
7 | // title: string;
8 | // @IsString()
9 | // body: string;
10 | // }
--------------------------------------------------------------------------------
/apps/server/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/apps/web/src/app/dashboard/analytics/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | const AnalyticsPage = () => {
4 | return (
5 |
6 |
Analytics
7 |
8 | );
9 | };
10 |
11 | export default AnalyticsPage;
12 |
--------------------------------------------------------------------------------
/packages/shared/drizzle.config.ts:
--------------------------------------------------------------------------------
1 | import 'dotenv/config';
2 |
3 | import { defineConfig } from 'drizzle-kit';
4 |
5 | export default defineConfig({
6 | schema: './src/db/schema.ts',
7 | dialect: 'postgresql',
8 | dbCredentials: {
9 | url: process.env.DATABASE_URL!,
10 | }
11 |
12 | });
--------------------------------------------------------------------------------
/apps/server/src/post/post.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { PostController } from './post.controller';
3 | import { PostService } from './post.service';
4 |
5 |
6 | @Module({
7 | imports: [],
8 | controllers: [PostController],
9 | providers: [PostService],
10 | })
11 | export class PostModule { }
12 |
13 |
--------------------------------------------------------------------------------
/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/src/types/next-auth-d.ts:
--------------------------------------------------------------------------------
1 | import { JWT } from "next-auth/jwt";
2 | import { DefaultSession } from "next-auth";
3 |
4 | declare module "next-auth/jwt" {
5 | interface JWT {
6 | accessToken?: string;
7 | }
8 | }
9 |
10 | declare module "next-auth" {
11 | interface Session {
12 | accessToken?: string & DefaultSession["user"];
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | );
13 | }
14 |
15 | export { Skeleton };
16 |
--------------------------------------------------------------------------------
/apps/web/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@/lib/auth";
2 |
3 | export default auth((req) => {
4 | if (!req.auth && req.nextUrl.pathname.startsWith("/dashboard")) {
5 | const newUrl = new URL("/", req.nextUrl.origin);
6 | return Response.redirect(newUrl);
7 | }
8 | });
9 |
10 | export const config = {
11 | matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
12 | };
13 |
--------------------------------------------------------------------------------
/apps/web/src/providers/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { ThemeProvider as NextThemesProvider } from "next-themes";
5 | import { type ThemeProviderProps } from "next-themes/dist/types";
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children} ;
9 | }
10 |
--------------------------------------------------------------------------------
/apps/web/src/components/blog/image-with-blur.tsx:
--------------------------------------------------------------------------------
1 | import Image, { type ImageProps } from "next/image";
2 |
3 | export const ImageWithBlur: React.FC = (props) => {
4 | return (
5 |
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/apps/server/src/core/drizzle/drizzle.module.ts:
--------------------------------------------------------------------------------
1 | import { Global, Module } from '@nestjs/common';
2 | export const DRIZZLE = Symbol('drizzle-connection');
3 | import { db } from '@template/shared/dist/src/db'
4 |
5 | @Global()
6 | @Module({
7 | providers: [
8 | {
9 | provide: DRIZZLE,
10 | useValue: db
11 | },
12 | ],
13 | exports: [DRIZZLE],
14 | })
15 | export class DrizzleModule { }
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "ui": "tui",
4 | "tasks": {
5 | "build": {
6 | "dependsOn": ["^build"],
7 | "inputs": ["$TURBO_DEFAULT$", ".env*"],
8 | "outputs": [".next/**", "!.next/cache/**"]
9 | },
10 | "lint": {
11 | "dependsOn": ["^lint"]
12 | },
13 | "dev": {
14 | "cache": false,
15 | "persistent": true
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/apps/server/src/core/auth/auth.module.ts:
--------------------------------------------------------------------------------
1 | import { Global, Module } from '@nestjs/common';
2 | import { AuthController } from './auth.controller';
3 | import { AuthService } from './auth.service';
4 | import { AuthGuard } from './guards/auth.guard';
5 |
6 |
7 | @Global()
8 | @Module({
9 | imports: [],
10 | controllers: [AuthController],
11 | providers: [AuthService, AuthGuard],
12 | exports: [AuthGuard, AuthService]
13 | })
14 | export class AuthModule { }
15 |
16 |
--------------------------------------------------------------------------------
/apps/server/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import { AuthGuard } from './core/auth/guards/auth.guard';
4 | import { Logger } from 'nestjs-pino';
5 |
6 | async function bootstrap() {
7 | const app = await NestFactory.create(AppModule, { bufferLogs: true });
8 | app.enableCors({
9 | origin: '*',
10 | });
11 | app.useLogger(app.get(Logger));
12 | await app.listen(3000);
13 | }
14 | bootstrap();
15 |
--------------------------------------------------------------------------------
/apps/web/src/components/dashboard/sign-out-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 | import { Button } from "../ui/button";
4 | import { signOut } from "next-auth/react";
5 |
6 | const SignoutButton = () => {
7 | return (
8 | signOut({ callbackUrl: "/" })}
12 | >
13 | Sign Out
14 |
15 | );
16 | };
17 |
18 | export default SignoutButton;
19 |
--------------------------------------------------------------------------------
/apps/web/src/app/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { useSession, signOut } from "next-auth/react";
5 | import Link from "next/link";
6 |
7 | export default function DashboardPage() {
8 | // const { data: session } = useSession()
9 |
10 | return (
11 |
12 |
Dashboard
13 | Welcome, Mit
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/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": "src/app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
--------------------------------------------------------------------------------
/docker-compose.postgres.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | postgres:
5 | image: postgres:16.1
6 | environment:
7 | POSTGRES_USER: ${POSTGRES_USER}
8 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
9 | POSTGRES_DB: ${POSTGRES_DB}
10 | ports:
11 | - "5432:5432"
12 | healthcheck:
13 | test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
14 | interval: 10s
15 | timeout: 5s
16 | retries: 5
17 | volumes:
18 | - ./pg_data:/var/lib/postgresql/data
--------------------------------------------------------------------------------
/packages/shared/src/db/index.ts:
--------------------------------------------------------------------------------
1 | // import { drizzle } from 'drizzle-orm/postgres-js';
2 | // import postgres from 'postgres';
3 | import { drizzle, NodePgDatabase } from "drizzle-orm/node-postgres";
4 | import { Pool } from "pg";
5 | import * as dotenv from 'dotenv';
6 | import * as schema from './schema';
7 |
8 | dotenv.config();
9 |
10 | const pool = new Pool({
11 | connectionString: process.env.DATABASE_URL,
12 | });
13 |
14 | export const db = drizzle(pool, { schema }) as NodePgDatabase;
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/apps/server/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | # run it from the repo root directory
2 | # docker build -f ./apps/api/Dockerfile.dev .
3 | FROM node:20-alpine AS base
4 | # =======================================================================
5 | FROM base AS builder
6 | RUN apk add --no-cache libc6-compat netcat-openbsd curl
7 | RUN apk update
8 |
9 | # Set pnpm
10 | ENV PNPM_HOME="/pnpm"
11 | ENV PATH="$PNPM_HOME:$PATH"
12 | RUN corepack enable
13 |
14 | WORKDIR /app
15 | RUN pnpm add -g turbo@2.1.2
16 |
17 | # Start API
18 | CMD pnpm install && pnpm run dev:docker
--------------------------------------------------------------------------------
/apps/web/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | # run it from the repo root directory
2 | # docker build -f ./apps/web/Dockerfile.dev .
3 | FROM node:20-alpine AS base
4 | # =======================================================================
5 | FROM base AS builder
6 | RUN apk add --no-cache libc6-compat
7 | RUN apk update
8 |
9 | # Set pnpm
10 | ENV PNPM_HOME="/pnpm"
11 | ENV PATH="$PNPM_HOME:$PATH"
12 |
13 | RUN corepack enable
14 |
15 | WORKDIR /app
16 | RUN pnpm add -g turbo@2.1.2
17 |
18 | # Start the Webapp
19 | CMD cd apps/web && pnpm install && pnpm run dev
20 |
--------------------------------------------------------------------------------
/apps/web/src/components/landing-page/background.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import Particles from "@/components/ui/particles";
3 |
4 | const Background = ({ children }: { children: ReactNode }) => {
5 | return (
6 |
16 | );
17 | };
18 |
19 | export default Background;
20 |
--------------------------------------------------------------------------------
/packages/shared/src/main.ts:
--------------------------------------------------------------------------------
1 | // contract.ts
2 |
3 | import { initContract } from '@ts-rest/core';
4 | import { postContract } from './routers/posts';
5 | import { userContract } from './routers/users';
6 | import { z } from 'zod';
7 |
8 | const c = initContract();
9 |
10 | export const contract = c.router(
11 | {
12 | posts: postContract,
13 | users: userContract,
14 | },
15 | { pathPrefix: '/api', strictStatusCodes: true }
16 | );
17 |
18 |
19 | // export * from './db';
20 | export * from './db/schema';
21 | export * from './db/types';
--------------------------------------------------------------------------------
/apps/web/src/components/ui/Subtitle.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import React from "react";
3 |
4 | const Subtitle = ({
5 | children,
6 | className,
7 | }: {
8 | children: React.ReactNode;
9 | className?: string;
10 | }) => {
11 | return (
12 |
18 | {children}
19 |
20 | );
21 | };
22 |
23 | export default Subtitle;
24 |
--------------------------------------------------------------------------------
/packages/shared/.env.example:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Use this when you are using Docker
4 | # Put your database credentials here from root .env fil.
5 | # Replace POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_HOST, POSTGRES_DB with your own values
6 | # DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:5432/${POSTGRES_DB}?schema=public"
7 |
8 | # Use this when you are using pnpm
9 | # Replace POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB with your own values
10 | DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?schema=public"
--------------------------------------------------------------------------------
/packages/shared/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | # run it from the repo root directory
2 | # docker build -f ./packages/shared/Dockerfile.dev .
3 | FROM node:20-alpine AS base
4 | # =======================================================================
5 | FROM base AS builder
6 | RUN apk add --no-cache libc6-compat
7 | RUN apk update
8 |
9 | # Set pnpm
10 | ENV PNPM_HOME="/pnpm"
11 | ENV PATH="$PNPM_HOME:$PATH"
12 | RUN corepack enable
13 |
14 | WORKDIR /app
15 | RUN pnpm add -g turbo@2.1.2
16 |
17 | # Start the Webapp
18 | CMD pnpm install && cd packages/shared && pnpm run db:push && pnpm run dev
19 |
--------------------------------------------------------------------------------
/apps/web/src/components/landing-page/footer.tsx:
--------------------------------------------------------------------------------
1 | const Footer = () => {
2 | return (
3 |
16 | );
17 | };
18 |
19 | export default Footer;
20 |
--------------------------------------------------------------------------------
/packages/shared/src/db/types.ts:
--------------------------------------------------------------------------------
1 | import { InferSelectModel, InferInsertModel } from 'drizzle-orm';
2 | import { post, user } from './schema';
3 | import * as schema from './schema';
4 | import { NodePgDatabase } from 'drizzle-orm/node-postgres';
5 |
6 | // import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
7 |
8 | export type DrizzleDB = NodePgDatabase;
9 |
10 | export type User = InferSelectModel;
11 | export type NewUser = InferInsertModel;
12 |
13 | export type Post = InferSelectModel;
14 | export type NewPost = InferInsertModel;
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # Dependencies
4 | node_modules
5 | .pnp
6 | .pnp.js
7 |
8 | # Local env files
9 | .env
10 | .env.local
11 | .env.development.local
12 | .env.test.local
13 | .env.production.local
14 |
15 | # Testing
16 | coverage
17 |
18 | # Turbo
19 | .turbo
20 |
21 | # Vercel
22 | .vercel
23 | # Build Outputs
24 | .next/
25 | out/
26 | build
27 | dist
28 |
29 | #DB
30 | pg_data/
31 | .pnpm-store
32 |
33 | # Debug
34 | npm-debug.log*
35 | yarn-debug.log*
36 | yarn-error.log*
37 |
38 | # Misc
39 | .DS_Store
40 | *.pem
41 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/social-button.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import { Button } from "./button";
4 |
5 | import type { Social } from "@/config/social-button-names-config";
6 | import { Icons } from "./icons";
7 |
8 | export function SocialIconButton({ href, title, icon }: Social) {
9 | const Icon = Icons[icon];
10 |
11 | return (
12 |
13 |
14 | {title}
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/apps/server/src/core/core.module.ts:
--------------------------------------------------------------------------------
1 | import { Module, Global, Post } from '@nestjs/common';
2 | import { AuthModule } from './auth/auth.module';
3 | import { AuthService } from './auth/auth.service';
4 | import { DrizzleModule } from './drizzle/drizzle.module';
5 | import { PostHogService } from './posthog/posthog.service';
6 |
7 | @Global()
8 | @Module({
9 | imports: [
10 | AuthModule,
11 | DrizzleModule,
12 | ],
13 | controllers: [],
14 | providers: [AuthService, PostHogService],
15 | exports: [DrizzleModule, AuthModule, AuthService, PostHogService],
16 | })
17 | export class CoreSharedModule { }
--------------------------------------------------------------------------------
/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 | # content collections
13 | .content-collections
14 |
15 | # next.js
16 | /.next/
17 | /out/
18 |
19 | # production
20 | /build
21 |
22 | # misc
23 | .DS_Store
24 | *.pem
25 |
26 | # debug
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # vercel
35 | .vercel
36 |
37 | # typescript
38 | *.tsbuildinfo
39 | next-env.d.ts
40 |
--------------------------------------------------------------------------------
/apps/web/src/app/dashboard/layout.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { MobileNavBar } from "@/components/dashboard/mobile-navbar";
3 | import AppSidebar from "@/components/dashboard/app-sidebar";
4 |
5 | export default function DashboardLayout({
6 | children,
7 | }: Readonly<{
8 | children: React.ReactNode;
9 | }>) {
10 | return (
11 |
12 |
13 |
14 |
15 |
{children}
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/apps/web/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_CLIENTSIDE_SERVER_URL="http://localhost:3000"
2 |
3 | # Use this if you are running the app locally
4 | NEXT_PUBLIC_SERVERSIDE__DOCKER_SERVER_URL="http://localhost:3000"
5 |
6 | # Use this if you are running the app via docker compose
7 | # NEXT_PUBLIC_SERVERSIDE__DOCKER_SERVER_URL="http://server:3000"
8 |
9 |
10 | AUTH_SECRET="kl+NHFSbbNiS/OT0QlnYnQP8TKoyqHAwgt/Hl9G9B+0=" # Added by `npx auth`. Read more: https://cli.authjs.dev
11 | AUTH_GOOGLE_ID="your-google-client-id"
12 | AUTH_GOOGLE_SECRET="your-google-client-secret"
13 | NEXT_PUBLIC_POSTHOG_KEY=
14 | NEXT_PUBLIC_POSTHOG_HOST=
15 | ENV="dev" # dev or prod
--------------------------------------------------------------------------------
/apps/web/next.config.ts:
--------------------------------------------------------------------------------
1 | import { withContentCollections } from "@content-collections/next";
2 | import type { NextConfig } from 'next';
3 |
4 |
5 | const nextConfig : NextConfig = {
6 | images: {
7 | domains: [
8 | "api.microlink.io", // Microlink Image Preview
9 |
10 | ],
11 | remotePatterns: [
12 | {
13 | protocol: 'https',
14 | hostname: 'lh3.googleusercontent.com',
15 | port: '',
16 | pathname: '/a/**',
17 | },
18 |
19 | ]
20 | },
21 | };
22 |
23 | export default withContentCollections(nextConfig);
24 |
--------------------------------------------------------------------------------
/apps/web/src/app/blog/layout.tsx:
--------------------------------------------------------------------------------
1 | import Background from "@/components/landing-page/background";
2 | import Navbar from "@/components/landing-page/navbar";
3 | import * as React from "react";
4 |
5 | export default function DashboardLayout({
6 | children,
7 | }: Readonly<{
8 | children: React.ReactNode;
9 | }>) {
10 | // const [open, setOpen] = React.useState(false);
11 | // const {data : currentSession} = useSession();
12 |
13 | return (
14 |
15 |
16 |
17 |
{children}
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/apps/server/.env.example:
--------------------------------------------------------------------------------
1 | # Use this when you are using Docker
2 | # Put your database credentials here from root .env fil.
3 | # Replace POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_HOST, POSTGRES_DB with your own values
4 | # DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:5432/${POSTGRES_DB}?schema=public"
5 |
6 | # Use this when you are using pnpm
7 | # Replace POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB with your own values
8 | DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?schema=public"
9 |
10 | GOOGLE_CLIENT_ID="your-google-client-id"
11 | POSTHOG_KEY=
12 | POSTHOG_HOST=
--------------------------------------------------------------------------------
/packages/shared/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "commonjs",
5 | "forceConsistentCasingInFileNames": true,
6 | "strict": true,
7 | "skipLibCheck": true,
8 | "isolatedModules": true,
9 | "preserveWatchOutput": true,
10 | "target": "ES6",
11 | // "rootDir": "./",
12 | // "moduleResolution": "node",
13 | "outDir": "dist",
14 | // "types": ["node"],
15 | "declaration": true,
16 | },
17 | "exclude": ["node_modules", "./dist/**/*"],
18 | "include": ["src/**/*","drizzle.config.ts"]
19 |
20 | }
--------------------------------------------------------------------------------
/apps/web/src/components/ui/Title.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | const Title = ({
4 | children,
5 | className,
6 | }: {
7 | children: React.ReactNode;
8 | className?: string;
9 | }) => {
10 | return (
11 |
20 | {children}
21 |
22 | );
23 | };
24 |
25 | export default Title;
26 |
--------------------------------------------------------------------------------
/apps/web/src/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(
7 | undefined
8 | );
9 |
10 | React.useEffect(() => {
11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
12 | const onChange = () => {
13 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
14 | };
15 | mql.addEventListener("change", onChange);
16 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
17 | return () => mql.removeEventListener("change", onChange);
18 | }, []);
19 |
20 | return !!isMobile;
21 | }
22 |
--------------------------------------------------------------------------------
/apps/web/src/app/dashboard/posts/page.tsx:
--------------------------------------------------------------------------------
1 | import CreatePost from "@/components/posts/create-post";
2 | import Posts from "@/components/posts/post-list";
3 | import { Skeleton } from "@/components/ui/skeleton";
4 | import { Suspense } from "react";
5 |
6 | const PostPage = () => {
7 | return (
8 |
9 |
10 |
Posts
11 |
12 |
13 |
}>
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default PostPage;
21 |
--------------------------------------------------------------------------------
/apps/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "strict": true,
9 | "allowSyntheticDefaultImports": true,
10 | "target": "ES2021",
11 | "sourceMap": true,
12 | "outDir": "./dist",
13 | "baseUrl": "./",
14 | "paths": {
15 | "@/*": ["./src/*"]
16 | },
17 | "incremental": true,
18 | "skipLibCheck": true,
19 | "strictNullChecks": true,
20 | "noImplicitAny": true,
21 | "strictBindCallApply": true,
22 | "forceConsistentCasingInFileNames": true,
23 | "noFallthroughCasesInSwitch": true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/apps/server/src/app.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 |
5 | describe('AppController', () => {
6 | let appController: AppController;
7 |
8 | beforeEach(async () => {
9 | const app: TestingModule = await Test.createTestingModule({
10 | controllers: [AppController],
11 | providers: [AppService],
12 | }).compile();
13 |
14 | appController = app.get(AppController);
15 | });
16 |
17 | describe('root', () => {
18 | it('should return "Hello World!"', () => {
19 | expect(appController.getHello()).toBe('Hello World!');
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/apps/web/src/config/dashboard-navitems.ts:
--------------------------------------------------------------------------------
1 | import {
2 | NotebookText,
3 | type LucideIcon,
4 | ChartPie,
5 | StickyNote,
6 | } from "lucide-react";
7 |
8 | export type NavItem = {
9 | disabled?: boolean;
10 | tooltip?: string;
11 | icon: LucideIcon;
12 | href: string;
13 | external?: boolean;
14 | label: string;
15 | tag?: React.ReactNode;
16 | };
17 |
18 | export const dashboardNavitems: NavItem[] = [
19 | {
20 | icon: NotebookText,
21 | href: "/dashboard/posts",
22 | label: "Posts",
23 | },
24 | {
25 | icon: ChartPie,
26 | href: "/dashboard/analytics",
27 | label: "Analytics",
28 | },
29 | {
30 | icon: StickyNote,
31 | label: "Notes",
32 | href: "/dashboard/notes",
33 | },
34 | ];
35 |
--------------------------------------------------------------------------------
/apps/server/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { INestApplication } from '@nestjs/common';
3 | import * as request from 'supertest';
4 | import { AppModule } from '../src/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 |
--------------------------------------------------------------------------------
/apps/web/src/config/social-button-names-config.ts:
--------------------------------------------------------------------------------
1 | import type { ValidIcon } from "@/components/ui/icons";
2 |
3 | export type Social = {
4 | title: string;
5 | href: string;
6 | icon: ValidIcon;
7 | };
8 |
9 | export const socialsConfig: Social[] = [
10 | {
11 | title: "Discord",
12 | href: "/discord",
13 | icon: "discord",
14 | },
15 | {
16 | title: "GitHub",
17 | href: "/github",
18 | icon: "github",
19 | },
20 | {
21 | title: "Twitter",
22 | href: "/twitter",
23 | icon: "twitter",
24 | },
25 | {
26 | title: "LinkedIn",
27 | href: "/linkedin",
28 | icon: "linkedin",
29 | },
30 | // {
31 | // title: "YouTube",
32 | // href: "/youtube",
33 | // icon: "youtube",
34 | // },
35 | ];
36 |
--------------------------------------------------------------------------------
/apps/server/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | tsconfigRootDir: __dirname,
6 | sourceType: 'module',
7 | },
8 | plugins: ['@typescript-eslint/eslint-plugin'],
9 | extends: [
10 | 'plugin:@typescript-eslint/recommended',
11 | 'plugin:prettier/recommended',
12 | ],
13 | root: true,
14 | env: {
15 | node: true,
16 | jest: true,
17 | },
18 | ignorePatterns: ['.eslintrc.js'],
19 | rules: {
20 | '@typescript-eslint/interface-name-prefix': 'off',
21 | '@typescript-eslint/explicit-function-return-type': 'off',
22 | '@typescript-eslint/explicit-module-boundary-types': 'off',
23 | '@typescript-eslint/no-explicit-any': 'off',
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/apps/web/src/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | import { Button, buttonVariants } from "@/components/ui/button";
2 | import Title from "@/components/ui/Title";
3 | import { cn } from "@/lib/utils";
4 | import Link from "next/link";
5 | import React from "react";
6 |
7 | const NotFound = () => {
8 | return (
9 |
10 |
11 |
404 Page Not Found
12 |
13 |
22 | Go to Home
23 |
24 |
25 | );
26 | };
27 |
28 | export default NotFound;
29 |
--------------------------------------------------------------------------------
/apps/web/src/lib/api-client.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { contract } from "@template/shared";
3 | import { initTsrReactQuery } from "@ts-rest/react-query/v5";
4 | import Cookies from "js-cookie";
5 | // import { auth } from './auth';
6 |
7 | export const api = initTsrReactQuery(contract, {
8 | baseUrl: process.env.NEXT_PUBLIC_CLIENTSIDE_SERVER_URL!,
9 | baseHeaders: {
10 | Authorization: () => `Bearer ${Cookies.get("access_token")}`,
11 | // 'x-app-source': 'ts-rest',
12 | // 'x-access-token': () => getAccessToken(),
13 | },
14 | // api: async (args) => {
15 | // const {data : authSession} = useSession();
16 | // args.headers = {
17 | // ...args.headers,
18 | // Authorization: `Bearer ${authSession?.accessToken}`,
19 | // };
20 | // return tsRestFetchApi(args);
21 | // }
22 | });
23 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | );
20 | }
21 | );
22 | Textarea.displayName = "Textarea";
23 |
24 | export { Textarea };
25 |
--------------------------------------------------------------------------------
/apps/web/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Background from "@/components/landing-page/background";
2 | import Features from "@/components/landing-page/features";
3 | import Footer from "@/components/landing-page/footer";
4 | import Guide from "@/components/landing-page/guide";
5 | import Hero from "@/components/landing-page/hero";
6 | import Navbar from "@/components/landing-page/navbar";
7 | import TechStack from "@/components/landing-page/tech-stack";
8 |
9 | export default async function Home() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/apps/web/src/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 "@/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 };
27 |
--------------------------------------------------------------------------------
/packages/shared/src/db/schema.ts:
--------------------------------------------------------------------------------
1 | import { pgTable, serial, text, uuid, varchar, boolean, timestamp } from "drizzle-orm/pg-core";
2 |
3 | export const post = pgTable("posts", {
4 | id: uuid("id").primaryKey().defaultRandom(),
5 | title: varchar("title").notNull(),
6 | createdAt: timestamp("created_at", { mode: "string" }).notNull().defaultNow(),
7 | body: text("body").notNull(),
8 | });
9 |
10 | export const user = pgTable("users", {
11 | id: varchar("id").primaryKey(),
12 | name: varchar("name", { length: 255 }),
13 | email: varchar("email", { length: 255 }).notNull().unique(),
14 | emailVerified: boolean("email_verfied").default(false),
15 | image: text("image"),
16 | createdAt: timestamp("created_at", { mode: "string" }).notNull().defaultNow(),
17 | updatedAt: timestamp("updated_at", { mode: "string" }).notNull().defaultNow()
18 | })
19 |
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "god-tier-saas-template",
3 | "private": true,
4 | "scripts": {
5 | "build": "turbo build",
6 | "dev": "concurrently \"turbo run dev --filter=@template/web\" \"turbo run dev --filter=@template/server\" \"turbo run dev --filter=@template/shared\"",
7 | "dev:db": "turbo run dev --filter=@template/shared",
8 | "build:shared": "turbo run build --filter=@template/shared",
9 | "dev:docker": "concurrently \"turbo run dev --filter=@template/server\" \"turbo run dev --filter=@template/shared\"",
10 | "lint": "turbo lint",
11 | "format": "prettier --write \"**/*.{ts,tsx,md}\""
12 | },
13 | "devDependencies": {
14 | "concurrently": "^9.0.1",
15 | "prettier": "^3.2.5",
16 | "turbo": "^2.1.2",
17 | "typescript": "^5.4.5"
18 | },
19 | "packageManager": "pnpm@8.15.6",
20 | "engines": {
21 | "node": ">=18"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | );
29 | Separator.displayName = SeparatorPrimitive.Root.displayName;
30 |
31 | export { Separator };
32 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | }
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "noEmit": true,
12 | "esModuleInterop": true,
13 | "module": "esnext",
14 | "moduleResolution": "bundler",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "incremental": true,
19 | "plugins": [
20 | {
21 | "name": "next"
22 | }
23 | ],
24 | "paths": {
25 | "@/*": [
26 | "./src/*"
27 | ],
28 | "content-collections": [
29 | "./.content-collections/generated"
30 | ]
31 | },
32 | "target": "ES2017"
33 | },
34 | "include": [
35 | "next-env.d.ts",
36 | "**/*.ts",
37 | "**/*.tsx",
38 | ".next/types/**/*.ts"
39 | ],
40 | "exclude": [
41 | "node_modules"
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/apps/web/src/components/landing-page/guide.tsx:
--------------------------------------------------------------------------------
1 | import Subtitle from "@/components/ui/Subtitle";
2 | import Title from "@/components/ui/Title";
3 |
4 | import CodeSelection from "./code-selection";
5 |
6 | const Guide = () => {
7 | return (
8 |
9 |
10 |
11 |
12 | End to end type safety to create and use the APIs
13 |
14 |
15 |
16 | Define your APIs with ts-rest Router, implement them in your NestJS
17 | controllers, and use them in NextJS using type-safe TanStack queries.
18 |
19 |
20 |
21 |
22 |
23 | );
24 | };
25 |
26 | export default Guide;
27 |
--------------------------------------------------------------------------------
/packages/shared/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@template/shared",
3 | "version": "0.0.1",
4 | "files": [
5 | "dist/**/*"
6 | ],
7 | "dependencies": {
8 | "@ts-rest/core": "^3.51.0",
9 | "dotenv": "^16.4.5",
10 | "drizzle-orm": "^0.33.0",
11 | "drizzle-zod": "^0.5.1",
12 | "pg": "^8.13.0",
13 | "postgres": "^3.4.4",
14 | "typescript": "^5",
15 | "zod": "^3.23.8"
16 | },
17 | "description": "Ts-rest shared contract ",
18 | "main": "dist/src/index.js",
19 | "scripts": {
20 | "build": "tsc",
21 | "db:generate": "npx drizzle-kit generate",
22 | "db:migrate": "npx drizzle-kit migrate",
23 | "db:push" : "pnpm db:generate && pnpm db:migrate",
24 | "dev": "pnpm db:push && tsc -w"
25 | },
26 | "devDependencies": {
27 | "@types/node": "^22.6.1",
28 | "@types/pg": "^8.11.10",
29 | "drizzle-kit": "^0.24.2"
30 | }
31 | }
--------------------------------------------------------------------------------
/apps/server/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 | import { DrizzleModule } from './core/drizzle/drizzle.module';
5 | import { ConfigModule } from '@nestjs/config';
6 | import { PostModule } from './post/post.module';
7 | import { AuthModule } from './core/auth/auth.module';
8 | import { CoreSharedModule } from './core/core.module';
9 | import { LoggerModule } from 'nestjs-pino';
10 |
11 | @Module({
12 | imports: [
13 | LoggerModule.forRoot({
14 | pinoHttp: {
15 | transport: {
16 | target: 'pino-pretty',
17 | options: {
18 | singleLine: true,
19 | }
20 | },
21 | },
22 | }),
23 | CoreSharedModule,
24 | PostModule,
25 | ConfigModule.forRoot({ isGlobal: true })
26 | ],
27 | controllers: [AppController],
28 | providers: [AppService],
29 | })
30 | export class AppModule { }
31 |
--------------------------------------------------------------------------------
/apps/web/src/components/blog/blog-quote.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { Minus } from "lucide-react";
3 | export type BlogQuoteProps = {
4 | children?: React.ReactNode;
5 | className?: string;
6 | author?: string;
7 | };
8 |
9 | export function BlogQuote({ children, className, ...props }: BlogQuoteProps) {
10 | return (
11 |
17 |
18 |
19 | {children}
20 |
21 | {props.author && (
22 |
23 | {" "}
24 |
{" "}
25 |
{props.author}
26 |
27 | )}
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/apps/web/src/components/blog/frame.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | export function Frame({
4 | as: Component = "div",
5 | size,
6 | className,
7 | children,
8 | }: {
9 | as?: any;
10 | size: "sm" | "md" | "lg";
11 | className?: string;
12 | children: React.ReactNode;
13 | }) {
14 | return (
15 |
16 |
27 |
34 | {children}
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Mit Suthar
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/animated-shiny-text.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties, FC, ReactNode } from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | interface AnimatedShinyTextProps {
6 | children: ReactNode;
7 | className?: string;
8 | shimmerWidth?: number;
9 | }
10 |
11 | const AnimatedShinyText: FC = ({
12 | children,
13 | className,
14 | shimmerWidth = 100,
15 | }) => {
16 | return (
17 |
35 | {children}
36 |
37 | );
38 | };
39 |
40 | export default AnimatedShinyText;
41 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
5 | import { CheckIcon } from "@radix-ui/react-icons";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ));
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
29 |
30 | export { Checkbox };
31 |
--------------------------------------------------------------------------------
/apps/web/content-collections.ts:
--------------------------------------------------------------------------------
1 | import { defineCollection, defineConfig } from "@content-collections/core";
2 | import { compileMDX } from "@content-collections/mdx";
3 | import { remarkGfm, remarkHeading, remarkStructure } from "fumadocs-core/mdx-plugins";
4 |
5 |
6 | const blogs = defineCollection({
7 | name: "posts",
8 | directory: "content/blog",
9 | include: "*.mdx",
10 | schema: (z) => ({
11 | title: z.string(),
12 | description: z.string(),
13 | author_name: z.string(),
14 | author_image: z.string(),
15 | type: z.string(),
16 | date: z.string(),
17 | // tags: z.array(z.string()),
18 | image: z.string().optional(),
19 | }),
20 | transform: async (document, context) => {
21 | const mdx = await compileMDX(context, document, {
22 | remarkPlugins: [remarkGfm, remarkHeading, remarkStructure],
23 | }
24 | );
25 |
26 | return {
27 | ...document,
28 | mdx,
29 | slug: document._meta.path,
30 | url: `/blog/${document._meta.path}`,
31 | };
32 | },
33 | });
34 |
35 | export default defineConfig({
36 | collections: [blogs],
37 | });
--------------------------------------------------------------------------------
/apps/web/src/components/blog/blog-list.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import type React from "react";
3 |
4 | export type BlogListProps = {
5 | children?: React.ReactNode;
6 | className?: string;
7 | };
8 |
9 | export function BlogList({ children, className }: BlogListProps) {
10 | // console.log("BlogList children", children);
11 | return (
12 |
20 | );
21 | }
22 | export function BlogListNumbered({ children, className }: BlogListProps) {
23 | // console.log("BlogList children", children);
24 | return (
25 |
31 | {children}
32 |
33 | );
34 | }
35 | export function BlogListItem({ children, className }: BlogListProps) {
36 | return (
37 |
43 | {children}
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/apps/server/src/core/auth/auth.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, Post, Req, HttpCode, HttpStatus, Body, UseGuards } from '@nestjs/common';
2 | import { AuthGuard } from './guards/auth.guard';
3 | import { RequestUserDto } from './dto/requestUserDto';
4 | import { AuthService } from './auth.service';
5 | import { TsRestHandler, tsRestHandler } from '@ts-rest/nest';
6 | import { contract } from '@template/shared';
7 |
8 | @Controller()
9 | export class AuthController {
10 |
11 | constructor(private readonly authService: AuthService) { }
12 |
13 | @UseGuards(AuthGuard)
14 | @TsRestHandler(contract.users.login)
15 | async login(@Req() req: RequestUserDto) {
16 | return tsRestHandler(contract.users.login, async () => {
17 | const currentUser = await this.authService.login(req.user);
18 | if (!currentUser) {
19 | return {
20 | status: 400,
21 | body: { message: "Failed to login user" }
22 | }
23 | }
24 | return {
25 | status: 201,
26 | body: currentUser
27 | };
28 |
29 |
30 |
31 | });
32 | // return this.authService.login(req.user);
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/apps/web/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = TooltipPrimitive.Root;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
31 |
--------------------------------------------------------------------------------
/apps/web/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter as FontSans } from "next/font/google";
3 | import { cn } from "@/lib/utils";
4 | import "./globals.css";
5 | import { Providers } from "@/providers/api-client-provider";
6 | import { SessionProvider } from "next-auth/react";
7 |
8 | import { Toaster } from "sonner";
9 | import { ThemeProvider } from "@/providers/theme-provider";
10 | import { auth } from "@/lib/auth";
11 |
12 | const fontSans = FontSans({
13 | subsets: ["latin"],
14 | variable: "--font-sans",
15 | });
16 |
17 | export const metadata: Metadata = {
18 | title: "Create Next App",
19 | description: "Generated by create next app",
20 | };
21 |
22 | export default async function RootLayout({
23 | children,
24 | }: Readonly<{
25 | children: React.ReactNode;
26 | }>) {
27 | const session = await auth();
28 | return (
29 |
30 |
31 |
32 |
33 | {children}
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/apps/web/src/hooks/use-window-scroll.ts:
--------------------------------------------------------------------------------
1 | // Props: https://github.com/uidotdev/usehooks/blob/90fbbb4cc085e74e50c36a62a5759a40c62bb98e/index.js#L1310
2 |
3 | import * as React from "react";
4 |
5 | export function useWindowScroll() {
6 | const [state, setState] = React.useState<{
7 | x: number | null;
8 | y: number | null;
9 | }>({
10 | x: null,
11 | y: null,
12 | });
13 |
14 | // biome-ignore lint/suspicious/noExplicitAny:
15 | const scrollTo = React.useCallback((...args: any[]) => {
16 | if (typeof args[0] === "object") {
17 | window.scrollTo(args[0]);
18 | } else if (typeof args[0] === "number" && typeof args[1] === "number") {
19 | window.scrollTo(args[0], args[1]);
20 | } else {
21 | throw new Error(
22 | "Invalid arguments passed to scrollTo. See here for more info. https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo"
23 | );
24 | }
25 | }, []);
26 |
27 | React.useLayoutEffect(() => {
28 | const handleScroll = () => {
29 | setState({ x: window.scrollX, y: window.scrollY });
30 | };
31 |
32 | handleScroll();
33 | window.addEventListener("scroll", handleScroll);
34 |
35 | return () => {
36 | window.removeEventListener("scroll", handleScroll);
37 | };
38 | }, []);
39 |
40 | return [state, scrollTo] as const;
41 | }
42 |
--------------------------------------------------------------------------------
/apps/server/src/core/auth/guards/auth.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, } from '@nestjs/common';
2 | import { AuthService } from '../auth.service';
3 |
4 | @Injectable()
5 | export class AuthGuard implements CanActivate {
6 |
7 | constructor(private readonly authService: AuthService) { }
8 |
9 |
10 | async canActivate(
11 | context: ExecutionContext,
12 | ): Promise {
13 |
14 | console.log('AuthGuard is running');
15 |
16 | const request = context.switchToHttp().getRequest();
17 | const authorization = request.headers.authorization;
18 |
19 | if (!authorization) {
20 | throw new UnauthorizedException();
21 | }
22 | console.log('Authorization:', authorization);
23 |
24 | const [scheme, token] = authorization.split(' ');
25 |
26 | if (scheme !== 'Bearer') {
27 | throw new UnauthorizedException();
28 | }
29 |
30 | try {
31 | const user = await this.authService.verifyToken(token);
32 | request.user = user;
33 | // request.user = {
34 | // id: "1",
35 | // email: "test@test.com",
36 | // name: "Test User",
37 | // }
38 |
39 | return true;
40 |
41 | }
42 | catch (error) {
43 | throw new UnauthorizedException();
44 | }
45 |
46 | }
47 | }
--------------------------------------------------------------------------------
/apps/server/Dockerfile:
--------------------------------------------------------------------------------
1 | # run directly from the repo root directory
2 | # docker build -f ./apps/api/Dockerfile .
3 | FROM node:20-alpine AS base
4 | # =======================================================================
5 | FROM base AS builder
6 | RUN apk add --no-cache libc6-compat
7 | RUN apk update
8 |
9 | # Set pnpm
10 | ENV PNPM_HOME="/pnpm"
11 | ENV PATH="$PNPM_HOME:$PATH"
12 | RUN corepack enable
13 |
14 | WORKDIR /app
15 | RUN pnpm add -g turbo@2.1.2
16 | COPY . .
17 | RUN turbo prune @template/server --docker
18 |
19 | # =======================================================================
20 | # Add lockfile and package.json's of isolated subworkspace
21 | FROM base AS installer
22 | RUN apk add --no-cache libc6-compat
23 | RUN apk update
24 | # Set pnpm
25 | ENV PNPM_HOME="/pnpm"
26 | ENV PATH="$PNPM_HOME:$PATH"
27 | RUN corepack enable
28 |
29 | WORKDIR /app
30 |
31 | COPY .gitignore .gitignore
32 | COPY --from=builder /app/out/json/ .
33 | COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
34 | RUN pnpm install
35 |
36 | # Build the project
37 | COPY --from=builder /app/out/full/ .
38 | #RUN pnpm turbo run build --filter=api...
39 | #avoid pulling from db
40 | RUN pnpm run build
41 |
42 | # ========================================================================
43 | FROM base AS runner
44 | RUN apk add --no-cache libc6-compat netcat-openbsd curl
45 |
46 | WORKDIR /app
47 |
48 | COPY --from=installer ./app .
49 | WORKDIR /app/apps/server
50 |
51 | EXPOSE 3000
52 | CMD node dist/src/main.js
53 |
--------------------------------------------------------------------------------
/apps/web/Dockerfile:
--------------------------------------------------------------------------------
1 | # run directly from the repo root directory
2 | # docker build -f ./apps/webapp/Dockerfile .
3 | FROM node:20-alpine AS base
4 | # =======================================================================
5 | # Turbo: Prepare a standalone workspace for docker
6 | FROM base AS builder
7 | RUN apk add --no-cache libc6-compat
8 | RUN apk update
9 |
10 | # Set pnpm
11 | ENV PNPM_HOME="/pnpm"
12 | ENV PATH="$PNPM_HOME:$PATH"
13 | RUN corepack enable
14 |
15 | WORKDIR /app
16 | RUN pnpm add -g turbo@2.1.2
17 | COPY . .
18 | RUN turbo prune @template/web --docker
19 |
20 | #check content
21 | RUN ls -la ./out/full/apps/web
22 |
23 | # =======================================================================
24 | # Install Deps and build project using PNPM
25 | FROM base AS installer
26 | RUN apk add --no-cache libc6-compat
27 | RUN apk update
28 | # Set pnpm
29 | ENV PNPM_HOME="/pnpm"
30 | ENV PATH="$PNPM_HOME:$PATH"
31 |
32 | RUN corepack enable
33 |
34 | WORKDIR /app
35 |
36 | RUN ls -la
37 |
38 | # First install the dependencies (as they change less often)
39 | COPY .gitignore .gitignore
40 | COPY --from=builder /app/out/json/ .
41 |
42 | # 🔴🔴🔴 possible bug due to missing dependencies here, when using "standalone mode"
43 | COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
44 |
45 | # install dependencies
46 | RUN pnpm install --shamefully-hoist
47 |
48 | # Build the project
49 | COPY --from=builder ./app/out/full/ .
50 | RUN pnpm run build
51 |
52 | CMD cd /app/apps/web/ && pnpm run start
53 |
54 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/copy-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import { Copy, CopyCheck } from "lucide-react";
5 | import * as React from "react";
6 |
7 | interface CopyButtonProps extends React.HTMLAttributes {
8 | value: string;
9 | src?: string;
10 | }
11 |
12 | async function copyToClipboardWithMeta(
13 | value: string,
14 | _meta?: Record
15 | ) {
16 | navigator.clipboard.writeText(value);
17 | }
18 |
19 | export function CopyButton({
20 | value,
21 | className,
22 | src,
23 | children,
24 | ...props
25 | }: CopyButtonProps) {
26 | const [hasCopied, setHasCopied] = React.useState(false);
27 |
28 | React.useEffect(() => {
29 | setTimeout(() => {
30 | setHasCopied(false);
31 | }, 2000);
32 | }, [hasCopied]);
33 |
34 | return (
35 | {
43 | copyToClipboardWithMeta(value, {
44 | component: src,
45 | });
46 | setHasCopied(true);
47 | }}
48 | {...props}
49 | >
50 | Copy
51 | {hasCopied ? (
52 |
53 | ) : (
54 |
55 | )}
56 | {children}
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/apps/web/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
37 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ));
21 | Avatar.displayName = AvatarPrimitive.Root.displayName;
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ));
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ));
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
49 |
50 | export { Avatar, AvatarImage, AvatarFallback };
51 |
--------------------------------------------------------------------------------
/docker-compose.dev.yml:
--------------------------------------------------------------------------------
1 | name: god-tier-saas-template
2 | version: '3.8'
3 |
4 | services:
5 | postgres:
6 | image: postgres:16.1
7 | environment:
8 | POSTGRES_USER: ${POSTGRES_USER}
9 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
10 | POSTGRES_DB: ${POSTGRES_DB}
11 | ports:
12 | - "5432:5432"
13 | healthcheck:
14 | test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
15 | interval: 10s
16 | timeout: 5s
17 | retries: 5
18 | volumes:
19 | - ./pg_data:/var/lib/postgresql/data
20 | networks:
21 | - backend
22 |
23 | server:
24 | build:
25 | context: ./
26 | dockerfile: ./apps/server/Dockerfile.dev
27 | ports:
28 | - 3000:3000
29 | environment:
30 | DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:5432/${POSTGRES_DB}?schema=public
31 | depends_on:
32 | postgres:
33 | condition: service_healthy
34 | restart: unless-stopped
35 | volumes:
36 | - .:/app
37 | healthcheck:
38 | test: ["CMD", "curl", "-f", "http://localhost:3000/"]
39 | interval: 10s
40 | timeout: 5s
41 | retries: 1000 # Try launching the API service as long as possible. Required for other services to start
42 | networks:
43 | - backend
44 |
45 | web:
46 | build:
47 | context: ./
48 | dockerfile: ./apps/web/Dockerfile.dev
49 | ports:
50 | - 8090:8090
51 | restart: unless-stopped
52 | networks:
53 | - backend
54 | volumes:
55 | - .:/app
56 | depends_on:
57 | server:
58 | condition: service_healthy
59 |
60 |
61 | networks:
62 | backend:
63 |
--------------------------------------------------------------------------------
/apps/web/src/providers/api-client-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3 | import { api } from "../lib/api-client";
4 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
5 | import posthog from "posthog-js";
6 | import { PostHogProvider, usePostHog } from "posthog-js/react";
7 | import { usePathname, useSearchParams } from "next/navigation";
8 | import { useEffect } from "react";
9 |
10 | const queryClient = new QueryClient();
11 |
12 | if (
13 | typeof window !== "undefined" &&
14 | process.env.NEXT_PUBLIC_POSTHOG_KEY !== "" &&
15 | process.env.NEXT_PUBLIC_POSTHOG_HOST !== ""
16 | ) {
17 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
18 | api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
19 | });
20 | }
21 |
22 | export function Providers({ children }: { children: React.ReactNode }) {
23 | const pathname = usePathname();
24 | const searchParams = useSearchParams();
25 | const posthog = usePostHog();
26 | useEffect(() => {
27 | // Track pageviews
28 | if (pathname && posthog) {
29 | let url = window.origin + pathname;
30 | if (searchParams.toString()) {
31 | url = url + `?${searchParams.toString()}`;
32 | }
33 | posthog.capture("$pageview", {
34 | $current_url: url,
35 | });
36 | }
37 | }, [pathname, searchParams, posthog]);
38 |
39 | return (
40 |
41 |
42 | {children}
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/apps/web/src/lib/auth.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 | import Google from "next-auth/providers/google";
3 | import { cookies } from "next/headers";
4 | import axios from "axios";
5 |
6 | export const { handlers, signIn, signOut, auth } = NextAuth({
7 | providers: [Google],
8 | callbacks: {
9 | async jwt({ token, account }) {
10 | if (account?.access_token) {
11 | token.accessToken = account.id_token;
12 | }
13 | return token;
14 | },
15 | async session({ session, token }) {
16 | return {
17 | ...session,
18 | accessToken: token?.accessToken,
19 | };
20 | },
21 | async signIn({ account, user, profile }) {
22 | // try {
23 | const token = account?.id_token;
24 |
25 | if (!token) {
26 | console.error("No access token available");
27 | return false;
28 | }
29 |
30 | const cookiesStore = await cookies();
31 |
32 | cookiesStore.set("access_token", token);
33 |
34 | try {
35 | const response = await axios.get(
36 | `${process.env.NEXT_PUBLIC_SERVERSIDE__DOCKER_SERVER_URL}/api/auth/login`,
37 | {
38 | headers: {
39 | Authorization: `Bearer ${token}`,
40 | },
41 | }
42 | );
43 | } catch (error) {
44 | console.log(error);
45 | return false;
46 | }
47 |
48 | return true;
49 | },
50 | },
51 | events: {
52 | signOut: async () => {
53 | const cookiesStore = await cookies();
54 | cookiesStore.set("access_token", "");
55 | },
56 | signIn: async (user) => {
57 | console.log("User is:", user);
58 | // return true;
59 | },
60 | },
61 | });
62 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/border-beam.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { cn } from "@/lib/utils";
3 |
4 | interface BorderBeamProps {
5 | className?: string;
6 | size?: number;
7 | duration?: number;
8 | borderWidth?: number;
9 | anchor?: number;
10 | colorFrom?: string;
11 | colorTo?: string;
12 | delay?: number;
13 | }
14 |
15 | export const BorderBeam = ({
16 | className,
17 | size = 200,
18 | duration = 15,
19 | anchor = 90,
20 | borderWidth = 1.5,
21 | colorFrom = "#ffaa40",
22 | colorTo = "#9c40ff",
23 | delay = 0,
24 | }: BorderBeamProps) => {
25 | return (
26 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/packages/shared/src/routers/users/index.ts:
--------------------------------------------------------------------------------
1 | import { initContract } from '@ts-rest/core';
2 | import { z } from 'zod';
3 |
4 | const c = initContract();
5 |
6 | const UserSchema = z.object({
7 | id: z.string().uuid().optional(), // uuid with random default
8 | name: z.string().max(255).nullable(), // varchar(255)
9 | email: z.string().email(), // unique and not null
10 | emailVerified: z.boolean().nullable(), // boolean, can be null
11 | image: z.string().nullable(), // text, can be null
12 | createdAt: z.string().optional(), // timestamp in string format, defaulting to now
13 | updatedAt: z.string().optional() // timestamp
14 | });
15 |
16 | const ErrorSchema = z.object({
17 | message: z.string(),
18 | });
19 |
20 | export const userContract = c.router(
21 | {
22 | login: {
23 | method: 'GET',
24 | path: '/auth/login',
25 | responses: {
26 | 201: UserSchema,
27 | 400: ErrorSchema,
28 | },
29 | summary: 'Login the user',
30 |
31 | },
32 | // getPosts: {
33 | // method: 'GET',
34 | // path: `/posts`,
35 | // responses: {
36 | // 200: PostSchema.array(),
37 | // 400: ErrorSchema,
38 | // },
39 | // summary: 'Get All Posts',
40 | // },
41 | // getPost: {
42 | // method: 'GET',
43 | // path: '/posts/:id',
44 | // pathParams: z.object({
45 | // id: z.string().uuid(),
46 | // }),
47 | // responses: {
48 | // 200: PostSchema,
49 | // 400: ErrorSchema,
50 | // },
51 | // summary: 'Get a post by id',
52 | // }
53 | }
54 | )
--------------------------------------------------------------------------------
/apps/web/src/app/blog/page.tsx:
--------------------------------------------------------------------------------
1 | import { allPosts } from "content-collections";
2 | import { MDXContent } from "@content-collections/mdx/react";
3 | import { BlogCard } from "@/components/blog/blog-card";
4 | import Title from "@/components/ui/Title";
5 | import Link from "next/link";
6 |
7 | export const metadata = {
8 | title: "Blog | APP NAME",
9 | description: "Description about your application.",
10 | openGraph: {
11 | title: "Blog | AP NAME",
12 | description: "Description about your application.",
13 | url: "siteUrl",
14 | siteName: "siteName",
15 | // images: [
16 | // {
17 | // url: "",
18 | // width: 1200,
19 | // height: 675,
20 | // },
21 | // ],
22 | },
23 | twitter: {
24 | title: "Blog | APP NAME",
25 | card: "summary_large_image",
26 | },
27 | icons: {
28 | shortcut: "img shorturl",
29 | },
30 | };
31 |
32 | const BlogPage = () => {
33 | return (
34 |
35 |
36 |
Blogs
37 |
38 |
39 |
40 | {allPosts.map((post) => (
41 |
42 |
52 |
53 | ))}
54 |
55 |
56 | );
57 | };
58 |
59 | export default BlogPage;
60 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/ripple.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { CSSProperties } from "react";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | interface RippleProps {
7 | mainCircleSize?: number;
8 | mainCircleOpacity?: number;
9 | numCircles?: number;
10 | className?: string;
11 | }
12 |
13 | const Ripple = React.memo(function Ripple({
14 | mainCircleSize = 210,
15 | mainCircleOpacity = 0.24,
16 | numCircles = 8,
17 | className,
18 | }: RippleProps) {
19 | return (
20 |
26 | {Array.from({ length: numCircles }, (_, i) => {
27 | const size = mainCircleSize + i * 70;
28 | const opacity = mainCircleOpacity - i * 0.03;
29 | const animationDelay = `${i * 0.06}s`;
30 | const borderStyle = i === numCircles - 1 ? "dashed" : "solid";
31 | const borderOpacity = 5 + i * 5;
32 |
33 | return (
34 |
52 | );
53 | })}
54 |
55 | );
56 | });
57 |
58 | Ripple.displayName = "Ripple";
59 |
60 | export default Ripple;
61 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/feature.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | const Feature = ({
4 | title,
5 | description,
6 | icon,
7 | index,
8 | }: {
9 | title: string;
10 | description: string;
11 | icon: React.ReactNode;
12 | index: number;
13 | }) => {
14 | return (
15 |
22 | {index < 4 && (
23 |
24 | )}
25 | {index >= 4 && (
26 |
27 | )}
28 |
29 | {icon}
30 |
31 |
32 |
33 |
34 | {title}
35 |
36 |
37 |
38 | {description}
39 |
40 |
41 | );
42 | };
43 |
44 | export default Feature;
45 |
--------------------------------------------------------------------------------
/docker-compose.prod.yml:
--------------------------------------------------------------------------------
1 | name: god-tier-saas-template
2 | version: '3.8'
3 |
4 | services:
5 | postgres:
6 | image: postgres:16.1
7 | environment:
8 | POSTGRES_USER: ${POSTGRES_USER}
9 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
10 | POSTGRES_DB: ${POSTGRES_DB}
11 | ports:
12 | - "5432:5432"
13 | healthcheck:
14 | test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
15 | interval: 10s
16 | timeout: 5s
17 | retries: 5
18 | volumes:
19 | - ./pg_data:/var/lib/postgresql/data
20 | networks:
21 | - backend
22 |
23 | server:
24 | build:
25 | context: ./
26 | dockerfile: ./apps/server/Dockerfile
27 | ports:
28 | - 3000:3000
29 | environment:
30 | GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
31 | DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:5432/${POSTGRES_DB}?schema=public
32 | depends_on:
33 | postgres:
34 | condition: service_healthy
35 | restart: unless-stopped
36 | volumes:
37 | - .:/app
38 | healthcheck:
39 | test: ["CMD", "curl", "-f", "http://localhost:3000/"]
40 | interval: 10s
41 | timeout: 5s
42 | retries: 1000 # Try launching the API service as long as possible. Required for other services to start
43 | networks:
44 | - backend
45 |
46 | web:
47 | build:
48 | context: ./
49 | dockerfile: ./apps/web/Dockerfile
50 | ports:
51 | - 8090:8090
52 | environment:
53 | NEXT_PUBLIC_SERVER_URL: ${NEXT_PUBLIC_SERVER_URL}
54 | ENV: ${ENV}
55 | AUTH_SECRET: ${AUTH_SECRET}
56 | AUTH_GOOGLE_ID: ${GOOGLE_CLIENT_ID}
57 | AUTH_GOOGLE_SECRET: ${AUTH_GOOGLE_SECRET}
58 | restart: unless-stopped
59 | networks:
60 | - backend
61 | volumes:
62 | - .:/app
63 | depends_on:
64 | server:
65 | condition: service_healthy
66 |
67 | networks:
68 | backend:
69 |
--------------------------------------------------------------------------------
/apps/web/src/app/dashboard/profile/page.tsx:
--------------------------------------------------------------------------------
1 | import { auth } from "@/lib/auth";
2 | import { Card, CardContent } from "@/components/ui/card";
3 | import { Label } from "@/components/ui/label";
4 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
5 | import SignoutButton from "@/components/dashboard/sign-out-button";
6 |
7 | const ProfilePage = async () => {
8 | const session = await auth();
9 |
10 | if (!session) {
11 | return null;
12 | }
13 |
14 | return (
15 |
16 |
17 |
18 |
19 |
23 |
24 | {(session?.user?.name ?? "U").slice(0, 2).toUpperCase()}
25 |
26 |
27 |
28 |
29 |
33 | Name
34 |
35 |
36 | {session?.user?.name}
37 |
38 |
39 |
40 |
44 | Email
45 |
46 |
47 | {session?.user?.email}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | );
56 | };
57 |
58 | export default ProfilePage;
59 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/custom-accordion-item.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useState } from "react";
3 | import { motion, AnimatePresence } from "framer-motion";
4 |
5 | const CustomAccordionItem = ({
6 | accordionTitle,
7 | accordionContent,
8 | doc_link,
9 | isOpen,
10 | onClick,
11 | }: {
12 | accordionTitle: string;
13 | accordionContent: string;
14 | doc_link?: string;
15 | isOpen: boolean;
16 | onClick: () => void;
17 | }) => {
18 | // const [isOpen, setIsOpen] = useState(false);
19 | return (
20 |
21 |
27 | {accordionTitle}
28 |
29 |
30 | {/* Animate the content's entry and exit */}
31 |
32 | {isOpen && (
33 |
40 |
43 | {accordionContent}{" "}
44 | {doc_link && (
45 |
{`[ Docs ]`}
50 | )}
51 |
52 |
53 | )}
54 |
55 |
56 | );
57 | };
58 |
59 | export default CustomAccordionItem;
60 |
--------------------------------------------------------------------------------
/apps/server/src/post/post.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable } from '@nestjs/common';
2 | import { DRIZZLE } from '@/core/drizzle/drizzle.module';
3 | import { post, DrizzleDB, NewPost } from '@template/shared';
4 | import { eq } from 'drizzle-orm';
5 | import { PinoLogger } from 'nestjs-pino';
6 | import { PostHogService } from '@/core/posthog/posthog.service';
7 |
8 | @Injectable()
9 | export class PostService {
10 |
11 | constructor(@Inject(DRIZZLE) private readonly db: DrizzleDB, private readonly logger: PinoLogger, private readonly postHogService: PostHogService) {
12 | this.logger.setContext(PostService.name);
13 | }
14 |
15 | async getPosts() {
16 | this.logger.info('Fetching posts');
17 | this.postHogService.captureEvent('posts', 'fetched');
18 | return this.db.select().from(post);
19 | }
20 |
21 | async addPost(newPost: NewPost) {
22 | this.logger.info('Adding post');
23 | this.postHogService.captureEvent('posts', 'added');
24 | const allPosts = await this.db.insert(post).values(newPost).returning();
25 | return allPosts[0];
26 | }
27 |
28 | async getPost(id: string) {
29 | this.logger.info('Fetching post');
30 | this.postHogService.captureEvent(`post-${id}`, 'fetched');
31 | const fetchedpost = await this.db.select().from(post).where(eq(post.id, id));
32 | return fetchedpost[0];
33 | }
34 |
35 | async updatePost(id: string, newPost: NewPost) {
36 | this.logger.info('Updating post');
37 | this.postHogService.captureEvent(`post-${id}`, 'updated');
38 | const updatedPost = await this.db.update(post).set(newPost).where(eq(post.id, id)).returning();
39 | return updatedPost[0];
40 | }
41 |
42 | async deletePost(id: string) {
43 | this.logger.info('Deleting post');
44 | this.postHogService.captureEvent(`post-${id}`, 'deleted');
45 | const deletedPost = await this.db.delete(post).where(eq(post.id, id)).returning();
46 | return deletedPost[0];
47 | }
48 |
49 |
50 |
51 | }
--------------------------------------------------------------------------------
/apps/web/src/components/blog/article.tsx:
--------------------------------------------------------------------------------
1 | import type { Post } from "content-collections";
2 | import Image from "next/image";
3 | import Link from "next/link";
4 |
5 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
6 |
7 | import { MDX } from "@/components/blog/mdx-content";
8 | import { format, parseISO } from "date-fns";
9 |
10 | export function Article({ post }: { post: Post }) {
11 | const getNameInitials = (name: string) => {
12 | const individualNames = name.split(" ");
13 | return (
14 | individualNames[0][0] + individualNames[individualNames.length - 1][0]
15 | );
16 | };
17 |
18 | return (
19 |
20 |
21 |
{post.title}
22 |
23 |
29 |
30 |
31 |
32 |
33 | {getNameInitials(post.author_name)}
34 |
35 |
36 |
41 | {post.author_name}
42 |
43 |
44 | {format(parseISO(post.date), "MMM dd, yyyy")}
45 | {/* {formatDate(new Date(post.date),'EEEE, MMMM do, yyyy')} */}
46 | {/* {post.readingTime} */}
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TabsPrimitive from "@radix-ui/react-tabs";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ));
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ));
53 | TabsContent.displayName = TabsPrimitive.Content.displayName;
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent };
56 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Card.displayName = "Card";
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | CardHeader.displayName = "CardHeader";
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ));
42 | CardTitle.displayName = "CardTitle";
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLParagraphElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ));
54 | CardDescription.displayName = "CardDescription";
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ));
62 | CardContent.displayName = "CardContent";
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ));
74 | CardFooter.displayName = "CardFooter";
75 |
76 | export {
77 | Card,
78 | CardHeader,
79 | CardFooter,
80 | CardTitle,
81 | CardDescription,
82 | CardContent,
83 | };
84 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion";
5 | import { ChevronDownIcon } from "@radix-ui/react-icons";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Accordion = AccordionPrimitive.Root;
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ));
21 | AccordionItem.displayName = "AccordionItem";
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ));
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ));
55 | AccordionContent.displayName = AccordionPrimitive.Content.displayName;
56 |
57 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
58 |
--------------------------------------------------------------------------------
/packages/shared/src/routers/posts/index.ts:
--------------------------------------------------------------------------------
1 | import { initContract } from '@ts-rest/core';
2 | import { createSelectSchema } from 'drizzle-zod';
3 | import { z } from 'zod';
4 | import { post } from '../../db/schema';
5 |
6 | const c = initContract();
7 |
8 | const PostSchema = createSelectSchema(post);
9 |
10 | const ErrorSchema = z.object({
11 | message: z.string(),
12 | });
13 |
14 | export const postContract = c.router(
15 | {
16 | createPost: {
17 | method: 'POST',
18 | path: '/posts',
19 | responses: {
20 | 200: PostSchema,
21 | 400: ErrorSchema,
22 | },
23 | body: PostSchema.omit({ id: true, createdAt: true }),
24 | summary: 'Create a post',
25 | },
26 | getPosts: {
27 | method: 'GET',
28 | path: `/posts`,
29 | responses: {
30 | 200: PostSchema.array(),
31 | 400: ErrorSchema,
32 | },
33 | summary: 'Get All Posts',
34 | },
35 | getPost: {
36 | method: 'GET',
37 | path: '/posts/:id',
38 | pathParams: z.object({
39 | id: z.string().uuid(),
40 | }),
41 | responses: {
42 | 200: PostSchema,
43 | 400: ErrorSchema,
44 | },
45 | summary: 'Get a post by id',
46 | },
47 | updatePost: {
48 | method: 'PUT',
49 | path: '/posts/:id',
50 | pathParams: z.object({
51 | id: z.string().uuid(),
52 | }),
53 | body: PostSchema.omit({ id: true }),
54 | responses: {
55 | 200: PostSchema,
56 | 400: ErrorSchema,
57 | },
58 | summary: 'Update a post by id',
59 | },
60 | deletePost: {
61 | method: 'DELETE',
62 | path: '/posts/:id',
63 | pathParams: z.object({
64 | id: z.string().uuid(),
65 | }),
66 | body: z.any(),
67 | responses: {
68 | 200: PostSchema,
69 | 400: ErrorSchema,
70 | },
71 | summary: 'Delete a post by id',
72 | }
73 | }
74 | )
--------------------------------------------------------------------------------
/apps/server/src/core/posthog/posthog.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger } from '@nestjs/common';
2 | import { PostHog } from 'posthog-node';
3 | import { ConfigService } from '@nestjs/config';
4 |
5 | @Injectable()
6 | export class PostHogService {
7 | private client: PostHog | null = null;
8 | private readonly logger = new Logger(PostHogService.name);
9 |
10 | constructor(private configService: ConfigService) {
11 | this.initializeClient();
12 | }
13 |
14 | private initializeClient() {
15 | const apiKey = this.configService.get('POSTHOG_KEY');
16 | const host = this.configService.get('POSTHOG_HOST');
17 |
18 | if (!apiKey) {
19 | this.logger.warn('PostHog API key is not set. PostHog tracking will be disabled.');
20 | return;
21 | }
22 |
23 | try {
24 | this.client = new PostHog(apiKey, { host });
25 | this.logger.log('PostHog client initialized successfully');
26 | } catch (error) {
27 | this.logger.error('Failed to initialize PostHog client', error);
28 | }
29 | }
30 |
31 | captureEvent(distinctId: string, event: string, properties?: any) {
32 | if (!this.client) {
33 | this.logger.warn('PostHog client is not initialized. Event not captured.');
34 | return;
35 | }
36 |
37 | try {
38 | this.client.capture({
39 | distinctId,
40 | event,
41 | properties,
42 | });
43 | } catch (error) {
44 | this.logger.error(`Failed to capture event: ${event}`, error);
45 | }
46 | }
47 |
48 | identifyUser(distinctId: string, properties?: any) {
49 | if (!this.client) {
50 | this.logger.warn('PostHog client is not initialized. User not identified.');
51 | return;
52 | }
53 |
54 | try {
55 | this.client.identify({
56 | distinctId,
57 | properties,
58 | });
59 | } catch (error) {
60 | this.logger.error(`Failed to identify user: ${distinctId}`, error);
61 | }
62 | }
63 |
64 | onModuleDestroy() {
65 | if (this.client) {
66 | this.client.shutdown();
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/apps/server/src/core/auth/auth.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable } from '@nestjs/common';
2 | import { OAuth2Client, TokenPayload } from 'google-auth-library';
3 | // import { User, user } from '@/drizzle/schema'
4 | import { DRIZZLE } from '@/core/drizzle/drizzle.module';
5 | import { DrizzleDB, user, NewUser } from '@template/shared';
6 | import { eq } from 'drizzle-orm';
7 |
8 | @Injectable()
9 | export class AuthService {
10 |
11 | constructor(@Inject(DRIZZLE) private readonly db: DrizzleDB) { }
12 |
13 |
14 |
15 | async verifyToken(bearToken: string) {
16 |
17 | try {
18 | const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
19 | const ticket = await client.verifyIdToken({
20 | idToken: bearToken,
21 | audience: process.env.GOOGLE_CLIENT_ID,
22 | });
23 | const payload = ticket.getPayload()!;
24 | const currentTimestamp = Math.floor(new Date().getTime() / 1000);
25 | if (payload.exp < currentTimestamp) {
26 | throw new Error();
27 | }
28 |
29 | if (payload.aud !== process.env.GOOGLE_CLIENT_ID) {
30 | throw new Error();
31 | }
32 |
33 | return {
34 | id: payload.sub,
35 | email: payload.email!,
36 | name: payload.name,
37 | image: payload.picture,
38 | emailverified: payload.email_verified,
39 | }
40 |
41 |
42 | }
43 | catch (error) {
44 | throw new Error('Invalid token');
45 | }
46 |
47 |
48 | }
49 |
50 | async login(loggedInUser: NewUser) {
51 |
52 | // check if user exists
53 | try {
54 | const userExists = await this.db.select().from(user).where(eq(user.id, loggedInUser.id));
55 | if (userExists.length > 0) {
56 | console.log('User already exists');
57 | return userExists[0];
58 | }
59 |
60 | // insert user
61 | const newUser = await this.db.insert(user).values(loggedInUser).returning();
62 | return newUser[0];
63 | } catch (error) {
64 | console.log('Error:', error);
65 | throw new Error('Error logging in user');
66 | }
67 |
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/apps/web/src/components/blog/article-toc.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { cn } from "@/lib/utils";
3 | import Link from "next/link";
4 | import React, { useState, useEffect } from "react";
5 |
6 | type TOCItemType = {
7 | title: React.ReactNode;
8 | url: string;
9 | depth: number;
10 | };
11 |
12 | // Custom hook to track the active section
13 | const useActiveSection = (tableOfContents: TOCItemType[], offset = 100) => {
14 | const [activeSection, setActiveSection] = useState("");
15 |
16 | useEffect(() => {
17 | const handleScroll = () => {
18 | const currentScrollPos = window.scrollY;
19 | let current = "";
20 |
21 | for (const item of tableOfContents) {
22 | const element = document.getElementById(item.url.slice(1));
23 | if (element) {
24 | const { top } = element.getBoundingClientRect();
25 | if (top - offset < 0) {
26 | current = item.url;
27 | } else {
28 | break;
29 | }
30 | }
31 | }
32 |
33 | setActiveSection(current);
34 | };
35 |
36 | window.addEventListener("scroll", handleScroll);
37 | handleScroll(); // Call once to set initial state
38 |
39 | return () => window.removeEventListener("scroll", handleScroll);
40 | }, [tableOfContents, offset]);
41 |
42 | return activeSection;
43 | };
44 |
45 | const ArticleTOC = ({
46 | tableOfContents,
47 | }: {
48 | tableOfContents: TOCItemType[];
49 | }) => {
50 | const activeSection = useActiveSection(tableOfContents);
51 |
52 | return (
53 |
54 | {tableOfContents.length > 0 &&
55 | tableOfContents.map((item: TOCItemType, index: number) => (
56 |
57 |
71 | {item.title}
72 |
73 |
74 | ))}
75 |
76 | );
77 | };
78 |
79 | export default ArticleTOC;
80 |
--------------------------------------------------------------------------------
/apps/web/src/components/posts/post-list.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { api } from "@/lib/api-client";
3 | import { Skeleton } from "@/components/ui/skeleton";
4 | import PostCard from "./post-card";
5 | import { useEffect, useState } from "react";
6 | import React from "react";
7 |
8 | const Posts = () => {
9 | const [isAtBottom, setIsAtBottom] = useState(false);
10 |
11 | useEffect(() => {
12 | const handleScroll = () => {
13 | const windowHeight = window.innerHeight;
14 | const documentHeight = document.documentElement.scrollHeight;
15 | const scrollTop =
16 | window.pageYOffset || document.documentElement.scrollTop;
17 | const scrollBottom = scrollTop + windowHeight;
18 |
19 | setIsAtBottom(scrollBottom >= documentHeight - 20); // 20px threshold
20 | };
21 |
22 | window.addEventListener("scroll", handleScroll);
23 | handleScroll(); // Check initial scroll position
24 |
25 | return () => window.removeEventListener("scroll", handleScroll);
26 | }, []);
27 |
28 | const { data, isLoading, isError } = api.posts.getPosts.useQuery({
29 | queryKey: ["posts"],
30 | retryOnMount: false,
31 | refetchOnMount: false,
32 | refetchOnWindowFocus: false,
33 | });
34 |
35 | return (
36 |
37 | {isLoading ? (
38 |
39 |
40 |
41 | ) : isError ? (
42 |
43 |
Error fetching posts
44 |
45 | ) : (
46 | <>
47 |
48 |
49 | {" "}
50 | {/* Added bottom padding */}
51 |
52 | {data?.body.map((card) => (
53 |
60 | ))}
61 |
62 |
63 | {!isAtBottom && (
64 |
65 | )}
66 |
67 | >
68 | )}
69 |
70 | );
71 | };
72 |
73 | export default Posts;
74 |
--------------------------------------------------------------------------------
/apps/web/src/components/landing-page/features.tsx:
--------------------------------------------------------------------------------
1 | import Feature from "@/components/ui/feature";
2 | import {
3 | IconTransfer,
4 | IconDatabase,
5 | IconBrush,
6 | IconShield,
7 | IconDeviceDesktopAnalytics,
8 | IconShip,
9 | IconBlockquote,
10 | IconPaint,
11 | } from "@tabler/icons-react";
12 | import Title from "../ui/Title";
13 |
14 | const Features = () => {
15 | const features = [
16 | {
17 | title: "End to End Type Safety",
18 | description:
19 | "Get the best developer experience with type-safety and easy API development.",
20 | icon: ,
21 | },
22 | {
23 | title: "Manage your database with Drizzle ORM",
24 | description:
25 | "Drizzle ORM enables fast, type-safe database queries with PostgreSQL.",
26 | icon: ,
27 | },
28 | {
29 | title: "Demo of Optimistic UI",
30 | description:
31 | "Example of a Posts page demonstrating Optimistic UI CRUD operations using Tanstack Query.",
32 | icon: ,
33 | },
34 | {
35 | title: "Secure Authentication",
36 | description:
37 | "Implement secure client-side and server-side authentication using AuthJS.",
38 | icon: ,
39 | },
40 | {
41 | title: "Analytics with PostHog",
42 | description:
43 | "Track user behavior and understand your customers better with PostHog.",
44 | icon: ,
45 | },
46 | {
47 | title: "Docker-ready for deployment.",
48 | description: "Deploy your application with Docker and Docker Compose.",
49 | icon: ,
50 | },
51 | {
52 | title: "Ready-to-use ContentLayer for blog and changelog",
53 | description:
54 | "Manage your blogs and changelogs with ready-to-use content collections.",
55 | icon: ,
56 | },
57 | {
58 | title: "Templates from landing page to Dashboard",
59 | description:
60 | "Use ready-to-use templatees for your landing page and dashboard.",
61 | icon: ,
62 | },
63 | ];
64 |
65 | return (
66 |
67 |
68 |
Features
69 |
70 |
71 | {features.map((feature, index) => (
72 |
73 | ))}
74 |
75 |
76 | );
77 | };
78 |
79 | export default Features;
80 |
--------------------------------------------------------------------------------
/apps/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@template/web",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev -p 8090",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "format": "prettier src/ --write"
11 | },
12 | "dependencies": {
13 | "@hookform/resolvers": "^3.9.0",
14 | "@radix-ui/react-accordion": "^1.2.0",
15 | "@radix-ui/react-avatar": "^1.1.1",
16 | "@radix-ui/react-checkbox": "^1.1.2",
17 | "@radix-ui/react-dialog": "^1.1.1",
18 | "@radix-ui/react-dropdown-menu": "^2.1.2",
19 | "@radix-ui/react-hover-card": "^1.1.1",
20 | "@radix-ui/react-icons": "^1.3.0",
21 | "@radix-ui/react-label": "^2.1.0",
22 | "@radix-ui/react-navigation-menu": "^1.2.0",
23 | "@radix-ui/react-separator": "^1.1.0",
24 | "@radix-ui/react-slot": "^1.1.0",
25 | "@radix-ui/react-tabs": "^1.1.1",
26 | "@radix-ui/react-tooltip": "^1.1.3",
27 | "@tabler/icons-react": "^3.19.0",
28 | "@tanstack/react-query": "5",
29 | "@tanstack/react-query-devtools": "^5.56.2",
30 | "@template/shared": "workspace:*",
31 | "@ts-rest/react-query": "^3.51.0",
32 | "axios": "^1.7.7",
33 | "class-variance-authority": "^0.7.0",
34 | "clsx": "^2.1.1",
35 | "date-fns": "^4.1.0",
36 | "framer-motion": "^11.9.0",
37 | "fumadocs-core": "^13.4.10",
38 | "js-cookie": "^3.0.5",
39 | "lucide-react": "^0.441.0",
40 | "mutative": "^1.0.10",
41 | "next": "15.0.1",
42 | "next-auth": "^5.0.0-beta.25",
43 | "next-themes": "^0.3.0",
44 | "posthog-js": "^1.165.0",
45 | "qss": "^3.0.0",
46 | "react": "19.0.0-rc-69d4b800-20241021",
47 | "react-dom": "19.0.0-rc-69d4b800-20241021",
48 | "react-hook-form": "^7.53.0",
49 | "sonner": "^1.5.0",
50 | "tailwind-merge": "^2.5.2",
51 | "tailwindcss-animate": "^1.0.7",
52 | "zod": "^3.23.8"
53 | },
54 | "devDependencies": {
55 | "@content-collections/core": "^0.7.2",
56 | "@content-collections/mdx": "^0.1.6",
57 | "@content-collections/next": "^0.2.3",
58 | "@tailwindcss/typography": "^0.5.15",
59 | "@types/js-cookie": "^3.0.6",
60 | "@types/node": "^20",
61 | "@types/react": "npm:types-react@19.0.0-rc.1",
62 | "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
63 | "@types/react-syntax-highlighter": "^15.5.13",
64 | "eslint": "^8",
65 | "eslint-config-next": "15.0.1",
66 | "eslint-config-prettier": "^9.0.0",
67 | "postcss": "^8",
68 | "react-syntax-highlighter": "^15.5.0",
69 | "tailwindcss": "^3.4.1",
70 | "typescript": "^5"
71 | },
72 | "overrides": {
73 | "@types/react": "npm:types-react@19.0.0-rc.1",
74 | "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/apps/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@template/server",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "",
6 | "private": true,
7 | "license": "UNLICENSED",
8 | "scripts": {
9 | "build": "nest build",
10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
11 | "start": "nest start",
12 | "dev": "(cd node_modules/@template/shared && pnpm run build) && nest start --watch",
13 | "start:debug": "nest start --debug --watch",
14 | "start:prod": "node dist/main",
15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
16 | "test": "jest",
17 | "test:watch": "jest --watch",
18 | "test:cov": "jest --coverage",
19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
20 | "test:e2e": "jest --config ./test/jest-e2e.json"
21 | },
22 | "dependencies": {
23 | "@nestjs/common": "^10.0.0",
24 | "@nestjs/config": "^3.2.3",
25 | "@nestjs/core": "^10.0.0",
26 | "@nestjs/platform-express": "^10.0.0",
27 | "@template/shared": "workspace:*",
28 | "@ts-rest/core": "^3.51.0",
29 | "@ts-rest/nest": "^3.51.0",
30 | "drizzle-orm": "^0.33.0",
31 | "google-auth-library": "^9.14.1",
32 | "nestjs-pino": "^4.1.0",
33 | "pino-http": "^10.3.0",
34 | "pino-pretty": "^11.2.2",
35 | "posthog-node": "^4.2.0",
36 | "reflect-metadata": "^0.2.0",
37 | "rxjs": "^7.8.1"
38 | },
39 | "devDependencies": {
40 | "@nestjs/cli": "^10.0.0",
41 | "@nestjs/schematics": "^10.0.0",
42 | "@nestjs/testing": "^10.0.0",
43 | "@types/express": "^4.17.17",
44 | "@types/jest": "^29.5.2",
45 | "@types/node": "^20.3.1",
46 | "@types/supertest": "^6.0.0",
47 | "@typescript-eslint/eslint-plugin": "^8.0.0",
48 | "@typescript-eslint/parser": "^8.0.0",
49 | "class-transformer": "^0.5.1",
50 | "class-validator": "^0.14.1",
51 | "drizzle-kit": "^0.24.2",
52 | "eslint": "^8.42.0",
53 | "eslint-config-prettier": "^9.0.0",
54 | "eslint-plugin-prettier": "^5.0.0",
55 | "jest": "^29.5.0",
56 | "prettier": "^3.0.0",
57 | "source-map-support": "^0.5.21",
58 | "supertest": "^7.0.0",
59 | "ts-jest": "^29.1.0",
60 | "ts-loader": "^9.4.3",
61 | "ts-node": "^10.9.1",
62 | "tsconfig-paths": "^4.2.0",
63 | "typescript": "^5.1.3"
64 | },
65 | "jest": {
66 | "moduleFileExtensions": [
67 | "js",
68 | "json",
69 | "ts"
70 | ],
71 | "rootDir": "src",
72 | "testRegex": ".*\\.spec\\.ts$",
73 | "transform": {
74 | "^.+\\.(t|j)s$": "ts-jest"
75 | },
76 | "collectCoverageFrom": [
77 | "**/*.(t|j)s"
78 | ],
79 | "coverageDirectory": "../coverage",
80 | "testEnvironment": "node"
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/apps/web/src/components/dashboard/mobile-navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | // import { signOut } from "next-auth/";
4 | import Link from "next/link";
5 | import { useRouter } from "next/navigation";
6 |
7 | import { LogOut, Menu, User } from "lucide-react";
8 |
9 | import { Button } from "@/components/ui/button";
10 | import {
11 | Sheet,
12 | SheetClose,
13 | SheetContent,
14 | SheetHeader,
15 | SheetTrigger,
16 | } from "@/components/ui/sheet";
17 | import { dashboardNavitems } from "@/config/dashboard-navitems";
18 | import { cn } from "@/lib/utils";
19 |
20 | import { UserButton } from "./user-button";
21 |
22 | type Props = {
23 | className?: string;
24 | };
25 |
26 | export const MobileNavBar = ({ className }: Props) => {
27 | const router = useRouter();
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 |
{"My App"}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
46 |
47 |
48 | Options
49 |
50 |
51 | {dashboardNavitems.map((item) => (
52 |
53 |
54 |
58 |
59 | {item.label}
60 |
61 |
62 |
63 | ))}
64 | {/* */}
65 |
66 |
70 |
71 | Sign Out
72 |
73 |
74 | {/* */}
75 |
76 |
77 |
78 |
79 |
80 | );
81 | };
82 |
--------------------------------------------------------------------------------
/apps/web/src/components/landing-page/code-selection.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import SyntaxHighlighter from "react-syntax-highlighter";
3 | import { nightOwl } from "react-syntax-highlighter/dist/esm/styles/hljs";
4 | import CustomAccordionItem from "@/components/ui/custom-accordion-item";
5 | import { useState } from "react";
6 | import { BorderBeam } from "@/components/ui/border-beam";
7 | import {
8 | AccordionData,
9 | AccordionDataItemType,
10 | } from "@/config/code-selection-items-config";
11 |
12 | const CodeSelection = () => {
13 | const [currentStep, setCurrentStep] = useState(0);
14 |
15 | return (
16 |
20 |
21 |
22 | {AccordionData[currentStep].fileName}
23 |
24 |
41 | {AccordionData[currentStep].codeString}
42 |
43 |
44 |
51 |
52 |
53 |
54 |
55 | Follow these steps to add and use the API
56 |
57 |
58 | {AccordionData.map((item: AccordionDataItemType, index: number) => (
59 | setCurrentStep(item.index)}
65 | key={index}
66 | />
67 | ))}
68 |
69 |
70 |
71 | );
72 | };
73 |
74 | export default CodeSelection;
75 |
--------------------------------------------------------------------------------
/apps/web/src/components/dashboard/app-sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import { usePathname } from "next/navigation";
5 | import React from "react";
6 | import { dashboardNavitems, NavItem } from "@/config/dashboard-navitems";
7 |
8 | import { Home, Inbox } from "lucide-react";
9 |
10 | import {
11 | Sidebar,
12 | SidebarContent,
13 | SidebarFooter,
14 | SidebarGroup,
15 | SidebarGroupContent,
16 | SidebarGroupLabel,
17 | SidebarHeader,
18 | SidebarMenu,
19 | SidebarMenuButton,
20 | SidebarMenuItem,
21 | SidebarProvider,
22 | } from "@/components/ui/sidebar";
23 | import { cn } from "@/lib/utils";
24 |
25 | import { UserButton } from "./user-button";
26 |
27 | function AppSidebar({ className }: { className: string }) {
28 | const pathName = usePathname();
29 |
30 | return (
31 |
32 |
33 |
34 |
35 |
36 |
37 |
41 | My App
42 |
43 |
44 |
45 |
46 |
47 |
48 | Options
49 |
50 |
51 | {dashboardNavitems.map((item) => (
52 |
53 |
60 |
61 |
62 | {item.label}
63 |
64 |
65 |
66 | ))}
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | );
82 | }
83 |
84 | export default AppSidebar;
85 |
--------------------------------------------------------------------------------
/apps/web/src/app/blog/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { allPosts } from "content-collections";
2 | import { getTableOfContents } from "fumadocs-core/server";
3 | import { MDXContent } from "@content-collections/mdx/react";
4 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
5 | import { format, parseISO } from "date-fns";
6 | import { notFound } from "next/navigation";
7 | import { MDX } from "@/components/blog/mdx-content";
8 | import { Article } from "@/components/blog/article";
9 | import Link from "next/link";
10 | import { cn } from "@/lib/utils";
11 | import ArticleTOC from "@/components/blog/article-toc";
12 | import { Button } from "@/components/ui/button";
13 | import { ChevronLeft } from "lucide-react";
14 |
15 | type TOCItemType = {
16 | title: React.ReactNode;
17 | url: string;
18 | depth: number;
19 | };
20 |
21 | export const generateStaticParams = async () =>
22 | allPosts.map((post) => ({
23 | slug: post.slug,
24 | }));
25 |
26 | const page = async (props: { params: Promise<{ slug: string }> }) => {
27 | const params = await props.params;
28 | const post = allPosts.find((post) => post.slug === params.slug);
29 | // const [tableOfContents,setTableOfContents] = useState([]);
30 | let tableOfContents: TOCItemType[] = [];
31 |
32 | if (post) {
33 | tableOfContents = await getTableOfContents(post.content);
34 | console.log(tableOfContents);
35 | // setTableOfContents(toc);
36 | }
37 |
38 | if (!post) {
39 | notFound();
40 | }
41 |
42 | return (
43 |
44 |
45 |
46 |
47 |
48 | {" "}
49 | {"Back"}
50 |
51 |
52 |
53 |
56 |
57 |
58 |
59 |
60 | Contents
61 |
62 |
65 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default page;
72 |
--------------------------------------------------------------------------------
/apps/web/src/config/landing-page-nav-items.ts:
--------------------------------------------------------------------------------
1 | import type { ValidIcon } from "@/components/ui/icons";
2 |
3 | export type Page = {
4 | title: string;
5 | subtitle?: string;
6 | description: string;
7 | href: string;
8 | icon: ValidIcon;
9 | disabled?: boolean;
10 | segment: string;
11 | children?: Page[];
12 | };
13 |
14 | type MarketingPageType = Page;
15 |
16 | export const marketingProductPagesConfig = [
17 | {
18 | href: "/",
19 | title: "Feature 1",
20 | subtitle: "The subtitle for feature 1",
21 | description: "The description for feature 1",
22 | segment: "features",
23 | icon: "activity",
24 | },
25 | {
26 | href: "/",
27 | title: "Feature 2",
28 | subtitle: "The subtitle for feature 2",
29 | description: "The description for feature 2",
30 | segment: "features",
31 | icon: "panel-top",
32 | },
33 | ] as const satisfies MarketingPageType[];
34 |
35 | export const marketingResourcePagesConfig = [
36 | {
37 | href: "/blog",
38 | title: "Blog",
39 | description: "All the latest articles and news from GTS.",
40 | segment: "blog",
41 | icon: "book",
42 | },
43 | {
44 | href: "/changelog",
45 | title: "Changelog",
46 | description: "All the latest features, fixes and work to GTS.",
47 | segment: "changelog",
48 | icon: "newspaper",
49 | },
50 | ] as const satisfies Page[];
51 |
52 | export const mainPageConfig = [
53 | {
54 | href: "/",
55 | title: "Features",
56 | description: "All product features for GTS",
57 | segment: "",
58 | icon: "package",
59 | children: marketingProductPagesConfig,
60 | },
61 | {
62 | href: "/",
63 | description: "All resources for GTS",
64 | title: "Resources",
65 | segment: "",
66 | icon: "library",
67 | children: marketingResourcePagesConfig,
68 | },
69 | {
70 | href: "/",
71 | title: "Pricing",
72 | description: "The pricing for GTS.",
73 | segment: "pricing",
74 | icon: "credit-card",
75 | },
76 | {
77 | href: "/",
78 | description: "The documentation for GTS.",
79 | title: "Docs",
80 | segment: "docs",
81 | icon: "book",
82 | },
83 | ] satisfies Page[];
84 |
85 | export function getPageBySegment(
86 | segment: string | string[],
87 | currentPage: readonly Page[] = mainPageConfig
88 | ): Page | undefined {
89 | if (typeof segment === "string") {
90 | const page = currentPage.find((page) => page.segment === segment);
91 | return page;
92 | }
93 | if (Array.isArray(segment) && segment.length > 0) {
94 | const [firstSegment, ...restSegments] = segment;
95 | const childPage = currentPage.find((page) => page.segment === firstSegment);
96 | if (childPage?.children) {
97 | return getPageBySegment(restSegments, childPage.children);
98 | }
99 | return childPage;
100 | }
101 | return undefined;
102 | }
103 |
--------------------------------------------------------------------------------
/apps/web/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 0 0% 3.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 0 0% 3.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 0 0% 3.9%;
13 | --primary: 0 0% 9%;
14 | --primary-foreground: 0 0% 98%;
15 | --secondary: 0 0% 96.1%;
16 | --secondary-foreground: 0 0% 9%;
17 | --muted: 0 0% 96.1%;
18 | --muted-foreground: 0 0% 45.1%;
19 | --accent: 0 0% 96.1%;
20 | --accent-foreground: 0 0% 9%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 0 0% 98%;
23 | --border: 0 0% 89.8%;
24 | --input: 0 0% 89.8%;
25 | --ring: 0 0% 3.9%;
26 | --radius: 0.75rem;
27 | --chart-1: 12 76% 61%;
28 | --chart-2: 173 58% 39%;
29 | --chart-3: 197 37% 24%;
30 | --chart-4: 43 74% 66%;
31 | --chart-5: 27 87% 67%;
32 |
33 | --color-1: 0 100% 63%;
34 | --color-2: 270 100% 63%;
35 | --color-3: 210 100% 63%;
36 | --color-4: 195 100% 63%;
37 | --color-5: 90 100% 63%;
38 | --sidebar-background: 0 0% 98%;
39 | --sidebar-foreground: 240 5.3% 26.1%;
40 | --sidebar-primary: 240 5.9% 10%;
41 | --sidebar-primary-foreground: 0 0% 98%;
42 | --sidebar-accent: 240 4.8% 95.9%;
43 | --sidebar-accent-foreground: 240 5.9% 10%;
44 | --sidebar-border: 220 13% 91%;
45 | --sidebar-ring: 217.2 91.2% 59.8%;
46 | }
47 |
48 | .dark {
49 | --background: 0 0% 3.9%;
50 | --foreground: 0 0% 98%;
51 | --card: 0 0% 3.9%;
52 | --card-foreground: 0 0% 98%;
53 | --popover: 0 0% 3.9%;
54 | --popover-foreground: 0 0% 98%;
55 | --primary: 0 0% 98%;
56 | --primary-foreground: 0 0% 9%;
57 | --secondary: 0 0% 14.9%;
58 | --secondary-foreground: 0 0% 98%;
59 | --muted: 0 0% 14.9%;
60 | --muted-foreground: 0 0% 63.9%;
61 | --accent: 0 0% 14.9%;
62 | --accent-foreground: 0 0% 98%;
63 | --destructive: 0 62.8% 30.6%;
64 | --destructive-foreground: 0 0% 98%;
65 | --border: 0 0% 14.9%;
66 | --input: 0 0% 14.9%;
67 | --ring: 0 0% 83.1%;
68 | --chart-1: 220 70% 50%;
69 | --chart-2: 160 60% 45%;
70 | --chart-3: 30 80% 55%;
71 | --chart-4: 280 65% 60%;
72 | --chart-5: 340 75% 55%;
73 | --color-1: 0 100% 63%;
74 | --color-2: 270 100% 63%;
75 | --color-3: 210 100% 63%;
76 | --color-4: 195 100% 63%;
77 | --color-5: 90 100% 63%;
78 | --sidebar-background: 240 5.9% 10%;
79 | --sidebar-foreground: 240 4.8% 95.9%;
80 | --sidebar-primary: 224.3 76.3% 48%;
81 | --sidebar-primary-foreground: 0 0% 100%;
82 | --sidebar-accent: 240 3.7% 15.9%;
83 | --sidebar-accent-foreground: 240 4.8% 95.9%;
84 | --sidebar-border: 240 3.7% 15.9%;
85 | --sidebar-ring: 217.2 91.2% 59.8%;
86 | }
87 | }
88 |
89 | @layer base {
90 | * {
91 | @apply border-border;
92 | }
93 | body {
94 | @apply bg-background text-foreground;
95 | font-feature-settings:
96 | "rlig" 1,
97 | "calt" 1;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/apps/web/src/components/blog/blog-card.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | // import type { Author } from "@/content/blog/authors";
4 | import { cn } from "@/lib/utils";
5 | import { format } from "date-fns";
6 | // import { Frame } from "../frame";
7 | // import { ImageWithBlur } from "../image-with-blur";
8 | import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
9 | import { Frame } from "./frame";
10 | import { ImageWithBlur } from "./image-with-blur";
11 |
12 | type BlogCardProps = {
13 | tags?: string[];
14 | imageUrl?: string;
15 | title?: string;
16 | subTitle?: string;
17 | authorName?: string;
18 | authorImage?: string;
19 | publishDate?: string;
20 | className?: string;
21 | type?: string;
22 | };
23 |
24 | export function BlogCard(props: BlogCardProps) {
25 | return (
26 |
32 |
33 |
34 |
35 |
43 |
44 |
45 |
46 |
47 |
48 | {/*
49 | {tags?.map((tag) => (
50 |
51 | {tag.charAt(0).toUpperCase() + tag.slice(1)}
52 |
53 | ))}
54 |
*/}
55 |
56 | {props.type}
57 |
58 |
59 | {props.title}
60 |
61 |
62 |
63 | {props.subTitle}
64 |
65 | {/* Todo: Needs ability to add multiple authors at some point */}
66 |
67 | {/*
68 |
69 |
70 |
71 |
72 |
73 |
{props.authorName}
74 |
75 | {format(new Date(props.publishDate!), "MMM dd, yyyy")}
76 |
77 |
78 |
*/}
79 |
80 |
81 |
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/apps/server/src/post/post.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, Post, Req, HttpCode, HttpStatus, Body, UseGuards } from '@nestjs/common';
2 | import { PostService } from './post.service';
3 | // import { CreatePostDto } from './dto/create-post.dto';
4 | import { contract } from '@template/shared';
5 | import { TsRestHandler, tsRestHandler } from '@ts-rest/nest';
6 | import { AuthGuard } from 'src/core/auth/guards/auth.guard';
7 |
8 |
9 | @Controller()
10 | export class PostController {
11 |
12 | constructor(private readonly postService: PostService) { }
13 |
14 |
15 | @UseGuards(AuthGuard)
16 | @TsRestHandler(contract.posts)
17 | async postHandler() {
18 | return tsRestHandler(contract.posts, {
19 | getPosts: async () => {
20 | const posts = await this.postService.getPosts();
21 | if (!posts) {
22 | return {
23 | status: 400,
24 | body: { message: "Failed to get posts" }
25 | }
26 | }
27 | return {
28 | status: 200,
29 | body: posts
30 | }
31 | },
32 | createPost: async ({ body }) => {
33 | const createdPost = await this.postService.addPost(body);
34 | if (!createdPost) {
35 | return {
36 | status: 400,
37 | body: { message: "Failed to get posts" }
38 | }
39 | }
40 | return {
41 | status: 200,
42 | body: createdPost
43 | }
44 | },
45 | getPost: async ({ params }) => {
46 | const post = await this.postService.getPost(params.id);
47 | if (!post) {
48 | return {
49 | status: 400,
50 | body: { message: "Failed to get post" }
51 | }
52 | }
53 | return {
54 | status: 200,
55 | body: post
56 | }
57 | },
58 | updatePost: async ({ params, body }) => {
59 | const updatedPost = await this.postService.updatePost(params.id, body);
60 | if (!updatedPost) {
61 | return {
62 | status: 400,
63 | body: { message: "Failed to update post" }
64 | }
65 | }
66 | return {
67 | status: 200,
68 | body: updatedPost
69 | }
70 |
71 | },
72 | deletePost: async ({ params }) => {
73 | const deletedPost = await this.postService.deletePost(params.id);
74 | if (!deletedPost) {
75 | return {
76 | status: 400,
77 | body: { message: "Failed to delete post" }
78 | }
79 | }
80 | return {
81 | status: 200,
82 | body: deletedPost
83 | }
84 | },
85 |
86 | })
87 |
88 | }
89 |
90 | // @Post()
91 | // createPost(@Body() post: CreatePostDto) {
92 | // return this.postService.addPost(post);
93 | // }
94 |
95 | // @Get()
96 | // getPosts() {
97 | // return this.postService.getPosts();
98 | // }
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | }
--------------------------------------------------------------------------------
/apps/web/src/config/code-selection-items-config.ts:
--------------------------------------------------------------------------------
1 | export type AccordionDataItemType = {
2 | title: string;
3 | content: string;
4 | index: number;
5 | codeString: string;
6 | fileName: string;
7 | doc_link?: string;
8 | };
9 |
10 | export const AccordionData: AccordionDataItemType[] = [
11 | {
12 | title: "1) Define the API with ts-rest Router in shared module",
13 | content:
14 | "Define your API endpoint fields like body, query, pathParams, and headers in the router function using a simple TypeScript type with the c.type helper, or use Zod objects instead.",
15 | index: 0,
16 | doc_link: "https://ts-rest.com/docs/core/",
17 | fileName: "shared/../router.ts",
18 | codeString: `
19 | import { initContract } from '@ts-rest/core';
20 | import { z } from 'zod';
21 |
22 | const c = initContract();
23 |
24 | export const contract = c.router(
25 | {
26 | getTodo: {
27 | method: 'GET',
28 | path: '/todos',
29 | responses: {
30 | 200: z.object({
31 | id: z.string(),
32 | todoTitle: z.string(),
33 | completed: z.boolean(),
34 |
35 | }).array(),
36 | 400: z.object({
37 | message: z.string(),
38 | }),
39 | },
40 | summary: 'Get all todos',
41 | }
42 | },
43 | );
44 | `,
45 | },
46 | {
47 | title: "2) Implement the API in the NestJS controller",
48 | content:
49 | "Use TsRestHandler to implement the API in the controller and it is also compatible with NestJS decorators.",
50 | index: 1,
51 | doc_link: "https://ts-rest.com/docs/react-query/query-client",
52 | fileName: "server/../todo.controller.ts",
53 | codeString: `
54 | import { Controller, Get, Post, Req, HttpCode, HttpStatus, Body, UseGuards } from '@nestjs/common';
55 | import { TodoService } from './todo.service';
56 | import { contract } from '@template/shared';
57 | import { TsRestHandler, tsRestHandler } from '@ts-rest/nest';
58 |
59 | @Controller()
60 | export class TodoController {
61 | constructor(private readonly todoService: TodoService) { }
62 |
63 | @TsRestHandler(contract.getTodo)
64 | async getTodos() {
65 | return tsRestHandler(contract.getTodo, async () => {
66 |
67 | const todos = await this.todoService.getPosts();
68 | if (!todos) {
69 | return {
70 | status: 400,
71 | body: { message: "Failed to get todos" }
72 | }
73 | }
74 | console.log(todos);
75 | return {
76 | status: 200,
77 | body: todos
78 | }
79 |
80 | })
81 | }
82 | }
83 | `,
84 | },
85 | {
86 | title: "3) Use the type-safe API on the frontend with Tanstack Query.",
87 | content:
88 | "You can use Tanstack query which provides type-safety for your fetch and mutation calls.",
89 | index: 2,
90 | fileName: "web/../todo/page.tsx",
91 | codeString: `
92 | "use client"
93 |
94 | import { api } from "@/lib/api-client"
95 |
96 |
97 | const TodoPage = () => {
98 |
99 | const {data,isLoading,isError} = api.getTodo.useQuery({
100 | queryKey: ['todos'],
101 | });
102 |
103 | return (
104 |
105 |
{isLoading ? 'Loading...' : JSON.stringify(data?.body)}
106 |
107 | )
108 | }
109 |
110 | export default TodoPage
111 | `,
112 | },
113 | ];
114 |
--------------------------------------------------------------------------------
/apps/web/content/blog/guide-to-setup-the-kit.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | date: 2024-02-10
3 | title: How to setup the kit
4 | image: "/images/blog/blog-1.png"
5 | description: It is a step by step guide to setup the kit
6 | author_name: Mit
7 | type: guide
8 | author_image: "/images/blog/mit.jpg"
9 | ---
10 |
11 | # Instructions to setup the kit
12 |
13 | First let's understand the project structure
14 |
15 |
16 | ## Project Structure
17 |
18 | - apps/web - The Next.js web application
19 | - apps/server - The Nest.js API server
20 | - packages/shared - The shared module which manages the database and ts-rest router for both the web and API projects
21 |
22 | ## Setup and Running Instructions
23 |
24 | ### 1. 🐳 Using Docker
25 |
26 | To set up and run the project using Docker Compose:
27 |
28 | 1. Ensure you have Docker and Docker Compose installed on your system.
29 | 2. Open a terminal and navigate to the project's root directory.
30 | 3. Copy the example environment files and add appropriate environment variables:
31 |
32 | ```bash
33 | cp apps/web/.env.example apps/web/.env.local
34 | cp .env.example .env
35 | cp apps/server/.env.example apps/server/.env
36 | cp packages/shared/.env.example packages/shared/.env
37 | ```
38 |
39 | Edit `.env` files and add the necessary environment variables. Follow instructions in each file and modify the values according to your environment and the way you want to run the app.
40 |
41 | 4. Remove all node_modules
42 |
43 | ```bash
44 | rm -rf node_modules .pnpm-store ./apps/web/node_modules ./apps/server/node_modules ./packages/shared/node_modules ./apps/server/dist pg_data
45 | ```
46 |
47 | 5. Run the following command:
48 |
49 | ```bash
50 | docker-compose -f docker-compose.dev.yml up
51 | ```
52 |
53 | This command will build and start all the necessary containers defined in the `docker-compose.dev.yml` file.
54 |
55 | ### 2. 📦 Using pnpm (Local Development)
56 |
57 | To set up and run the project locally using pnpm:
58 |
59 | 1. Make sure you have Node.js (version 18 or higher) and pnpm installed on your system.
60 | 2. Open a terminal and navigate to the project's root directory.
61 | 3. Copy the example environment files and add appropriate environment variables:
62 |
63 | ```bash
64 | cp apps/web/.env.example apps/web/.env.local
65 | cp .env.example .env
66 | cp apps/server/.env.example apps/server/.env
67 | cp packages/shared/.env.example packages/shared/.env
68 | ```
69 |
70 | Edit both `.env` files and add the necessary environment variables. Here docker-compose.postgres.yml used db credentials from .env file. So make sure the credentails that you used in apps.server/.env and packages.shared/.env are same as root .env file. Follow instructions in each file and modify the values according to your environment and the way you want to run the app.
71 |
72 | 4. Install the dependencies by running:
73 |
74 | ```bash
75 | pnpm install
76 | ```
77 |
78 | 5. Build the shared module
79 |
80 | ```bash
81 | pnpm run build:shared
82 | ```
83 |
84 | 6. Start the PostgreSQL database using Docker Compose:
85 |
86 | ```bash
87 | docker-compose -f docker-compose.postgres.yml up -d
88 | ```
89 |
90 | 7. Start the development servers by running:
91 |
92 | ```bash
93 | pnpm run dev
94 | ```
95 |
96 | This command will concurrently start both the Next.js web application (on port 8090) and the NestJS API server with shared module which manages db and ts-rest router. It uses Turbo to manage the monorepo workspace and run the development scripts for both the web and API projects simultaneously.
97 |
98 | Make sure your environment variables are properly configured to connect to the PostgreSQL database started by Docker Compose.
99 |
--------------------------------------------------------------------------------
/apps/web/src/components/blog/dark-theme.ts:
--------------------------------------------------------------------------------
1 | import { SyntaxHighlighterProps } from "react-syntax-highlighter";
2 | export default {
3 | "hljs-type": {
4 | color: "#F8F8F2", //No effect in tests
5 | },
6 | "hljs-keyword": {
7 | color: "#9D72FF",
8 | },
9 | "hljs-built_in": {
10 | color: "#3CEEAE",
11 | },
12 | "hljs-built_in-name": {
13 | color: "#F8F8F2", //No effect in tests
14 | },
15 | "hljs-literal": {
16 | color: "#9D72FF",
17 | },
18 | "hljs-number": {
19 | color: "#F8F8F2",
20 | },
21 | "hljs-string": {
22 | color: "#3CEEAE",
23 | },
24 | "hljs-subst": {
25 | color: "#F8F8F2",
26 | },
27 | "hljs-symbol": {
28 | color: "#FB3186",
29 | },
30 | "hljs-variable": {
31 | color: "#FB3186",
32 | },
33 | "hljs-variable-language": {
34 | color: "#F8F8F2", //No effect in tests
35 | },
36 | "hljs-variable-constant": {
37 | color: "#F8F8F2", //No effect in tests
38 | },
39 | "hljs-title": {
40 | color: "#FB3186",
41 | },
42 | "hljs-title.class": {
43 | color: "#F8F8F2", //No effect in tests
44 | },
45 | "hljs-title-class-inherited": {
46 | color: "#F8F8F2", //No effect in tests
47 | },
48 | "hljs-title-function": {
49 | color: "#F8F8F2", //No effect in tests
50 | },
51 | "hljs-title-function-invoke": {
52 | color: "#F8F8F2", //No effect in tests
53 | },
54 | "hljs-params": {
55 | color: "#3CEEAE",
56 | },
57 | "hljs-comment": {
58 | color: "#F8F8F2",
59 | },
60 | "hljs-doctag": {
61 | color: "#F8F8F2", //No effect in tests
62 | },
63 |
64 | //Meta
65 |
66 | "hljs-meta": {
67 | color: "#F8F8F2", //No effect in tests
68 | },
69 | "hljs-meta-prompt": {
70 | color: "#F8F8F2", //No effect in tests
71 | },
72 | "hljs-meta-keyword": {
73 | color: "#F8F8F2", //No effect in tests
74 | },
75 | "hljs-meta-string": {
76 | color: "#F8F8F2", //No effect in tests
77 | },
78 |
79 | // //Tags, attributes, configs
80 | "hljs-section": {
81 | color: "#FB3186",
82 | },
83 | "hljs-tag": {
84 | color: "#9D72FF",
85 | },
86 | "hljs-name": {
87 | color: "#9D72FF",
88 | },
89 | "hljs-attr": {
90 | color: "#FB3186",
91 | },
92 | "hljs-attrribute": {
93 | color: "#F8F8F2", //No effect in tests
94 | },
95 |
96 | // // Text Markup
97 | "hljs-bullet": {
98 | color: "#9D72FF",
99 | },
100 | "hljs-code": {
101 | color: "#3CEEAE",
102 | },
103 | "hljs-formula": {
104 | color: "#9D72FF",
105 | },
106 | "hljs-link": {
107 | color: "#9D72FF",
108 | },
109 | "hljs-quote": {
110 | color: "#F8F8F2",
111 | },
112 |
113 | // //CSS
114 |
115 | "hljs-selector-tag": {
116 | color: "#9D72FF",
117 | },
118 | "hljs-selector-id": {
119 | color: "#F8F8F2",
120 | },
121 | "hljs-selector-class": {
122 | color: "#3CEEAE",
123 | },
124 | "hljs-selector-attr": {
125 | color: "#3CEEAE",
126 | },
127 | "hljs-selector-pseudo": {
128 | color: "#F8F8F2",
129 | },
130 |
131 | // //Templates
132 |
133 | "hljs-template-tag": {
134 | color: "#F8F8F2",
135 | },
136 |
137 | "hljs-template-variable": {
138 | color: "#F8F8F2",
139 | },
140 | "hljs-diff": {
141 | color: "#ffff00",
142 | },
143 | "hljs-addition": {
144 | color: "#049d16",
145 | },
146 | "hljs-deletion": {
147 | color: "#aa0202",
148 | },
149 |
150 | hljs: {
151 | display: "block",
152 | overflow: "auto",
153 | color: "#F8F8F2",
154 | backgroundColor: "transparent",
155 | },
156 | "hljs-emphasis": {
157 | fontStyle: "italic",
158 | },
159 | "hljs-strong": {
160 | fontWeight: "bold",
161 | },
162 | };
163 |
--------------------------------------------------------------------------------
/apps/web/src/components/landing-page/tech-stack.tsx:
--------------------------------------------------------------------------------
1 | import Title from "@/components/ui/Title";
2 | import Ripple from "../ui/ripple";
3 | import { Button, buttonVariants } from "../ui/button";
4 | import React from "react";
5 | import { StarFilledIcon } from "@radix-ui/react-icons";
6 | import Link from "next/link";
7 | import { cn } from "@/lib/utils";
8 |
9 | const HoverButton = ({ children }: { children: React.ReactNode }) => {
10 | return (
11 |
14 | );
15 | };
16 |
17 | const TechStack = () => {
18 | return (
19 |
20 |
21 |
22 | Built on leading open-source technologies
23 |
24 |
25 |
26 |
27 |
28 |
35 |
39 |
40 |
41 |
42 |
43 |
48 |
49 |
50 |
51 |
56 |
57 |
58 |
59 |
64 |
65 |
66 |
67 |
72 |
73 |
74 |
75 |
80 |
81 |
82 |
83 |
88 |
89 |
90 |
91 |
101 |
Star on Github
102 |
103 |
104 | {/*
*/}
105 |
106 |
107 |
108 | );
109 | };
110 |
111 | export default TechStack;
112 |
--------------------------------------------------------------------------------
/apps/web/src/components/blog/mdx-content.tsx:
--------------------------------------------------------------------------------
1 | import { useMDXComponent } from "@content-collections/mdx/react";
2 | import type { ImageProps } from "next/image";
3 | import Image from "next/image";
4 | import type { DetailedHTMLProps, ImgHTMLAttributes, JSX } from "react";
5 | import { BlogCodeBlock, BlogCodeBlockSingle } from "./blog-code-block";
6 | import {
7 | BlogList,
8 | BlogListItem,
9 | BlogListNumbered,
10 | type BlogListProps,
11 | } from "./blog-list";
12 | import { BlogQuote, type BlogQuoteProps } from "./blog-quote";
13 | import { ImageWithBlur } from "./image-with-blur";
14 | import { Alert } from "@/components/ui/alert";
15 |
16 | export const MdxComponents = {
17 | Image: (props: ImageProps) =>
18 | props.width || props.fill ? (
19 |
24 | ) : (
25 |
32 | ),
33 | img: (
34 | props: DetailedHTMLProps<
35 | ImgHTMLAttributes,
36 | HTMLImageElement
37 | >
38 | ) => ,
39 | Callout: Alert,
40 | th: (props: JSX.IntrinsicAttributes) => (
41 |
45 | ),
46 | tr: (props: JSX.IntrinsicAttributes) => (
47 |
48 | ),
49 | td: (props: JSX.IntrinsicAttributes) => (
50 |
54 | ),
55 | a: (props: JSX.IntrinsicAttributes) => (
56 |
61 | ),
62 | blockquote: (props: BlogQuoteProps) => BlogQuote(props),
63 | BlogQuote: (props: BlogQuoteProps) => BlogQuote(props),
64 | ol: (props: BlogListProps) => BlogListNumbered(props),
65 | ul: (props: BlogListProps) => BlogList(props),
66 | li: (props: BlogListProps) => BlogListItem(props),
67 | h1: (props: any) => (
68 |
72 | ),
73 | h2: (props: JSX.IntrinsicAttributes) => (
74 |
78 | ),
79 | h3: (props: JSX.IntrinsicAttributes) => (
80 |
84 | ),
85 | h4: (props: JSX.IntrinsicAttributes) => (
86 |
87 | ),
88 | p: (props: JSX.IntrinsicAttributes) => (
89 |
93 | ),
94 | code: (props: JSX.IntrinsicAttributes) => (
95 |
99 | ),
100 | pre: (props: JSX.IntrinsicAttributes) => ,
101 | BlogCodeBlock,
102 | };
103 |
104 | interface MDXProps {
105 | code: string;
106 | }
107 |
108 | export function MDX({ code }: MDXProps) {
109 | const Component = useMDXComponent(code);
110 | return (
111 |
116 | );
117 | }
118 |
--------------------------------------------------------------------------------
/apps/web/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | theme: {
11 | extend: {
12 | keyframes: {
13 | ripple: {
14 | '0%, 100%': {
15 | transform: 'translate(-50%, -50%) scale(1)'
16 | },
17 | '50%': {
18 | transform: 'translate(-50%, -50%) scale(0.9)'
19 | }
20 | },
21 | 'border-beam': {
22 | '100%': {
23 | 'offset-distance': '100%'
24 | }
25 | },
26 | shine: {
27 | from: {
28 | backgroundPosition: '200% 0'
29 | },
30 | to: {
31 | backgroundPosition: '-200% 0'
32 | }
33 | },
34 | 'shiny-text': {
35 | '0%, 90%, 100%': {
36 | 'background-position': 'calc(-100% - var(--shiny-width)) 0'
37 | },
38 | '30%, 60%': {
39 | 'background-position': 'calc(100% + var(--shiny-width)) 0'
40 | }
41 | },
42 | rainbow: {
43 | '0%': {
44 | 'background-position': '0%'
45 | },
46 | '100%': {
47 | 'background-position': '200%'
48 | }
49 | }
50 | },
51 | animation: {
52 | shine: 'shine 8s ease-in-out infinite',
53 | 'shiny-text': 'shiny-text 8s infinite',
54 | rainbow: 'rainbow var(--speed, 2s) infinite linear',
55 | 'border-beam': 'border-beam calc(var(--duration)*1s) infinite linear',
56 | ripple: 'ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite'
57 | },
58 | colors: {
59 | background: 'hsl(var(--background))',
60 | foreground: 'hsl(var(--foreground))',
61 | card: {
62 | DEFAULT: 'hsl(var(--card))',
63 | foreground: 'hsl(var(--card-foreground))'
64 | },
65 | popover: {
66 | DEFAULT: 'hsl(var(--popover))',
67 | foreground: 'hsl(var(--popover-foreground))'
68 | },
69 | primary: {
70 | DEFAULT: 'hsl(var(--primary))',
71 | foreground: 'hsl(var(--primary-foreground))'
72 | },
73 | secondary: {
74 | DEFAULT: 'hsl(var(--secondary))',
75 | foreground: 'hsl(var(--secondary-foreground))'
76 | },
77 | muted: {
78 | DEFAULT: 'hsl(var(--muted))',
79 | foreground: 'hsl(var(--muted-foreground))'
80 | },
81 | accent: {
82 | DEFAULT: 'hsl(var(--accent))',
83 | foreground: 'hsl(var(--accent-foreground))'
84 | },
85 | destructive: {
86 | DEFAULT: 'hsl(var(--destructive))',
87 | foreground: 'hsl(var(--destructive-foreground))'
88 | },
89 | border: 'hsl(var(--border))',
90 | input: 'hsl(var(--input))',
91 | ring: 'hsl(var(--ring))',
92 | chart: {
93 | '1': 'hsl(var(--chart-1))',
94 | '2': 'hsl(var(--chart-2))',
95 | '3': 'hsl(var(--chart-3))',
96 | '4': 'hsl(var(--chart-4))',
97 | '5': 'hsl(var(--chart-5))'
98 | },
99 | 'color-1': 'hsl(var(--color-1))',
100 | 'color-2': 'hsl(var(--color-2))',
101 | 'color-3': 'hsl(var(--color-3))',
102 | 'color-4': 'hsl(var(--color-4))',
103 | 'color-5': 'hsl(var(--color-5))',
104 | sidebar: {
105 | DEFAULT: 'hsl(var(--sidebar-background))',
106 | foreground: 'hsl(var(--sidebar-foreground))',
107 | primary: 'hsl(var(--sidebar-primary))',
108 | 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
109 | accent: 'hsl(var(--sidebar-accent))',
110 | 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
111 | border: 'hsl(var(--sidebar-border))',
112 | ring: 'hsl(var(--sidebar-ring))'
113 | }
114 | },
115 | borderRadius: {
116 | lg: 'var(--radius)',
117 | md: 'calc(var(--radius) - 2px)',
118 | sm: 'calc(var(--radius) - 4px)'
119 | }
120 | }
121 | },
122 | plugins: [
123 | require("tailwindcss-animate"),
124 | require('@tailwindcss/typography'),
125 | ],
126 | };
127 | export default config;
128 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DialogPrimitive from "@radix-ui/react-dialog";
5 | import { Cross2Icon } from "@radix-ui/react-icons";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = DialogPrimitive.Portal;
14 |
15 | const DialogClose = DialogPrimitive.Close;
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ));
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ));
54 | DialogContent.displayName = DialogPrimitive.Content.displayName;
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | );
68 | DialogHeader.displayName = "DialogHeader";
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | );
82 | DialogFooter.displayName = "DialogFooter";
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ));
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | };
123 |
--------------------------------------------------------------------------------
/apps/web/src/components/dashboard/user-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import type React from "react";
5 |
6 | import { ChevronRight, LogOut, User, Sun, Moon, Monitor } from "lucide-react";
7 | import { signOut, useSession } from "next-auth/react";
8 | import { useTheme } from "next-themes";
9 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
10 | import {
11 | DropdownMenu,
12 | DropdownMenuContent,
13 | DropdownMenuCheckboxItem,
14 | DropdownMenuGroup,
15 | DropdownMenuItem,
16 | DropdownMenuSeparator,
17 | DropdownMenuTrigger,
18 | } from "@/components/ui/dropdown-menu";
19 |
20 | type UserButtonProps = {
21 | navbarMode?: boolean;
22 | };
23 |
24 | export const UserButton = ({ navbarMode = false }: UserButtonProps) => {
25 | const { data: currentSession } = useSession();
26 | const { setTheme, theme } = useTheme();
27 |
28 | const router = useRouter();
29 |
30 | if (!currentSession) {
31 | return null;
32 | }
33 |
34 | return (
35 |
36 |
40 |
41 |
42 | {currentSession.user?.image ? (
43 |
47 | ) : null}
48 |
49 | {(currentSession.user?.name ?? "U").slice(0, 2).toUpperCase()}
50 |
51 |
52 |
53 | {!navbarMode ? (
54 | <>
55 |
{currentSession.user?.name}
56 |
57 | >
58 | ) : null}
59 |
60 |
61 |
65 |
66 | {/* */}
67 | setTheme("light")}
70 | className="cursor-pointer"
71 | >
72 |
73 | Light
74 |
75 | {/* */}
76 | {/* */}
77 | setTheme("dark")}
80 | className="cursor-pointer"
81 | >
82 |
83 | Dark
84 |
85 | {/* */}
86 | {/* */}
87 | setTheme("system")}
90 | className="cursor-pointer"
91 | >
92 |
93 | System
94 |
95 | {/* */}
96 |
97 |
98 | router.push("/dashboard/profile")}
100 | className="cursor-pointer"
101 | >
102 |
103 | Profile
104 |
105 |
106 |
107 |
108 |
109 |
110 | {/* */}
111 |
112 | signOut({ callbackUrl: "/" })}
114 | className="cursor-pointer"
115 | >
116 |
117 | Sign out
118 |
119 | {/* */}
120 |
121 |
122 |
123 | );
124 | };
125 |
--------------------------------------------------------------------------------
/apps/server/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
6 | [circleci-url]: https://circleci.com/gh/nestjs/nest
7 |
8 | A progressive Node.js framework for building efficient and scalable server-side applications.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 | ## Description
26 |
27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
28 |
29 | ## Project setup
30 |
31 | ```bash
32 | $ pnpm install
33 | ```
34 |
35 | ## Compile and run the project
36 |
37 | ```bash
38 | # development
39 | $ pnpm run start
40 |
41 | # watch mode
42 | $ pnpm run start:dev
43 |
44 | # production mode
45 | $ pnpm run start:prod
46 | ```
47 |
48 | ## Run tests
49 |
50 | ```bash
51 | # unit tests
52 | $ pnpm run test
53 |
54 | # e2e tests
55 | $ pnpm run test:e2e
56 |
57 | # test coverage
58 | $ pnpm run test:cov
59 | ```
60 |
61 | ## Resources
62 |
63 | Check out a few resources that may come in handy when working with NestJS:
64 |
65 | - Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
66 | - For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
67 | - To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
68 | - Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
69 | - Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
70 | - To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
71 | - Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
72 |
73 | ## Support
74 |
75 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
76 |
77 | ## Stay in touch
78 |
79 | - Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
80 | - Website - [https://nestjs.com](https://nestjs.com/)
81 | - Twitter - [@nestframework](https://twitter.com/nestframework)
82 |
83 | ## License
84 |
85 | Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
86 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🚀 God Tier SaaS - Build the next Enterprise application
2 |
3 |
4 |
5 | A SaaS Kit offering end-to-end type safety with ts-rest between Next.js and Nest.js, along with integrated services like AuthJS for authentication, PostHog for analytics, and Drizzle ORM with PostgreSQL for database management.
6 |
7 | Perfect to build scalable SaaS applications.
8 |
9 | ## Key Features
10 |
11 | - End-to-end Type safety with ts-rest
12 | - Dashboard and Landing Page with NextJS
13 | - Optimistic UI with TanStack Query
14 | - Database management with Drizzle ORM
15 | - Authentication ready with NextAuth.js
16 | - Containerized development environment
17 | - Monorepo structure for efficient code organization
18 | - Analytics with PostHog
19 | - Ready-to-use ContentLayer for blog and changelog
20 |
21 |
22 | ## Tech Stack
23 | - NextJS - Frontend framework
24 | - NestJS - Backend framewor
25 | - ts-rest - End-to-end type safety
26 | - Drizzle ORM - Database management
27 | - NextAuth.js - Authentication
28 | - Docker - Containerization
29 | - PostHog - Analytics
30 | - Content Collections - Blog and changelog
31 |
32 | This stack ensures a robust, scalable, and maintainable application with strong typing throughout the entire codebase and secure authentication.
33 |
34 | ## Project Structure
35 |
36 | - apps/web - The Next.js web application
37 | - apps/server - The Nest.js API server
38 | - packages/shared - The shared module which manages the database and ts-rest router for both the web and Server projects
39 |
40 | ## Setup and Running Instructions
41 |
42 | ### 1. 🐳 Using Docker
43 |
44 | To set up and run the project using Docker Compose:
45 |
46 | 1. Ensure you have Docker and Docker Compose installed on your system.
47 | 2. Open a terminal and navigate to the project's root directory.
48 | 3. Copy the example environment files and add appropriate environment variables:
49 |
50 | ```bash
51 | cp apps/web/.env.example apps/web/.env.local
52 | cp .env.example .env
53 | cp apps/server/.env.example apps/server/.env
54 | cp packages/shared/.env.example packages/shared/.env
55 | ```
56 |
57 | Edit `.env` files and add the necessary environment variables. Follow instructions in each file and modify the values according to your environment and the way you want to run the app.
58 |
59 | 4. Remove all node_modules
60 |
61 | ```bash
62 | rm -rf node_modules .pnpm-store ./apps/web/node_modules ./apps/server/node_modules ./packages/shared/node_modules ./apps/server/dist pg_data
63 | ```
64 |
65 | 5. Run the following command:
66 |
67 | ```bash
68 | docker-compose -f docker-compose.dev.yml up
69 | ```
70 |
71 | This command will build and start all the necessary containers defined in the `docker-compose.dev.yml` file.
72 |
73 | ### 2. 📦 Using pnpm (Local Development)
74 |
75 | To set up and run the project locally using pnpm:
76 |
77 | 1. Make sure you have Node.js (version 18 or higher) and pnpm installed on your system.
78 | 2. Open a terminal and navigate to the project's root directory.
79 | 3. Copy the example environment files and add appropriate environment variables:
80 |
81 | ```bash
82 | cp apps/web/.env.example apps/web/.env.local
83 | cp .env.example .env
84 | cp apps/server/.env.example apps/server/.env
85 | cp packages/shared/.env.example packages/shared/.env
86 | ```
87 |
88 | Edit both `.env` files and add the necessary environment variables. Here docker-compose.postgres.yml used db credentials from .env file. So make sure the credentails that you used in apps.server/.env and packages.shared/.env are same as root .env file. Follow instructions in each file and modify the values according to your environment and the way you want to run the app.
89 |
90 | 4. Install the dependencies by running:
91 |
92 | ```bash
93 | pnpm install
94 | ```
95 |
96 | 5. Build the shared module
97 |
98 | ```bash
99 | pnpm run build:shared
100 | ```
101 |
102 | 6. Start the PostgreSQL database using Docker Compose:
103 |
104 | ```bash
105 | docker-compose -f docker-compose.postgres.yml up -d
106 | ```
107 |
108 | 7. Start the development servers by running:
109 |
110 | ```bash
111 | pnpm run dev
112 | ```
113 |
114 | This command will concurrently start both the Next.js web application (on port 8090) and the NestJS API server with shared module which manages db and ts-rest router. It uses Turbo to manage the monorepo workspace and run the development scripts for both the web and API projects simultaneously.
115 |
116 | Make sure your environment variables are properly configured to connect to the PostgreSQL database started by Docker Compose.
117 |
118 | ## Support
119 |
120 | Support me by giving a star ⭐ on this repository.
121 |
122 | ## License
123 |
124 | This project is licensed under the MIT License.
--------------------------------------------------------------------------------
/apps/web/src/components/ui/link-preview.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
3 | import Image from "next/image";
4 | import { encode } from "qss";
5 | import React from "react";
6 | import {
7 | AnimatePresence,
8 | motion,
9 | useMotionValue,
10 | useSpring,
11 | } from "framer-motion";
12 | import Link from "next/link";
13 | import { cn } from "@/lib/utils";
14 |
15 | type LinkPreviewProps = {
16 | children: React.ReactNode;
17 | url: string;
18 | className?: string;
19 | width?: number;
20 | height?: number;
21 | quality?: number;
22 | layout?: string;
23 | } & (
24 | | { isStatic: true; imageSrc: string }
25 | | { isStatic?: false; imageSrc?: never }
26 | );
27 |
28 | export const LinkPreview = ({
29 | children,
30 | url,
31 | className,
32 | width = 200,
33 | height = 125,
34 | quality = 50,
35 | layout = "fixed",
36 | isStatic = false,
37 | imageSrc = "",
38 | }: LinkPreviewProps) => {
39 | let src;
40 | if (!isStatic) {
41 | const params = encode({
42 | url,
43 | screenshot: true,
44 | meta: false,
45 | embed: "screenshot.url",
46 | colorScheme: "dark",
47 | "viewport.isMobile": true,
48 | "viewport.deviceScaleFactor": 1,
49 | "viewport.width": width * 3,
50 | "viewport.height": height * 3,
51 | });
52 | src = `https://api.microlink.io/?${params}`;
53 | } else {
54 | src = imageSrc;
55 | }
56 |
57 | const [isOpen, setOpen] = React.useState(false);
58 |
59 | const [isMounted, setIsMounted] = React.useState(false);
60 |
61 | React.useEffect(() => {
62 | setIsMounted(true);
63 | }, []);
64 |
65 | const springConfig = { stiffness: 100, damping: 15 };
66 | const x = useMotionValue(0);
67 |
68 | const translateX = useSpring(x, springConfig);
69 |
70 | const handleMouseMove = (event: any) => {
71 | const targetRect = event.target.getBoundingClientRect();
72 | const eventOffsetX = event.clientX - targetRect.left;
73 | const offsetFromCenter = (eventOffsetX - targetRect.width / 2) / 2; // Reduce the effect to make it subtle
74 | x.set(offsetFromCenter);
75 | };
76 |
77 | return (
78 | <>
79 | {isMounted ? (
80 |
81 |
90 |
91 | ) : null}
92 |
93 | {
97 | setOpen(open);
98 | }}
99 | >
100 |
105 | {children}
106 |
107 |
108 |
114 |
115 | {isOpen && (
116 |
134 |
139 |
149 |
150 |
151 | )}
152 |
153 |
154 |
155 | >
156 | );
157 | };
158 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SheetPrimitive from "@radix-ui/react-dialog";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 | import { X } from "lucide-react";
7 |
8 | import { cn } from "@/lib/utils";
9 |
10 | const Sheet = SheetPrimitive.Root;
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger;
13 |
14 | const SheetClose = SheetPrimitive.Close;
15 |
16 | const SheetPortal = SheetPrimitive.Portal;
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ));
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | );
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ));
75 | SheetContent.displayName = SheetPrimitive.Content.displayName;
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | );
89 | SheetHeader.displayName = "SheetHeader";
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | );
103 | SheetFooter.displayName = "SheetFooter";
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ));
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName;
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ));
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName;
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | };
141 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as LabelPrimitive from "@radix-ui/react-label";
5 | import { Slot } from "@radix-ui/react-slot";
6 | import {
7 | Controller,
8 | ControllerProps,
9 | FieldPath,
10 | FieldValues,
11 | FormProvider,
12 | useFormContext,
13 | } from "react-hook-form";
14 |
15 | import { cn } from "@/lib/utils";
16 | import { Label } from "@/components/ui/label";
17 |
18 | const Form = FormProvider;
19 |
20 | type FormFieldContextValue<
21 | TFieldValues extends FieldValues = FieldValues,
22 | TName extends FieldPath = FieldPath,
23 | > = {
24 | name: TName;
25 | };
26 |
27 | const FormFieldContext = React.createContext(
28 | {} as FormFieldContextValue
29 | );
30 |
31 | const FormField = <
32 | TFieldValues extends FieldValues = FieldValues,
33 | TName extends FieldPath = FieldPath,
34 | >({
35 | ...props
36 | }: ControllerProps) => {
37 | return (
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | const useFormField = () => {
45 | const fieldContext = React.useContext(FormFieldContext);
46 | const itemContext = React.useContext(FormItemContext);
47 | const { getFieldState, formState } = useFormContext();
48 |
49 | const fieldState = getFieldState(fieldContext.name, formState);
50 |
51 | if (!fieldContext) {
52 | throw new Error("useFormField should be used within ");
53 | }
54 |
55 | const { id } = itemContext;
56 |
57 | return {
58 | id,
59 | name: fieldContext.name,
60 | formItemId: `${id}-form-item`,
61 | formDescriptionId: `${id}-form-item-description`,
62 | formMessageId: `${id}-form-item-message`,
63 | ...fieldState,
64 | };
65 | };
66 |
67 | type FormItemContextValue = {
68 | id: string;
69 | };
70 |
71 | const FormItemContext = React.createContext(
72 | {} as FormItemContextValue
73 | );
74 |
75 | const FormItem = React.forwardRef<
76 | HTMLDivElement,
77 | React.HTMLAttributes
78 | >(({ className, ...props }, ref) => {
79 | const id = React.useId();
80 |
81 | return (
82 |
83 |
84 |
85 | );
86 | });
87 | FormItem.displayName = "FormItem";
88 |
89 | const FormLabel = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => {
93 | const { error, formItemId } = useFormField();
94 |
95 | return (
96 |
102 | );
103 | });
104 | FormLabel.displayName = "FormLabel";
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } =
111 | useFormField();
112 |
113 | return (
114 |
125 | );
126 | });
127 | FormControl.displayName = "FormControl";
128 |
129 | const FormDescription = React.forwardRef<
130 | HTMLParagraphElement,
131 | React.HTMLAttributes
132 | >(({ className, ...props }, ref) => {
133 | const { formDescriptionId } = useFormField();
134 |
135 | return (
136 |
142 | );
143 | });
144 | FormDescription.displayName = "FormDescription";
145 |
146 | const FormMessage = React.forwardRef<
147 | HTMLParagraphElement,
148 | React.HTMLAttributes
149 | >(({ className, children, ...props }, ref) => {
150 | const { error, formMessageId } = useFormField();
151 | const body = error ? String(error?.message) : children;
152 |
153 | if (!body) {
154 | return null;
155 | }
156 |
157 | return (
158 |
164 | {body}
165 |
166 | );
167 | });
168 | FormMessage.displayName = "FormMessage";
169 |
170 | export {
171 | useFormField,
172 | Form,
173 | FormItem,
174 | FormLabel,
175 | FormControl,
176 | FormDescription,
177 | FormMessage,
178 | FormField,
179 | };
180 |
--------------------------------------------------------------------------------
/apps/web/src/components/landing-page/hero.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import Link from "next/link";
3 | import { Button, buttonVariants } from "@/components/ui/button";
4 | import { ChevronRight } from "lucide-react";
5 | import { ArrowRightIcon } from "@radix-ui/react-icons";
6 | import AnimatedShinyText from "../ui/animated-shiny-text";
7 | import { LinkPreview } from "../ui/link-preview";
8 | import { GitHubLogoIcon } from "@radix-ui/react-icons";
9 | import { HeroVideoDialog } from "../ui/hero-video-dialog";
10 | import Title from "../ui/Title";
11 | import Subtitle from "../ui/Subtitle";
12 |
13 | const Hero = () => {
14 | return (
15 |
16 |
17 |
18 |
25 |
26 |
27 | 🎉 Introducing God Tier SaaS
28 |
29 |
30 |
31 |
32 |
33 |
Build the next Enterprise application
34 |
35 |
36 | {/*
*/}
37 |
38 | A SaaS Kit offering end-to-end type safety with{" "}
39 | ts-rest {" "}
40 | between Next.js {" "}
41 | and Nest.js ,{" "}
42 | along with integrated services like{" "}
43 | AuthJS for
44 | authentication,{" "}
45 | PostHog for
46 | analytics, and{" "}
47 |
48 | Drizzle ORM
49 | {" "}
50 | with PostgreSQL for database management.
51 |
52 | Perfect to build scalable SaaS applications.
53 |
54 |
55 | {/*
*/}
56 |
57 |
58 |
59 | {/* Start Building */}
60 |
70 | Start Building
71 |
72 |
83 |
84 | View on GitHub
85 |
86 |
87 |
88 |
89 |
90 |
91 | {/* Video Dialog */}
92 |
93 |
100 |
107 |
108 |
109 | );
110 | };
111 |
112 | export default Hero;
113 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/hero-video-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { AnimatePresence, motion } from "framer-motion";
5 | import { Play, XIcon } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | type AnimationStyle =
10 | | "from-bottom"
11 | | "from-center"
12 | | "from-top"
13 | | "from-left"
14 | | "from-right"
15 | | "fade"
16 | | "top-in-bottom-out"
17 | | "left-in-right-out";
18 |
19 | interface HeroVideoProps {
20 | animationStyle?: AnimationStyle;
21 | videoSrc: string;
22 | thumbnailSrc: string;
23 | thumbnailAlt?: string;
24 | className?: string;
25 | }
26 |
27 | const animationVariants = {
28 | "from-bottom": {
29 | initial: { y: "100%", opacity: 0 },
30 | animate: { y: 0, opacity: 1 },
31 | exit: { y: "100%", opacity: 0 },
32 | },
33 | "from-center": {
34 | initial: { scale: 0.5, opacity: 0 },
35 | animate: { scale: 1, opacity: 1 },
36 | exit: { scale: 0.5, opacity: 0 },
37 | },
38 | "from-top": {
39 | initial: { y: "-100%", opacity: 0 },
40 | animate: { y: 0, opacity: 1 },
41 | exit: { y: "-100%", opacity: 0 },
42 | },
43 | "from-left": {
44 | initial: { x: "-100%", opacity: 0 },
45 | animate: { x: 0, opacity: 1 },
46 | exit: { x: "-100%", opacity: 0 },
47 | },
48 | "from-right": {
49 | initial: { x: "100%", opacity: 0 },
50 | animate: { x: 0, opacity: 1 },
51 | exit: { x: "100%", opacity: 0 },
52 | },
53 | fade: {
54 | initial: { opacity: 0 },
55 | animate: { opacity: 1 },
56 | exit: { opacity: 0 },
57 | },
58 | "top-in-bottom-out": {
59 | initial: { y: "-100%", opacity: 0 },
60 | animate: { y: 0, opacity: 1 },
61 | exit: { y: "100%", opacity: 0 },
62 | },
63 | "left-in-right-out": {
64 | initial: { x: "-100%", opacity: 0 },
65 | animate: { x: 0, opacity: 1 },
66 | exit: { x: "100%", opacity: 0 },
67 | },
68 | };
69 |
70 | export function HeroVideoDialog({
71 | animationStyle = "from-center",
72 | videoSrc,
73 | thumbnailSrc,
74 | thumbnailAlt = "Video thumbnail",
75 | className,
76 | }: HeroVideoProps) {
77 | const [isVideoOpen, setIsVideoOpen] = useState(false);
78 | const selectedAnimation = animationVariants[animationStyle];
79 |
80 | return (
81 |
82 |
setIsVideoOpen(true)}
85 | >
86 |
93 |
108 |
109 |
110 | {isVideoOpen && (
111 | setIsVideoOpen(false)}
115 | exit={{ opacity: 0 }}
116 | className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-md"
117 | >
118 |
123 |
124 |
125 |
126 |
127 |
133 |
134 |
135 |
136 | )}
137 |
138 |
139 | );
140 | }
141 |
--------------------------------------------------------------------------------
/apps/web/src/components/posts/create-post.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Button } from "@/components/ui/button";
3 | import { useState } from "react";
4 | import {
5 | Dialog,
6 | DialogContent,
7 | DialogHeader,
8 | DialogTitle,
9 | } from "@/components/ui/dialog";
10 | import { Input } from "@/components/ui/input";
11 | import { Textarea } from "@/components/ui/textarea";
12 | import { zodResolver } from "@hookform/resolvers/zod";
13 | import { useForm } from "react-hook-form";
14 | import { z } from "zod";
15 | import {
16 | Form,
17 | FormControl,
18 | FormDescription,
19 | FormField,
20 | FormItem,
21 | FormLabel,
22 | FormMessage,
23 | } from "@/components/ui/form";
24 | import { api } from "@/lib/api-client";
25 | import { toast } from "sonner";
26 |
27 | const formSchema = z.object({
28 | title: z.string().min(2, {
29 | message: "title must be at least 2 characters.",
30 | }),
31 | body: z.string().min(2, {
32 | message: "body must be at least 2 characters.",
33 | }),
34 | });
35 |
36 | const CreatePost = () => {
37 | const queryClient = api.useQueryClient();
38 | const form = useForm>({
39 | resolver: zodResolver(formSchema),
40 | defaultValues: {
41 | title: "",
42 | body: "",
43 | },
44 | });
45 | const [isDialogOpen, setIsDialogOpen] = useState(false);
46 | const { mutate } = api.posts.createPost.useMutation({
47 | onMutate: async (data) => {
48 | // get current posts, so we can reset back to it if the mutation fails
49 | const previousData = queryClient.posts.getPosts.getQueryData(["posts"]);
50 |
51 | const optimisticPost = {
52 | ...data.body,
53 | createdAt: new Date().toString(),
54 | id: crypto.randomUUID(),
55 | };
56 |
57 | // optimistically update the cache with the new post
58 | queryClient.posts.getPosts.setQueryData(["posts"], (oldData) => {
59 | if (oldData?.status === 200) {
60 | return {
61 | ...oldData,
62 | body: [...oldData?.body, optimisticPost],
63 | };
64 | }
65 | return oldData;
66 | });
67 |
68 | return { previousData };
69 | },
70 | onError(error, _variables, context) {
71 | toast.error("Failed to create post");
72 | queryClient.posts.getPosts.setQueryData(["posts"], context?.previousData);
73 | },
74 | onSettled() {
75 | // trigger a refetch regardless of it the mutation was successful or not
76 | queryClient.refetchQueries({ queryKey: ["posts"] });
77 | },
78 | });
79 |
80 | const handleOpenDialog = (openVal: boolean) => {
81 | setIsDialogOpen(openVal);
82 | form.reset();
83 | };
84 |
85 | function onSubmit(values: z.infer) {
86 | console.log(values);
87 | mutate(
88 | { body: values },
89 | {
90 | onSuccess() {
91 | toast.success("Post created successfully");
92 | },
93 | }
94 | );
95 | setIsDialogOpen(false);
96 | form.reset();
97 | }
98 |
99 | return (
100 |
101 |
setIsDialogOpen(true)}>
102 | Create Post
103 |
104 |
105 |
106 |
107 | Create a New Post
108 |
109 |
110 |
152 |
153 |
154 |
155 |
156 |
157 | );
158 | };
159 |
160 | export default CreatePost;
161 |
--------------------------------------------------------------------------------