diff --git a/.eslintignore b/.eslintignore index 199b0e5673faf..446dd6cde78a3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ .babelrc.js +apps/changelog diff --git a/app/changelog/feed.xml/route.ts b/app/changelog/feed.xml/route.ts index 178632ba4680f..7c6644c3b3820 100644 --- a/app/changelog/feed.xml/route.ts +++ b/app/changelog/feed.xml/route.ts @@ -20,12 +20,14 @@ export async function GET() { allChangelogs.map(changelog => { return feed.item({ title: changelog.title, + // @ts-expect-error TODO(lforst): This is broken for some reason description: changelog.summary, url: `https://sentry.io/changelog/${changelog.slug}`, categories: changelog.categories.map(category => { return category.name; }) || [], + // @ts-expect-error TODO(lforst): This is broken for some reason date: changelog.publishedAt, }); }); diff --git a/apps/changelog/.env.example b/apps/changelog/.env.example new file mode 100644 index 0000000000000..14daf94f70fa6 --- /dev/null +++ b/apps/changelog/.env.example @@ -0,0 +1,8 @@ +# rename this file to .env and supply the values listed below +# also make sure they are available to the build tool (e.g. Vercel/Netlify) +# warning: variables prefixed with NEXT_PUBLIC_ will be made available to client-side code +# be careful not to expose sensitive data + +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/changelog +NEXTAUTH_URL=http://localhost:3000 +NEXTAUTH_SECRET=secret diff --git a/apps/changelog/.gitignore b/apps/changelog/.gitignore new file mode 100644 index 0000000000000..99bc887ddec0a --- /dev/null +++ b/apps/changelog/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# dotenv environment variables file +.env +.env.development diff --git a/apps/changelog/README.md b/apps/changelog/README.md new file mode 100644 index 0000000000000..234c6d154f643 --- /dev/null +++ b/apps/changelog/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +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. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +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. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/apps/changelog/docker-compose.yml b/apps/changelog/docker-compose.yml new file mode 100644 index 0000000000000..aa1b61124ad29 --- /dev/null +++ b/apps/changelog/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3.7' +services: + postgres: + container_name: changelog_postgres + image: postgres:latest + restart: always + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=postgres + ports: + - '5432:5432' + volumes: + - postgres_data:/var/lib/postgresql/data/ +volumes: + postgres_data: diff --git a/apps/changelog/next.config.mjs b/apps/changelog/next.config.mjs new file mode 100644 index 0000000000000..d98ec6db1c92e --- /dev/null +++ b/apps/changelog/next.config.mjs @@ -0,0 +1,11 @@ +import {withSentryConfig} from '@sentry/nextjs'; + +const nextConfig = { + trailingSlash: true, + eslint: { + ignoreDuringBuilds: true, + }, + transpilePackages: ['next-mdx-remote'], +}; + +export default withSentryConfig(nextConfig); diff --git a/apps/changelog/package.json b/apps/changelog/package.json new file mode 100644 index 0000000000000..fe47e4e6b1b69 --- /dev/null +++ b/apps/changelog/package.json @@ -0,0 +1,58 @@ +{ + "name": "sentry-changelog", + "version": "1.0.0", + "description": "The Sentry changelog application", + "main": "index.js", + "repository": "https://github.com/getsentry/sentry-docs", + "author": "getsentry", + "license": "FSL-1.1-Apache-2.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "migrate:dev": "dotenv -e .env.development -- yarn prisma migrate reset" + }, + "dependencies": { + "rehype-prism-plus": "^1.6.3", + "rehype-slug": "^6.0.0", + "@auth/prisma-adapter": "^1.2.0", + "nextjs-toploader": "^1.6.6", + "prism-sentry": "^1.0.2", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-toolbar": "^1.0.4", + "@radix-ui/themes": "^2.0.3", + "@sentry/nextjs": "^8.8.0", + "@google-cloud/storage": "^7.7.0", + "@prisma/client": "^5.8.1", + "next": "^14.2.5", + "next-auth": "^4.24.5", + "next-mdx-remote": "^4.4.1", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-select": "^5.7.3", + "sass": "^1.69.5", + "react-textarea-autosize": "^8.5.3", + "rss": "^1.2.2", + "textarea-markdown-editor": "^1.0.4" + }, + "devDependencies": { + "autoprefixer": "^10.4.17", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18.3.0", + "prisma": "^5.8.1", + "@types/rss": "^0.0.32", + "eslint": "^8", + "eslint-config-next": "^14.2.5", + "postcss": "^8.4.33", + "@tailwindcss/forms": "^0.5.7", + "tailwindcss": "^3.4.1", + "@tailwindcss/typography": "^0.5.10", + "typescript": "^5" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/apps/changelog/postcss.config.mjs b/apps/changelog/postcss.config.mjs new file mode 100644 index 0000000000000..755dcec3fcd94 --- /dev/null +++ b/apps/changelog/postcss.config.mjs @@ -0,0 +1,11 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + "postcss-import": {}, + "tailwindcss/nesting": {}, + tailwindcss: {}, + autoprefixer: {}, + }, +}; + +export default config; diff --git a/apps/changelog/prisma/migrations/0_init/migration.sql b/apps/changelog/prisma/migrations/0_init/migration.sql new file mode 100644 index 0000000000000..9688e9373bd52 --- /dev/null +++ b/apps/changelog/prisma/migrations/0_init/migration.sql @@ -0,0 +1,121 @@ +-- CreateTable +CREATE TABLE "Account" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refresh_token" TEXT, + "access_token" TEXT, + "expires_at" INTEGER, + "token_type" TEXT, + "scope" TEXT, + "id_token" TEXT, + "session_state" TEXT, + + CONSTRAINT "Account_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL, + "sessionToken" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "name" TEXT, + "email" TEXT, + "emailVerified" TIMESTAMP(3), + "image" TEXT, + "admin" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VerificationToken" ( + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL +); + +-- CreateTable +CREATE TABLE "Changelog" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "publishedAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "title" VARCHAR(255) NOT NULL, + "slug" VARCHAR(255) NOT NULL, + "image" TEXT, + "content" TEXT, + "summary" TEXT, + "published" BOOLEAN NOT NULL DEFAULT false, + "deleted" BOOLEAN NOT NULL DEFAULT false, + "authorId" TEXT, + + CONSTRAINT "Changelog_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Category" ( + "id" TEXT NOT NULL, + "name" VARCHAR(255) NOT NULL, + "deleted" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "Category_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_CategoryToChangelog" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); + +-- CreateIndex +CREATE UNIQUE INDEX "Changelog_slug_key" ON "Changelog"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "Category_name_key" ON "Category"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "_CategoryToChangelog_AB_unique" ON "_CategoryToChangelog"("A", "B"); + +-- CreateIndex +CREATE INDEX "_CategoryToChangelog_B_index" ON "_CategoryToChangelog"("B"); + +-- AddForeignKey +ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Changelog" ADD CONSTRAINT "Changelog_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_CategoryToChangelog" ADD CONSTRAINT "_CategoryToChangelog_A_fkey" FOREIGN KEY ("A") REFERENCES "Category"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_CategoryToChangelog" ADD CONSTRAINT "_CategoryToChangelog_B_fkey" FOREIGN KEY ("B") REFERENCES "Changelog"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/changelog/prisma/schema.prisma b/apps/changelog/prisma/schema.prisma new file mode 100644 index 0000000000000..ffd451e9cbb61 --- /dev/null +++ b/apps/changelog/prisma/schema.prisma @@ -0,0 +1,80 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["fullTextSearch", "tracing"] +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) +} + +model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + accounts Account[] + sessions Session[] + changelogs Changelog[] + admin Boolean @default(false) +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) +} + +model Changelog { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + publishedAt DateTime? @default(now()) + updatedAt DateTime @updatedAt + title String @db.VarChar(255) + slug String @unique @db.VarChar(255) + image String? + content String? + summary String? + published Boolean @default(false) + deleted Boolean @default(false) + authorId String? + author User? @relation(fields: [authorId], references: [id]) + categories Category[] +} + +model Category { + id String @id @default(cuid()) + name String @unique @db.VarChar(255) + deleted Boolean @default(false) + changelogs Changelog[] +} diff --git a/apps/changelog/prisma/seed/seed.mjs b/apps/changelog/prisma/seed/seed.mjs new file mode 100644 index 0000000000000..052f2b50ce3e5 --- /dev/null +++ b/apps/changelog/prisma/seed/seed.mjs @@ -0,0 +1,84 @@ +import {PrismaClient} from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function seed() { + try { + // Create changelogs + const changelog1 = await prisma.changelog.create({ + data: { + id: '1', + createdAt: new Date(), + publishedAt: new Date(), + updatedAt: new Date(), + title: 'Changelog 1', + slug: 'changelog-1', + content: 'Changelog 1 content', + summary: 'Changelog 1 summary', + published: true, + deleted: false, + }, + }); + + const changelog2 = await prisma.changelog.create({ + data: { + id: '2', + createdAt: new Date(), + publishedAt: new Date(), + updatedAt: new Date(), + title: 'Changelog 2', + slug: 'changelog-2', + content: 'Changelog 2 content', + summary: 'Changelog 2 summary', + published: true, + deleted: false, + }, + }); + + const changelog3 = await prisma.changelog.create({ + data: { + id: '3', + createdAt: new Date('01/01/2020'), + publishedAt: new Date('01/01/2020'), + updatedAt: new Date('01/01/2020'), + title: 'Changelog 3', + slug: 'changelog-3', + content: 'Changelog 3 content with [markdown content](https://de.wikipedia.org/wiki/Markdown)', + summary: 'Changelog 3 summary with [markdown content](https://de.wikipedia.org/wiki/Markdown)', + published: true, + deleted: false, + }, + }); + + // Create categories + await prisma.category.create({ + data: { + id: '1', + name: 'Category 1', + deleted: false, + changelogs: { + connect: [{id: changelog1.id}], + }, + }, + }); + + await prisma.category.create({ + data: { + id: '2', + name: 'Category 2', + deleted: false, + changelogs: { + connect: [{id: changelog2.id}, {id: changelog3.id}], + }, + }, + }); + + console.log('Seed data created successfully!'); + } catch (error) { + console.error('Error seeding data:', error); + } finally { + await prisma.$disconnect(); + } +} + +seed(); diff --git a/apps/changelog/src/app/api/auth/[...nextauth]/route.ts b/apps/changelog/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000000000..5043aed46f01b --- /dev/null +++ b/apps/changelog/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,6 @@ +import {authOptions} from '@/server/authOptions'; +import NextAuth from 'next-auth'; + +const handler = NextAuth(authOptions); + +export {handler as GET, handler as POST}; diff --git a/apps/changelog/src/app/changelog/%5Fadmin/[id]/edit/page.tsx b/apps/changelog/src/app/changelog/%5Fadmin/[id]/edit/page.tsx new file mode 100644 index 0000000000000..3b19096a140f2 --- /dev/null +++ b/apps/changelog/src/app/changelog/%5Fadmin/[id]/edit/page.tsx @@ -0,0 +1,96 @@ +import {Fragment, Suspense} from 'react'; +import Link from 'next/link'; + +import {prismaClient} from '@/server/prisma-client'; +import {editChangelog} from '@/server/actions/changelog'; +import {TitleSlug} from '@/client/components/titleSlug'; +import {FileUpload} from '@/client/components/fileUpload'; +import {Select} from '@/client/components/ui/Select'; +import {ForwardRefEditor} from '@/client/components/forwardRefEditor'; +import {Button} from '@/client/components/ui/Button'; + +export default async function ChangelogCreatePage({params}: {params: {id: string}}) { + const categories = await prismaClient.category.findMany({ + orderBy: { + name: 'asc', + }, + }); + const changelog = await prismaClient.changelog.findUnique({ + where: {id: params.id}, + include: { + author: true, + categories: true, + }, + }); + + if (!changelog) { + return ( + +
+

Changelog not found

+
+ +
+ ); + } + + return ( +
+
+ + + +
+ + + + This will be shown in the list + +
+
+ + + + ); +} diff --git a/apps/changelog/src/app/changelog/%5Fadmin/create/page.tsx b/apps/changelog/src/app/changelog/%5Fadmin/create/page.tsx new file mode 100644 index 0000000000000..37d0022448e6e --- /dev/null +++ b/apps/changelog/src/app/changelog/%5Fadmin/create/page.tsx @@ -0,0 +1,73 @@ +import {Fragment} from 'react'; +import Link from 'next/link'; +import {prismaClient} from '@/server/prisma-client'; +import {createChangelog} from '@/server/actions/changelog'; +import {TitleSlug} from '@/client/components/titleSlug'; +import {FileUpload} from '@/client/components/fileUpload'; +import {Select} from '@/client/components/ui/Select'; +import {ForwardRefEditor} from '@/client/components/forwardRefEditor'; +import {Button} from '@/client/components/ui/Button'; +import {getServerSession} from 'next-auth/next'; +import {notFound} from 'next/navigation'; +import {authOptions} from '@/server/authOptions'; + +export default async function ChangelogCreatePage() { + const session = await getServerSession(authOptions); + + if (!session) { + return notFound(); + } + + const categories = await prismaClient.category.findMany({ + orderBy: { + name: 'asc', + }, + }); + + return ( +
+
+ + +
+ +