diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2e0f5a0 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +# Make sure to override these in deployment +DATABASE_URL=postgresql://postgres:@localhost:5432/next-prisma-starter-new diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..8c54e0e --- /dev/null +++ b/.eslintrc @@ -0,0 +1,25 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": [ + "plugin:@typescript-eslint/recommended-type-checked", + "plugin:@typescript-eslint/stylistic-type-checked", + "plugin:react/recommended", + "plugin:react-hooks/recommended" + ], + "reportUnusedDisableDirectives": true, + "parserOptions": { + "project": "tsconfig.json", + "ecmaVersion": 2018, + "sourceType": "module" + }, + "rules": { + "@typescript-eslint/consistent-type-definitions": "off", + "react/react-in-jsx-scope": "off", + "react/prop-types": "off" + }, + "settings": { + "react": { + "version": "detect" + } + } +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..f1a74c5 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: barbarbar338 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..82895be --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..bb139df --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,106 @@ +name: Testing +on: [push] +jobs: + e2e: + name: "Build + E2E tests" + services: + postgres: + image: postgres + env: + POSTGRES_DATABASE: trpcdb + POSTGRES_USER: postgres + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - 5432:5432 + env: + NODE_ENV: test + NEXTAUTH_SECRET: supersecret + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: pnpm/action-setup@v2 + with: + version: 8.5.1 + + - uses: actions/setup-node@v3 + with: + node-version: 20.x + cache: "pnpm" # You can active this cache when your repo has a lockfile + + - name: Install deps (with cache) + run: pnpm install + + - name: Install playwright + run: pnpm playwright install chromium + + - name: Next.js cache + uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/.next/cache + key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-nextjs + + - name: Setup Prisma + run: pnpm prebuild + + - run: pnpm build + - run: pnpm test-e2e + + - name: Check types + run: pnpm tsc + + - name: Upload test results + if: ${{ always() }} + uses: actions/upload-artifact@v2 + with: + name: test results + path: | + playwright/test-results + + unit: + name: "Unit tests + typecheck" + services: + postgres: + image: postgres + env: + POSTGRES_DATABASE: trpcdb + POSTGRES_USER: postgres + POSTGRES_HOST_AUTH_METHOD: trust + ports: + - 5432:5432 + env: + NODE_ENV: test + NEXTAUTH_SECRET: supersecret + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: pnpm/action-setup@v2 + with: + version: 8.5.1 + + - uses: actions/setup-node@v3 + with: + node-version: 20.x + cache: "pnpm" # You can active this cache when your repo has a lockfile + + - name: Install deps (with cache) + run: pnpm install + + - name: Next.js cache + uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/.next/cache + key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-nextjs + + - name: Setup Prisma + run: pnpm prebuild + + - run: pnpm test-unit + - run: pnpm tsc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62f2ebf --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +tsconfig.tsbuildinfo +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel + +*.db +*.db-journal + + +# testing +playwright/test-results + +# environment +*.env* +!.env.example + +# prisma +migrations diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..6c59086 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +enable-pre-post-scripts=true diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..bc4c2f6 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "tabWidth": 4, + "useTabs": true, + "bracketSpacing": true, + "singleQuote": false, + "endOfLine": "crlf", + "trailingComma": "all", + "semi": true +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..37b2e84 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "prisma.prisma" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4d360cb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/README.md b/README.md index 3b24869..cf348b3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ -# 🖥️ HastePaste App Website +# 🖥️ HastePaste -> Tried to make a free to use service for everyone but failed. It was a great experience to serve millions of people but it was not possible to continue it for free. Project gets more expensive as it grows. It was hard to find a sponsor for this project, everyone wanted to use it for free but no one wanted to pay for its expenses. It was afforable for me for a while but when the database and requests increased, everything became more expensive. It was a great experience to serve a free to use service, thanks. -> -> ~barbarbar338 +A simple pastebin service made with NextJS, TRPC, Prisma, PostgreSQL and TailwindCSS. -Note: this is an old version of hastepaste, feel free to use it for your own projects. +Accessible at [hastepaste.xyz](https://hastepaste.xyz) diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..8e30b2d --- /dev/null +++ b/next.config.js @@ -0,0 +1,22 @@ +// @ts-check +/* eslint-disable @typescript-eslint/no-var-requires */ +const { env } = require("./src/server/env"); + +/** + * @template {import("next").NextConfig} T + * @param {T} config + * @constraint {{import("next").NextConfig}} + */ +function getConfig(config) { + return config; +} + +module.exports = getConfig({ + publicRuntimeConfig: { + NODE_ENV: env.NODE_ENV, + }, + eslint: { ignoreDuringBuilds: !!process.env.CI }, + typescript: { + ignoreBuildErrors: true, + }, +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..2fd6216 --- /dev/null +++ b/package.json @@ -0,0 +1,72 @@ +{ + "name": "hastepaste", + "version": "3.0.0-alpha.1", + "private": true, + "publishConfig": { + "access": "restricted" + }, + "prisma": { + "seed": "tsx prisma/seed.ts" + }, + "scripts": { + "generate": "prisma generate", + "prisma-studio": "prisma studio", + "db-seed": "prisma db seed", + "db-reset": "prisma migrate dev reset", + "dx:next": "run-s migrate-dev db-seed && next dev", + "dx:prisma-studio": "yarn prisma-studio", + "dx": "run-p dx:* --print-label", + "dev": "yarn dx:next", + "prebuild": "run-s generate migrate", + "build": "next build", + "start": "next start", + "lint": "eslint --cache --ext \".js,.ts,.tsx\" src", + "lint-fix": "yarn lint --fix", + "migrate-dev": "prisma migrate dev", + "migrate": "prisma migrate deploy", + "test-unit": "vitest", + "test-e2e": "playwright test", + "test-start": "run-s test-unit test-e2e", + "postinstall": "yarn generate", + "format": "prettier --write .", + "update": "taze latest -w" + }, + "dependencies": { + "@prisma/client": "^5.13.0", + "@tanstack/react-query": "^5.32.1", + "@trpc/client": "npm:@trpc/client@next", + "@trpc/next": "npm:@trpc/next@next", + "@trpc/react-query": "npm:@trpc/react-query@next", + "@trpc/server": "npm:@trpc/server@next", + "clsx": "^2.1.1", + "next": "^14.2.3", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "superjson": "^2.2.1", + "zod": "^3.23.6" + }, + "devDependencies": { + "@playwright/test": "^1.43.1", + "@types/node": "^20.12.8", + "@types/react": "^18.3.1", + "@typescript-eslint/eslint-plugin": "^7.8.0", + "@typescript-eslint/parser": "^7.8.0", + "autoprefixer": "^10.4.19", + "dotenv": "^16.4.5", + "eslint": "^9.2.0", + "eslint-config-next": "^14.2.3", + "eslint-plugin-react": "^7.34.1", + "eslint-plugin-react-hooks": "^4.6.2", + "npm-run-all": "^4.1.5", + "postcss": "^8.4.38", + "prettier": "^3.2.5", + "prisma": "^5.13.0", + "start-server-and-test": "^2.0.3", + "tailwindcss": "^3.4.3", + "taze": "^0.13.8", + "tsx": "^4.9.0", + "typescript": "^5.4.5", + "vite": "^5.2.11", + "vitest": "^1.6.0" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..5c91862 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,24 @@ +import { PlaywrightTestConfig, devices } from "@playwright/test"; + +const opts = { + headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS, +}; +const config: PlaywrightTestConfig = { + testDir: "./playwright", + timeout: 35e3, + outputDir: "./playwright/test-results", + reporter: process.env.CI ? "github" : "list", + use: { + ...devices["Desktop Chrome"], + headless: opts.headless, + video: "on", + }, + retries: process.env.CI ? 3 : 0, + webServer: { + command: process.env.CI ? "npm run start" : "npm run dev", + reuseExistingServer: Boolean(process.env.TEST_LOCAL === "1"), + port: 3000, + }, +}; + +export default config; diff --git a/playwright/smoke.test.ts b/playwright/smoke.test.ts new file mode 100644 index 0000000..eb941a1 --- /dev/null +++ b/playwright/smoke.test.ts @@ -0,0 +1,22 @@ +import { test } from "@playwright/test"; + +test.setTimeout(35e3); + +test("go to /", async ({ page }) => { + await page.goto("/"); + + await page.waitForSelector(`text=Starter`); +}); + +test("add a post", async ({ page }) => { + const nonce = `${Math.random()}`; + + await page.goto("/"); + await page.fill(`[name=title]`, nonce); + await page.fill(`[name=text]`, nonce); + await page.click(`form [type=submit]`); + await page.waitForLoadState("networkidle"); + await page.reload(); + + await page.waitForSelector(`text="${nonce}"`); +}); diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..e873f1a --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..ca58799 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,17 @@ +datasource db { + provider = "postgres" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" +} + +model Post { + id String @id @default(uuid()) + title String + text String + + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..83c8e39 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,27 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +async function main() { + const firstPostId = "5c03994c-fc16-47e0-bd02-d218a370a078"; + await prisma.post.upsert({ + where: { + id: firstPostId, + }, + create: { + id: firstPostId, + title: "First Post", + text: "This is an example post generated from `prisma/seed.ts`", + }, + update: {}, + }); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..4965832 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/vercel.svg b/public/vercel.svg new file mode 100644 index 0000000..fbf0e25 --- /dev/null +++ b/public/vercel.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/src/components/DefaultLayout.tsx b/src/components/DefaultLayout.tsx new file mode 100644 index 0000000..dca936b --- /dev/null +++ b/src/components/DefaultLayout.tsx @@ -0,0 +1,17 @@ +import Head from "next/head"; +import type { ReactNode } from "react"; + +type DefaultLayoutProps = { children: ReactNode }; + +export const DefaultLayout = ({ children }: DefaultLayoutProps) => { + return ( + <> + + Prisma Starter + + + +
{children}
+ + ); +}; diff --git a/src/pages/404.tsx b/src/pages/404.tsx new file mode 100644 index 0000000..196d55f --- /dev/null +++ b/src/pages/404.tsx @@ -0,0 +1,15 @@ +import Link from "next/link"; + +export default function Custom404() { + return ( +
+ + Home + +
+

404

+

Page not found

+
+
+ ); +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx new file mode 100644 index 0000000..734c265 --- /dev/null +++ b/src/pages/_app.tsx @@ -0,0 +1,28 @@ +import type { NextPage } from "next"; +import type { AppType, AppProps } from "next/app"; +import type { ReactElement, ReactNode } from "react"; + +import { DefaultLayout } from "~/components/DefaultLayout"; +import { trpc } from "~/utils/trpc"; +import "~/styles/globals.css"; + +export type NextPageWithLayout< + TProps = Record, + TInitialProps = TProps, +> = NextPage & { + getLayout?: (page: ReactElement) => ReactNode; +}; + +type AppPropsWithLayout = AppProps & { + Component: NextPageWithLayout; +}; + +const MyApp = (({ Component, pageProps }: AppPropsWithLayout) => { + const getLayout = + Component.getLayout ?? + ((page) => {page}); + + return getLayout(); +}) as AppType; + +export default trpc.withTRPC(MyApp); diff --git a/src/pages/api/trpc/[trpc].ts b/src/pages/api/trpc/[trpc].ts new file mode 100644 index 0000000..0736420 --- /dev/null +++ b/src/pages/api/trpc/[trpc].ts @@ -0,0 +1,14 @@ +import * as trpcNext from "@trpc/server/adapters/next"; +import { createContext } from "~/server/context"; +import { appRouter } from "~/server/routers/_app"; + +export default trpcNext.createNextApiHandler({ + router: appRouter, + createContext, + onError({ error }) { + if (error.code === "INTERNAL_SERVER_ERROR") { + // send to bug reporting + console.error("Something went wrong", error); + } + }, +}); diff --git a/src/pages/index.tsx b/src/pages/index.tsx new file mode 100644 index 0000000..b700379 --- /dev/null +++ b/src/pages/index.tsx @@ -0,0 +1,155 @@ +import { trpc } from "../utils/trpc"; +import type { NextPageWithLayout } from "./_app"; +import type { inferProcedureInput } from "@trpc/server"; +import Link from "next/link"; +import { Fragment } from "react"; +import type { AppRouter } from "~/server/routers/_app"; + +const IndexPage: NextPageWithLayout = () => { + const utils = trpc.useUtils(); + const postsQuery = trpc.post.list.useInfiniteQuery( + { + limit: 5, + }, + { + getNextPageParam(lastPage) { + return lastPage.nextCursor; + }, + }, + ); + + const addPost = trpc.post.add.useMutation({ + async onSuccess() { + await utils.post.list.invalidate(); + }, + }); + + return ( +
+

+ Welcome to your tRPC with Prisma starter! +

+

+ If you get stuck, check{" "} + + the docs + + , write a message in our{" "} + + Discord-channel + + , or write a message in{" "} + + GitHub Discussions + + . +

+ +
+
+

+ Latest Posts + {postsQuery.status === "pending" && "(loading)"} +

+ + + + {postsQuery.data?.pages.map((page, index) => ( + + {page.items.map((item) => ( +
+

+ {item.title} +

+ + View more + +
+ ))} +
+ ))} +
+ +
+ +
+

Add a Post

+ +
{ + e.preventDefault(); + const $form = e.currentTarget; + const values = Object.fromEntries(new FormData($form)); + type Input = inferProcedureInput< + AppRouter["post"]["add"] + >; + const input: Input = { + title: values.title as string, + text: values.text as string, + }; + try { + await addPost.mutateAsync(input); + + $form.reset(); + } catch (cause) { + console.error({ cause }, "Failed to add post"); + } + }} + > +
+ +