├── .npmrc ├── .github ├── CODEOWNERS ├── dependabot.yml ├── workflows │ ├── spammy-guardian.yml │ └── codeql.yml └── ISSUE_TEMPLATE │ ├── BUG_REPORT.yml │ └── FEATURE_REQUEST.yml ├── .husky ├── .gitignore └── pre-commit ├── pnpm-workspace.yaml ├── apps ├── api │ ├── .npmrc │ ├── server │ │ ├── models │ │ │ ├── tag.model.ts │ │ │ ├── profile.model.ts │ │ │ ├── comment.model.ts │ │ │ ├── article.model.ts │ │ │ ├── http-exception.model.ts │ │ │ └── user.model.ts │ │ ├── utils │ │ │ ├── foo.ts │ │ │ ├── generate-token.ts │ │ │ ├── prisma.ts │ │ │ ├── hash-password.ts │ │ │ ├── author.mapper.ts │ │ │ ├── profile.utils.ts │ │ │ ├── article.mapper.ts │ │ │ └── auth.ts │ │ ├── public │ │ │ └── images │ │ │ │ ├── demo-avatar.png │ │ │ │ └── smiley-cyrus.jpeg │ │ ├── routes │ │ │ └── api │ │ │ │ ├── [...].options.ts │ │ │ │ ├── v2 │ │ │ │ ├── auth │ │ │ │ │ ├── logout.post.ts │ │ │ │ │ ├── login.post.ts │ │ │ │ │ └── signup.post.ts │ │ │ │ └── profile │ │ │ │ │ ├── [id].get.ts │ │ │ │ │ └── [id].put.ts │ │ │ │ ├── user │ │ │ │ ├── index.get.ts │ │ │ │ └── index.put.ts │ │ │ │ ├── profiles │ │ │ │ └── [username] │ │ │ │ │ ├── index.get.ts │ │ │ │ │ └── follow │ │ │ │ │ ├── index.post.ts │ │ │ │ │ └── index.delete.ts │ │ │ │ ├── tags │ │ │ │ └── index.get.ts │ │ │ │ ├── articles │ │ │ │ ├── [slug] │ │ │ │ │ ├── index.delete.ts │ │ │ │ │ ├── comments │ │ │ │ │ │ ├── [id].delete.ts │ │ │ │ │ │ ├── index.get.ts │ │ │ │ │ │ └── index.post.ts │ │ │ │ │ ├── index.get.ts │ │ │ │ │ ├── favorite │ │ │ │ │ │ ├── index.post.ts │ │ │ │ │ │ └── index.delete.ts │ │ │ │ │ └── index.put.ts │ │ │ │ ├── feed.get.ts │ │ │ │ ├── index.post.ts │ │ │ │ └── index.get.ts │ │ │ │ └── users │ │ │ │ ├── login.post.ts │ │ │ │ └── index.post.ts │ │ └── auth-event-handler.ts │ ├── .gitignore │ ├── prisma │ │ ├── dev.db │ │ ├── migrations │ │ │ ├── migration_lock.toml │ │ │ └── 20241009081140_init │ │ │ │ └── migration.sql │ │ ├── schema.prisma │ │ └── seed.ts │ ├── tsconfig.json │ ├── README.md │ ├── nitro.config.ts │ └── package.json └── documentation │ ├── src │ ├── content │ │ ├── docs │ │ │ ├── specifications │ │ │ │ ├── frontend │ │ │ │ │ ├── swagger.mdx │ │ │ │ │ ├── tests.md │ │ │ │ │ ├── styles.md │ │ │ │ │ ├── routing.md │ │ │ │ │ ├── api.md │ │ │ │ │ └── templates.md │ │ │ │ ├── backend │ │ │ │ │ ├── tests.md │ │ │ │ │ ├── cors.md │ │ │ │ │ ├── postman.md │ │ │ │ │ ├── introduction.md │ │ │ │ │ ├── error-handling.md │ │ │ │ │ ├── api-response-format.md │ │ │ │ │ └── endpoints.md │ │ │ │ └── mobile-specs │ │ │ │ │ └── introduction.md │ │ │ ├── implementation-creation │ │ │ │ ├── features.md │ │ │ │ ├── introduction.md │ │ │ │ └── expectations.md │ │ │ ├── index.mdx │ │ │ ├── community │ │ │ │ ├── resources.md │ │ │ │ ├── special-thanks.md │ │ │ │ └── authors.md │ │ │ └── introduction.mdx │ │ └── config.ts │ ├── tailwind.css │ ├── env.d.ts │ └── assets │ │ └── img │ │ ├── end.png │ │ ├── favicon.ico │ │ ├── conduit_l.png │ │ ├── realworld.png │ │ ├── spaceship.png │ │ ├── stacks_hr.gif │ │ ├── realworld-logo.png │ │ ├── codebaseshow-logo.png │ │ ├── realworld-dual-mode.png │ │ ├── logo.svg │ │ └── codebaseshow-logo.svg │ ├── .vscode │ ├── extensions.json │ └── launch.json │ ├── tsconfig.json │ ├── .gitignore │ ├── tailwind.config.mjs │ ├── public │ └── favicon.svg │ ├── package.json │ ├── README.md │ └── astro.config.mjs ├── Procfile ├── media ├── end.png ├── conduit_l.png ├── realworld.png ├── stacks_hr.gif ├── realworld-logo.png ├── spacer-1669x257.gif ├── realworld-dual-mode.png └── mobile_icons │ ├── android │ ├── playstore-icon.png │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ ├── mipmap-ldpi │ │ └── ic_launcher.png │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ └── mipmap-xxxhdpi │ │ └── ic_launcher.png │ ├── ios │ ├── iTunesArtwork@1x.png │ ├── iTunesArtwork@2x.png │ ├── iTunesArtwork@3x.png │ ├── AppIcon.appiconset │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-57x57@1x.png │ │ ├── Icon-App-57x57@2x.png │ │ ├── Icon-App-60x60@1x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-72x72@1x.png │ │ ├── Icon-App-72x72@2x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ ├── Icon-App-76x76@3x.png │ │ ├── Icon-Small-50x50@1x.png │ │ ├── Icon-Small-50x50@2x.png │ │ ├── Icon-App-83.5x83.5@2x.png │ │ └── Contents.json │ └── README.md │ ├── imessenger │ ├── icon-messages-app-27x20@1x.png │ ├── icon-messages-app-27x20@2x.png │ ├── icon-messages-app-27x20@3x.png │ ├── icon-messages-app-iPhone-60x45@1x.png │ ├── icon-messages-app-iPhone-60x45@2x.png │ ├── icon-messages-app-iPhone-60x45@3x.png │ ├── icon-messages-app-store-1024x768.png │ ├── icon-messages-transcript-32x24@1x.png │ ├── icon-messages-transcript-32x24@2x.png │ ├── icon-messages-transcript-32x24@3x.png │ ├── icon-messages-app-iPadAir-67x50@2x.png │ └── icon-messages-app-iPadAir-74x55@2x.png │ └── watchkit │ └── AppIcon.appiconset │ ├── Icon-24@2x.png │ ├── Icon-29@2x.png │ ├── Icon-29@3x.png │ ├── Icon-40@2x.png │ ├── Icon-44@2x.png │ ├── Icon-86@2x.png │ ├── Icon-98@2x.png │ ├── Icon-27.5@2x.png │ └── Contents.json ├── MAINTENANCE.md ├── api ├── README.md ├── run-api-tests.sh └── openapi.yml ├── turbo.json ├── .gitignore ├── package.json ├── LICENSE ├── README.md ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md /.npmrc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /apps/api/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: cd apps/api && npx prisma generate && && pnpm build && pnpm start 2 | -------------------------------------------------------------------------------- /apps/api/server/models/tag.model.ts: -------------------------------------------------------------------------------- 1 | export interface Tag { 2 | name: string; 3 | } 4 | -------------------------------------------------------------------------------- /media/end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/end.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /apps/api/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .data 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | -------------------------------------------------------------------------------- /apps/api/server/utils/foo.ts: -------------------------------------------------------------------------------- 1 | export const useFoo = () => { 2 | console.log('done') 3 | } 4 | -------------------------------------------------------------------------------- /media/conduit_l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/conduit_l.png -------------------------------------------------------------------------------- /media/realworld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/realworld.png -------------------------------------------------------------------------------- /media/stacks_hr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/stacks_hr.gif -------------------------------------------------------------------------------- /apps/api/prisma/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/apps/api/prisma/dev.db -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/specifications/frontend/swagger.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Swagger 3 | --- 4 | -------------------------------------------------------------------------------- /apps/documentation/src/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /media/realworld-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/realworld-logo.png -------------------------------------------------------------------------------- /media/spacer-1669x257.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/spacer-1669x257.gif -------------------------------------------------------------------------------- /media/realworld-dual-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/realworld-dual-mode.png -------------------------------------------------------------------------------- /apps/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | // https://nitro.unjs.io/guide/typescript 2 | { 3 | "extends": "./.nitro/types/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /apps/documentation/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /apps/documentation/src/assets/img/end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/apps/documentation/src/assets/img/end.png -------------------------------------------------------------------------------- /apps/api/server/public/images/demo-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/apps/api/server/public/images/demo-avatar.png -------------------------------------------------------------------------------- /apps/documentation/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /apps/documentation/src/assets/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/apps/documentation/src/assets/img/favicon.ico -------------------------------------------------------------------------------- /media/mobile_icons/android/playstore-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/android/playstore-icon.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/iTunesArtwork@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/iTunesArtwork@1x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/iTunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/iTunesArtwork@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/iTunesArtwork@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/iTunesArtwork@3x.png -------------------------------------------------------------------------------- /apps/api/server/public/images/smiley-cyrus.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/apps/api/server/public/images/smiley-cyrus.jpeg -------------------------------------------------------------------------------- /apps/documentation/src/assets/img/conduit_l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/apps/documentation/src/assets/img/conduit_l.png -------------------------------------------------------------------------------- /apps/documentation/src/assets/img/realworld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/apps/documentation/src/assets/img/realworld.png -------------------------------------------------------------------------------- /apps/documentation/src/assets/img/spaceship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/apps/documentation/src/assets/img/spaceship.png -------------------------------------------------------------------------------- /apps/documentation/src/assets/img/stacks_hr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/apps/documentation/src/assets/img/stacks_hr.gif -------------------------------------------------------------------------------- /apps/api/README.md: -------------------------------------------------------------------------------- 1 | # Nitro starter 2 | 3 | Look at the [nitro quick start](https://nitro.unjs.io/guide#quick-start) to learn more how to get started. 4 | -------------------------------------------------------------------------------- /apps/documentation/src/assets/img/realworld-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/apps/documentation/src/assets/img/realworld-logo.png -------------------------------------------------------------------------------- /apps/api/server/models/profile.model.ts: -------------------------------------------------------------------------------- 1 | export interface Profile { 2 | username: string; 3 | bio: string; 4 | image: string; 5 | following: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/[...].options.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(async (event) => { 2 | setResponseStatus(event, 200); 3 | return ""; 4 | }); 5 | -------------------------------------------------------------------------------- /apps/documentation/src/assets/img/codebaseshow-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/apps/documentation/src/assets/img/codebaseshow-logo.png -------------------------------------------------------------------------------- /media/mobile_icons/android/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/android/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /media/mobile_icons/android/mipmap-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/android/mipmap-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /media/mobile_icons/android/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/android/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /media/mobile_icons/android/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/android/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/documentation/src/assets/img/realworld-dual-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/apps/documentation/src/assets/img/realworld-dual-mode.png -------------------------------------------------------------------------------- /media/mobile_icons/android/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/android/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /media/mobile_icons/android/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/android/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /apps/api/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /media/mobile_icons/imessenger/icon-messages-app-27x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/imessenger/icon-messages-app-27x20@1x.png -------------------------------------------------------------------------------- /media/mobile_icons/imessenger/icon-messages-app-27x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/imessenger/icon-messages-app-27x20@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/imessenger/icon-messages-app-27x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/imessenger/icon-messages-app-27x20@3x.png -------------------------------------------------------------------------------- /apps/documentation/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "react" 6 | } 7 | } -------------------------------------------------------------------------------- /media/mobile_icons/watchkit/AppIcon.appiconset/Icon-24@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/watchkit/AppIcon.appiconset/Icon-24@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/watchkit/AppIcon.appiconset/Icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/watchkit/AppIcon.appiconset/Icon-29@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/watchkit/AppIcon.appiconset/Icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/watchkit/AppIcon.appiconset/Icon-29@3x.png -------------------------------------------------------------------------------- /media/mobile_icons/watchkit/AppIcon.appiconset/Icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/watchkit/AppIcon.appiconset/Icon-40@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/watchkit/AppIcon.appiconset/Icon-44@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/watchkit/AppIcon.appiconset/Icon-44@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/watchkit/AppIcon.appiconset/Icon-86@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/watchkit/AppIcon.appiconset/Icon-86@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/watchkit/AppIcon.appiconset/Icon-98@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/watchkit/AppIcon.appiconset/Icon-98@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-60x60@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-60x60@1x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-76x76@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-76x76@3x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-Small-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-Small-50x50@1x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-Small-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-Small-50x50@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/watchkit/AppIcon.appiconset/Icon-27.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/watchkit/AppIcon.appiconset/Icon-27.5@2x.png -------------------------------------------------------------------------------- /apps/api/server/routes/api/v2/auth/logout.post.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(async (event) => { 2 | deleteCookie(event, 'auth_token'); 3 | 4 | return "Logged out"; 5 | }); 6 | -------------------------------------------------------------------------------- /media/mobile_icons/imessenger/icon-messages-app-iPhone-60x45@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/imessenger/icon-messages-app-iPhone-60x45@1x.png -------------------------------------------------------------------------------- /media/mobile_icons/imessenger/icon-messages-app-iPhone-60x45@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/imessenger/icon-messages-app-iPhone-60x45@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/imessenger/icon-messages-app-iPhone-60x45@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/imessenger/icon-messages-app-iPhone-60x45@3x.png -------------------------------------------------------------------------------- /media/mobile_icons/imessenger/icon-messages-app-store-1024x768.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/imessenger/icon-messages-app-store-1024x768.png -------------------------------------------------------------------------------- /media/mobile_icons/imessenger/icon-messages-transcript-32x24@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/imessenger/icon-messages-transcript-32x24@1x.png -------------------------------------------------------------------------------- /media/mobile_icons/imessenger/icon-messages-transcript-32x24@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/imessenger/icon-messages-transcript-32x24@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/imessenger/icon-messages-transcript-32x24@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/imessenger/icon-messages-transcript-32x24@3x.png -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/ios/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/imessenger/icon-messages-app-iPadAir-67x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/imessenger/icon-messages-app-iPadAir-67x50@2x.png -------------------------------------------------------------------------------- /media/mobile_icons/imessenger/icon-messages-app-iPadAir-74x55@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/universe1216/node-demo/HEAD/media/mobile_icons/imessenger/icon-messages-app-iPadAir-74x55@2x.png -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/specifications/backend/tests.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tests 3 | --- 4 | 5 | Include _at least_ **one** unit test in your repo to demonstrate how testing works (full testing coverage is _not_ required!) 6 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/specifications/frontend/tests.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tests 3 | --- 4 | 5 | Include _at least_ **one** unit test in your repo to demonstrate how testing works (full testing coverage is _not_ required!) 6 | -------------------------------------------------------------------------------- /apps/api/server/utils/generate-token.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | export const useGenerateToken = (id: number): string => 4 | jwt.sign({user: {id}}, process.env.JWT_SECRET, { 5 | expiresIn: '60d', 6 | }); 7 | -------------------------------------------------------------------------------- /apps/api/server/models/comment.model.ts: -------------------------------------------------------------------------------- 1 | import { Article } from './article.model'; 2 | 3 | export interface Comment { 4 | id: number; 5 | createdAt: Date; 6 | updatedAt: Date; 7 | body: string; 8 | article?: Article; 9 | } 10 | -------------------------------------------------------------------------------- /apps/documentation/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from 'astro:content'; 2 | import { docsSchema } from '@astrojs/starlight/schema'; 3 | 4 | export const collections = { 5 | docs: defineCollection({ schema: docsSchema() }), 6 | }; 7 | -------------------------------------------------------------------------------- /apps/api/server/utils/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | let _prisma; 4 | 5 | export const usePrisma = () => { 6 | if (!_prisma) { 7 | _prisma = new PrismaClient(); 8 | } 9 | return _prisma; 10 | } 11 | -------------------------------------------------------------------------------- /apps/api/nitro.config.ts: -------------------------------------------------------------------------------- 1 | //https://nitro.unjs.io/config 2 | export default defineNitroConfig({ 3 | srcDir: "server", 4 | preset: 'heroku', 5 | routeRules: { 6 | '/api/**': { cors: true, headers: { 'access-control-allow-methods': '*' } }, 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /apps/api/server/models/article.model.ts: -------------------------------------------------------------------------------- 1 | import { Comment } from './comment.model'; 2 | 3 | export interface Article { 4 | id: number; 5 | title: string; 6 | slug: string; 7 | description: string; 8 | comments: Comment[]; 9 | favorited: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /MAINTENANCE.md: -------------------------------------------------------------------------------- 1 | ## Update nx 2 | 3 | ```shell 4 | npx nx migrate latest 5 | ``` 6 | 7 | ### run migrations 8 | 9 | ```shell 10 | npx nx migrate --run-migrations 11 | ``` 12 | 13 | ## detect MDX2 errors for Storybook 14 | 15 | run `npx @hipster/mdx2-issue-checker` 16 | -------------------------------------------------------------------------------- /apps/documentation/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # RealWorld API Spec 2 | 3 | ## Running API tests locally 4 | 5 | To locally run the provided Postman collection against your backend, execute: 6 | 7 | ``` 8 | APIURL=http://localhost:3000/api ./run-api-tests.sh 9 | ``` 10 | 11 | For more details, see [`run-api-tests.sh`](run-api-tests.sh). 12 | -------------------------------------------------------------------------------- /apps/api/server/utils/hash-password.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs'; 2 | 3 | export const useHashPassword = (password: string) => { 4 | return bcrypt.hash(password, 10); 5 | } 6 | 7 | export const useDecrypt = (input: string, password: string) => { 8 | return bcrypt.compare(input, password); 9 | } 10 | -------------------------------------------------------------------------------- /apps/api/server/models/http-exception.model.ts: -------------------------------------------------------------------------------- 1 | class HttpException extends Error { 2 | errorCode: number; 3 | constructor( 4 | errorCode: number, 5 | public readonly message: string | any, 6 | ) { 7 | super(message); 8 | this.errorCode = errorCode; 9 | } 10 | } 11 | 12 | export default HttpException; 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: '/' 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: npm 10 | directory: '/' 11 | schedule: 12 | interval: weekly 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /apps/documentation/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": [] 8 | }, 9 | "lint": { 10 | "dependsOn": ["^lint"] 11 | }, 12 | "dev": { 13 | "cache": false, 14 | "persistent": true 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/api/server/utils/author.mapper.ts: -------------------------------------------------------------------------------- 1 | import { User } from '~/models/user.model'; 2 | 3 | const authorMapper = (author: any, id?: number) => ({ 4 | username: author.username, 5 | bio: author.bio, 6 | image: author.image, 7 | following: id 8 | ? author?.followedBy.some((followingUser: Partial) => followingUser.id === id) 9 | : false, 10 | }); 11 | 12 | export default authorMapper; 13 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/specifications/backend/cors.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CORS 3 | --- 4 | 5 | ## Considerations for your backend with [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) 6 | 7 | If the backend is about to run on a different host/port than the frontend, make sure to handle `OPTIONS` too and return correct `Access-Control-Allow-Origin` and `Access-Control-Allow-Headers` (e.g. `Content-Type`). 8 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/v2/profile/[id].get.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(async (event) => { 2 | const id = getRouterParam(event, 'id'); 3 | 4 | const profile = await usePrisma().profile.findUnique({ 5 | where: { 6 | id 7 | }, 8 | select: { 9 | id: true, 10 | username: true, 11 | bio: true, 12 | image: true, 13 | } 14 | }) 15 | }); 16 | -------------------------------------------------------------------------------- /apps/api/server/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import { Article } from './article.model'; 2 | import { Comment } from './comment.model'; 3 | 4 | export interface User { 5 | id: number; 6 | username: string; 7 | email: string; 8 | password: string; 9 | bio: string | null; 10 | image: any | null; 11 | articles: Article[]; 12 | favorites: Article[]; 13 | followedBy: User[]; 14 | following: User[]; 15 | comments: Comment[]; 16 | demo: boolean; 17 | } 18 | -------------------------------------------------------------------------------- /apps/api/server/utils/profile.utils.ts: -------------------------------------------------------------------------------- 1 | import { User } from '~/models/user.model'; 2 | import { Profile } from '~/models/profile.model'; 3 | 4 | const profileMapper = (user: any, id: number | undefined): Profile => ({ 5 | username: user.username, 6 | bio: user.bio, 7 | image: user.image, 8 | following: id 9 | ? user?.followedBy.some((followingUser: Partial) => followingUser.id === id) 10 | : false, 11 | }); 12 | 13 | export default profileMapper; 14 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/implementation-creation/features.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Features 3 | --- 4 | 5 | **General functionality:** 6 | 7 | - Authenticate users via JWT (login/signup pages + logout button on settings page) 8 | - CRU- users (sign up & settings page - no deleting required) 9 | - CRUD Articles 10 | - CR-D Comments on articles (no updating required) 11 | - GET and display paginated lists of articles 12 | - Favorite articles 13 | - Follow other users 14 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/specifications/backend/postman.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Postman 3 | --- 4 | 5 | For your convenience, we have a [Postman collection](https://github.com/gothinkster/realworld/blob/master/api/Conduit.postman_collection.json) that you can use to test your API endpoints as you build your app. 6 | 7 | ## Running API tests locally 8 | 9 | To locally run the provided Postman collection against your backend, follow instructions [here](https://github.com/gothinkster/realworld/tree/main/api). 10 | -------------------------------------------------------------------------------- /api/run-api-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | 4 | SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" 5 | 6 | APIURL=${APIURL:-https://api.realworld.io/api} 7 | USERNAME=${USERNAME:-u`date +%s`} 8 | EMAIL=${EMAIL:-$USERNAME@mail.com} 9 | PASSWORD=${PASSWORD:-password} 10 | 11 | npx newman run $SCRIPTDIR/Conduit.postman_collection.json \ 12 | --delay-request 500 \ 13 | --global-var "APIURL=$APIURL" \ 14 | --global-var "USERNAME=$USERNAME" \ 15 | --global-var "EMAIL=$EMAIL" \ 16 | --global-var "PASSWORD=$PASSWORD" \ 17 | "$@" 18 | -------------------------------------------------------------------------------- /.github/workflows/spammy-guardian.yml: -------------------------------------------------------------------------------- 1 | name: Spammy Guardian 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | issueId: 6 | description: 'id of the issue to test againt' 7 | required: true 8 | issue_comment: 9 | issues: 10 | types: [opened] 11 | jobs: 12 | spammy-guardian: 13 | runs-on: ubuntu-latest 14 | if: ${{ github.actor != 'dependabot[bot]' || github.actor != 'netlify[bot]' }} 15 | steps: 16 | - uses: kerhub/spammy-guardian@fa79bcda24df6dae5b93285e1749e59c77add4bd 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.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 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | 31 | # Debug 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | 36 | # Misc 37 | .DS_Store 38 | *.pem 39 | 40 | 41 | # IDEs 42 | .idea 43 | -------------------------------------------------------------------------------- /apps/api/server/utils/article.mapper.ts: -------------------------------------------------------------------------------- 1 | import authorMapper from './author.mapper'; 2 | 3 | const articleMapper = (article: any, id?: number) => ({ 4 | slug: article.slug, 5 | title: article.title, 6 | description: article.description, 7 | body: article.body, 8 | tagList: article.tagList.map((tag: any) => tag.name), 9 | createdAt: article.createdAt, 10 | updatedAt: article.updatedAt, 11 | favorited: article.favoritedBy.some((item: any) => item.id === id), 12 | favoritesCount: article.favoritedBy.length, 13 | author: authorMapper(article.author, id), 14 | }); 15 | 16 | export default articleMapper; 17 | -------------------------------------------------------------------------------- /apps/documentation/tailwind.config.mjs: -------------------------------------------------------------------------------- 1 | import starlightPlugin from '@astrojs/starlight-tailwind'; 2 | 3 | // Generated color palettes 4 | const accent = { 200: '#e3b6ed', 600: '#a700c3', 900: '#4e0e5b', 950: '#36113e' }; 5 | const gray = { 100: '#f8f4fe', 200: '#f2e9fd', 300: '#c7bdd5', 400: '#9581ae', 500: '#614e78', 700: '#412e55', 800: '#2f1c42', 900: '#1c1425' }; 6 | 7 | /** @type {import('tailwindcss').Config} */ 8 | export default { 9 | content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], 10 | theme: { 11 | extend: { 12 | colors: { accent, gray }, 13 | }, 14 | }, 15 | plugins: [starlightPlugin()], 16 | }; 17 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/specifications/backend/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | --- 4 | 5 | All backend implementations need to adhere to our [API spec](https://github.com/gothinkster/realworld/tree/main/api). 6 | 7 | For your convenience, we have a [Postman collection](https://github.com/gothinkster/realworld/blob/main/api/Conduit.postman_collection.json) that you can use to test your API endpoints as you build your app. 8 | 9 | Check out our [starter kit](https://github.com/gothinkster/realworld-starter-kit) to create a new implementation, please read [references to the API specs & testing](/specifications/backend/introduction) required for creating a new backend. 10 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: RealWorld apps 3 | description: It's all about building real world, production ready apps. 4 | template: splash 5 | hero: 6 | tagline: | 7 | While most "todo" demos provide an excellent cursory glance at a framework's capabilities, they typically don't convey the knowledge & perspective required to actually build real applications with it. That's why we, with the help of open source experts, design and serve as exemplary real world applications for each framework. 8 | image: 9 | file: ../../assets/img/realworld-logo.png 10 | actions: 11 | - text: Documentation 12 | link: /introduction 13 | icon: right-arrow 14 | --- 15 | -------------------------------------------------------------------------------- /apps/documentation/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/user/index.get.ts: -------------------------------------------------------------------------------- 1 | import {User} from "~/models/user.model"; 2 | import {definePrivateEventHandler} from "~/auth-event-handler"; 3 | 4 | export default definePrivateEventHandler(async (event, {auth}) => { 5 | const user = (await usePrisma().user.findUnique({ 6 | where: { 7 | id: auth.id, 8 | }, 9 | select: { 10 | id: true, 11 | email: true, 12 | username: true, 13 | bio: true, 14 | image: true, 15 | }, 16 | })) as User; 17 | 18 | return { 19 | user: { 20 | ...user, 21 | token: useGenerateToken(user.id), 22 | } 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/specifications/backend/error-handling.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Error handling 3 | --- 4 | 5 | ### Errors and Status Codes 6 | 7 | If a request fails any validations, expect a 422 and errors in the following format: 8 | 9 | ```JSON 10 | { 11 | "errors":{ 12 | "body": [ 13 | "can't be empty" 14 | ] 15 | } 16 | } 17 | ``` 18 | 19 | #### Other status codes: 20 | 21 | 401 for Unauthorized requests, when a request requires authentication but it isn't provided 22 | 23 | 403 for Forbidden requests, when a request may be valid but the user doesn't have permissions to perform the action 24 | 25 | 404 for Not found requests, when a resource can't be found to fulfill the request 26 | -------------------------------------------------------------------------------- /apps/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "nitro build", 5 | "dev": "nitro dev", 6 | "prepare": "nitro prepare", 7 | "preview": "node .output/server/index.mjs", 8 | "start": "node .output/server/index.mjs", 9 | "db:seed": "npx prisma db seed" 10 | }, 11 | "prisma": { 12 | "seed": "npx ts-node --transpile-only ./prisma/seed.ts" 13 | }, 14 | "devDependencies": { 15 | "@ngneat/falso": "^5.0.0", 16 | "@types": "link:@types", 17 | "jsonwebtoken": "^9.0.2", 18 | "nitropack": "latest", 19 | "prisma": "^5.18.0" 20 | }, 21 | "dependencies": { 22 | "@prisma/client": "^5.18.0", 23 | "bcryptjs": "^2.4.3", 24 | "slugify": "^1.6.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/v2/profile/[id].put.ts: -------------------------------------------------------------------------------- 1 | import {z} from "zod"; 2 | 3 | const profileSchema = z.object({ 4 | image: z.string().url().optional(), 5 | bio: z.string().optional(), 6 | }); 7 | 8 | export default defineEventHandler(async (event) => { 9 | useCheckAuth('required'); 10 | 11 | const id = getRouterParam(event, 'id'); 12 | const body = readValidatedBody(event, profileSchema.parse); 13 | 14 | const updatedProfile = await usePrisma().update({ 15 | where: { 16 | id 17 | }, 18 | select: { 19 | id: true, 20 | username: true, 21 | bio: true, 22 | image: true, 23 | } 24 | }); 25 | 26 | return updatedProfile; 27 | }); 28 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/profiles/[username]/index.get.ts: -------------------------------------------------------------------------------- 1 | import HttpException from "~/models/http-exception.model"; 2 | import profileMapper from "~/utils/profile.utils"; 3 | import {definePrivateEventHandler} from "~/auth-event-handler"; 4 | 5 | export default definePrivateEventHandler(async (event, {auth}) => { 6 | const username = getRouterParam(event, 'username'); 7 | 8 | const profile = await usePrisma().user.findUnique({ 9 | where: { 10 | username, 11 | }, 12 | include: { 13 | followedBy: true, 14 | }, 15 | }); 16 | 17 | if (!profile) { 18 | throw new HttpException(404, {}); 19 | } 20 | 21 | return {profile: profileMapper(profile, auth.id)}; 22 | }, {requireAuth: false}); 23 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/profiles/[username]/follow/index.post.ts: -------------------------------------------------------------------------------- 1 | import profileMapper from "~/utils/profile.utils"; 2 | import {definePrivateEventHandler} from "~/auth-event-handler"; 3 | 4 | export default definePrivateEventHandler(async (event, {auth}) => { 5 | const username = getRouterParam(event, 'username'); 6 | 7 | const profile = await usePrisma().user.update({ 8 | where: { 9 | username, 10 | }, 11 | data: { 12 | followedBy: { 13 | connect: { 14 | id: auth.id, 15 | }, 16 | }, 17 | }, 18 | include: { 19 | followedBy: true, 20 | }, 21 | }); 22 | 23 | return {profile: profileMapper(profile, auth.id)}; 24 | }); 25 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/profiles/[username]/follow/index.delete.ts: -------------------------------------------------------------------------------- 1 | import profileMapper from "~/utils/profile.utils"; 2 | import {definePrivateEventHandler} from "~/auth-event-handler"; 3 | 4 | export default definePrivateEventHandler(async (event, {auth}) => { 5 | const username = getRouterParam(event, 'username'); 6 | 7 | const profile = await usePrisma().user.update({ 8 | where: { 9 | username, 10 | }, 11 | data: { 12 | followedBy: { 13 | disconnect: { 14 | id: auth.id, 15 | }, 16 | }, 17 | }, 18 | include: { 19 | followedBy: true, 20 | }, 21 | }); 22 | 23 | return {profile: profileMapper(profile, auth.id)}; 24 | }); 25 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/specifications/mobile-specs/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | --- 4 | 5 | ### [Icons for (iOS/Android)](https://github.com/gothinkster/realworld/tree/master/spec/mobile_icons) 6 | 7 | ### Styles/Templates 8 | 9 | Unfortunately, there isn't a common way for us to reuse & share styles/templates for cross-platform mobile apps. 10 | 11 | Instead, we recommend using the Medium.com [iOS](https://itunes.apple.com/us/app/medium/id828256236?mt=8) and [Android](https://play.google.com/store/apps/details?id=com.medium.reader&hl=en) apps as a "north star" regarding general UI functionality/layout, but try not to go too overboard otherwise it will unnecessarily complicate your codebase (in other words, [KISS](https://en.wikipedia.org/wiki/KISS_principle) :) 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: 'CodeQL' 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '24 3 * * 3' 7 | 8 | jobs: 9 | analyze: 10 | name: Analyze 11 | runs-on: ubuntu-latest 12 | permissions: 13 | actions: read 14 | contents: read 15 | security-events: write 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | language: ['javascript'] 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Initialize CodeQL 27 | uses: github/codeql-action/init@v2 28 | with: 29 | languages: ${{ matrix.language }} 30 | 31 | - name: Perform CodeQL Analysis 32 | uses: github/codeql-action/analyze@v2 33 | with: 34 | category: '/language:${{matrix.language}}' 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report 2 | description: Report a bug in the RealWorld project 3 | title: '[Bug]: ' 4 | labels: 5 | - bug 6 | body: 7 | - type: dropdown 8 | attributes: 9 | label: Relevant scope 10 | description: What is the scope of this request? 11 | options: 12 | - Frontend specs 13 | - Backend specs 14 | - Deployed demo 15 | - 'Other: describe below' 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: Description 21 | description: A clear and concise description of the problem 22 | validations: 23 | required: true 24 | - type: markdown 25 | attributes: 26 | value: >- 27 | This template was generated with [Issue Forms 28 | Creator](https://www.issue-forms-creator.app/) 29 | -------------------------------------------------------------------------------- /apps/documentation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "documentation", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro check && astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@astrojs/check": "^0.9.3", 14 | "@astrojs/react": "^3.6.2", 15 | "@astrojs/starlight": "^0.26.1", 16 | "@astrojs/starlight-tailwind": "^2.0.3", 17 | "@astrojs/tailwind": "^5.1.0", 18 | "@types/react": "^18.3.4", 19 | "@types/react-dom": "^18.3.0", 20 | "astro": "^4.10.2", 21 | "react": "^18.3.1", 22 | "react-dom": "^18.3.1", 23 | "sharp": "^0.32.5", 24 | "swagger-ui-react": "^5.17.14", 25 | "tailwindcss": "^3.4.10", 26 | "typescript": "^5.5.4", 27 | "zod": "^3.23.8" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/implementation-creation/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | --- 4 | 5 | **Conduit** is a social blogging site (i.e. a Medium.com clone). It uses a custom API for all requests, including authentication. 6 | 7 | :::tip 8 | Check for [Discussions](https://github.com/gothinkster/realworld/discussions/categories/wip-implementations) about works in progress as we don't list duplicate projects. 9 | An opportunity to collaborate might await you already. 10 | ::: 11 | 12 | Otherwise: 13 | 14 | 1. [fork our starter kit](https://github.com/gothinkster/realworld-starter-kit) 15 | 2. Read the following sections: _expectations_ and _features_ for a better understanding of this project 16 | 3. Read the frontend and/or the backend specs 17 | 4. Submit the new implementation on [CodebaseShow](https://codebase.show/projects/realworld) 18 | 19 | **Happy coding!** 20 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/specifications/frontend/styles.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Styles 3 | --- 4 | 5 | We created a custom Bootstrap 4 style & templates to ensure all frontends had consistent UI functionality. Our [starter kit](https://github.com/gothinkster/realworld-starter-kit) includes all the [templates & info required to get up and running](https://github.com/gothinkster/realworld-starter-kit/blob/master/FRONTEND_INSTRUCTIONS.md). 6 | 7 | Instead of having the Bootstrap theme included locally, we recommend loading the precompiled theme from our CDN (our [header template](/specifications/frontend/templates.md#header) does this by default): 8 | 9 | ```html 10 | 11 | ``` 12 | 13 | Alternatively, if you want to make modifications to the theme, check out the [theme's repo](https://github.com/gothinkster/conduit-bootstrap-template). 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realworld", 3 | "author": { 4 | "name": "Thinkster" 5 | }, 6 | "version": "1.2.0", 7 | "license": "MIT", 8 | "description": "Fullstack example codebases for React, Angular, Node, Django, Rails & more", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/gothinkster/realworld.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/gothinkster/realworld/issues" 15 | }, 16 | "homepage": "https://github.com/gothinkster/realworld#readme", 17 | "keywords": [ 18 | "react", 19 | "angular", 20 | "fullstack", 21 | "examples", 22 | "node" 23 | ], 24 | "scripts": { 25 | "build": "turbo build", 26 | "dev": "turbo dev", 27 | "lint": "turbo lint", 28 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 29 | }, 30 | "devDependencies": { 31 | "prettier": "^3.2.5", 32 | "turbo": "latest" 33 | }, 34 | "packageManager": "pnpm@8.9.0", 35 | "engines": { 36 | "node": ">=18" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/specifications/frontend/routing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Routing 3 | --- 4 | 5 | - Home page (URL: /#/ ) 6 | - List of tags 7 | - List of articles pulled from either Feed, Global, or by Tag 8 | - Pagination for list of articles 9 | - Sign in/Sign up pages (URL: /#/login, /#/register ) 10 | - Uses JWT (store the token in localStorage) 11 | - Authentication can be easily switched to session/cookie based 12 | - Settings page (URL: /#/settings ) 13 | - Editor page to create/edit articles (URL: /#/editor, /#/editor/article-slug-here ) 14 | - Article page (URL: /#/article/article-slug-here ) 15 | - Delete article button (only shown to article's author) 16 | - Render markdown from server client side 17 | - Comments section at bottom of page 18 | - Delete comment button (only shown to comment's author) 19 | - Profile page (URL: /#/profile/:username, /#/profile/:username/favorites ) 20 | - Show basic user info 21 | - List of articles populated from author's created articles or author's favorited articles 22 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/tags/index.get.ts: -------------------------------------------------------------------------------- 1 | import {Tag} from "~/models/tag.model"; 2 | import {definePrivateEventHandler} from "~/auth-event-handler"; 3 | 4 | export default definePrivateEventHandler(async (event, {auth}) => { 5 | const queries = []; 6 | queries.push({demo: true}); 7 | 8 | if (auth) { 9 | queries.push({ 10 | id: { 11 | equals: auth.id, 12 | }, 13 | }); 14 | } 15 | 16 | const tags = await usePrisma().tag.findMany({ 17 | where: { 18 | articles: { 19 | some: { 20 | author: { 21 | OR: queries, 22 | }, 23 | }, 24 | }, 25 | }, 26 | select: { 27 | name: true, 28 | }, 29 | orderBy: { 30 | articles: { 31 | _count: 'desc', 32 | }, 33 | }, 34 | take: 10, 35 | }); 36 | 37 | return {tags: tags.map((tag: Tag) => tag.name)}; 38 | }, {requireAuth: false}); 39 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/articles/[slug]/index.delete.ts: -------------------------------------------------------------------------------- 1 | import HttpException from "~/models/http-exception.model"; 2 | import {definePrivateEventHandler} from "~/auth-event-handler"; 3 | 4 | export default definePrivateEventHandler(async (event, {auth}) => { 5 | const slug = getRouterParam(event, 'slug'); 6 | 7 | const existingArticle = await usePrisma().article.findFirst({ 8 | where: { 9 | slug, 10 | }, 11 | select: { 12 | author: { 13 | select: { 14 | id: true, 15 | username: true, 16 | }, 17 | }, 18 | }, 19 | }); 20 | 21 | if (!existingArticle) { 22 | throw new HttpException(404, {}); 23 | } 24 | 25 | if (existingArticle.author.id !== auth.id) { 26 | throw new HttpException(403, { 27 | message: 'You are not authorized to delete this article', 28 | }); 29 | } 30 | await usePrisma().article.delete({ 31 | where: { 32 | slug, 33 | }, 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /media/mobile_icons/ios/README.md: -------------------------------------------------------------------------------- 1 | ## iTunesArtwork & iTunesArtwork@2x (App Icon) file extension: 2 | 3 | PNG extension is prepended to these two files - 4 | 5 | While Apple suggested to omit the extension for these files, 6 | the '.png' extension is actually required for iTunesConnect submission. 7 | 8 | This is done for you so you don't have to. 9 | 10 | However, for Ad_hoc or Enterprise distribution, the extension should be removed 11 | from the files before adding to XCode to avoid error. 12 | 13 | refs: https://developer.apple.com/library/ios/qa/qa1686/_index.html 14 | 15 | ## iTunesArtwork & iTunesArtwork@2x (App Icon) transparency handling: 16 | 17 | As images with alpha channels or transparencies cannot be set as an application's icon on 18 | iTunesConnect, all transparent pixels in your images will be converted into 19 | solid blacks. 20 | 21 | To achieve the best result, you're advised to adjust the transparency settings 22 | in your source files before converting them with makeAppIcon. 23 | 24 | refs: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/AppIcons.html 25 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/community/resources.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Resources 3 | --- 4 | 5 | # Community created resources 6 | 7 | - Performance comparisons: 8 | - [A Real-World Comparison of Front-End Frameworks with Benchmarks 2020](https://medium.com/dailyjs/a-realworld-comparison-of-front-end-frameworks-2020-4e50655fe4c1) 9 | - [A Real-World Comparison of Front-End Frameworks with Benchmarks 2019](https://medium.freecodecamp.org/a-realworld-comparison-of-front-end-frameworks-with-benchmarks-2019-update-4be0d3c78075) 10 | - [A Real-World Comparison of Front-End Frameworks with Benchmarks 2018](https://medium.freecodecamp.org/a-real-world-comparison-of-front-end-frameworks-with-benchmarks-2018-update-e5760fb4a962) 11 | - [A Real-World Comparison of Front-End Frameworks with Benchmarks 2017](https://medium.freecodecamp.org/a-real-world-comparison-of-front-end-frameworks-with-benchmarks-e1cb62fd526c) 12 | 13 | :::tip 14 | Hello fellow writer, get in touch with us in [**GitHub Discussions**](https://github.com/gothinkster/realworld/discussions/categories/community) so we can add your RealWorld related content here. 15 | ::: 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Thinkster 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/api/server/routes/api/articles/[slug]/comments/[id].delete.ts: -------------------------------------------------------------------------------- 1 | import HttpException from "~/models/http-exception.model"; 2 | import {definePrivateEventHandler} from "~/auth-event-handler"; 3 | 4 | export default definePrivateEventHandler(async (event, {auth}) => { 5 | const id = Number(getRouterParam(event, 'id')); 6 | 7 | const comment = await usePrisma().comment.findFirst({ 8 | where: { 9 | id, 10 | author: { 11 | id: auth.id, 12 | }, 13 | }, 14 | select: { 15 | author: { 16 | select: { 17 | id: true, 18 | username: true, 19 | }, 20 | }, 21 | }, 22 | }); 23 | 24 | if (!comment) { 25 | throw new HttpException(404, {}); 26 | } 27 | 28 | if (comment.author.id !== auth.id) { 29 | throw new HttpException(403, { 30 | message: 'You are not authorized to delete this comment', 31 | }); 32 | } 33 | 34 | await usePrisma().comment.delete({ 35 | where: { 36 | id, 37 | }, 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /apps/api/server/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import {default as jwt} from "jsonwebtoken"; 2 | 3 | export const useCheckAuth = (mode: 'optional' | 'required') => (event) => { 4 | // const token = getCookie(event, 'auth_token'); 5 | const header = getHeader(event, 'authorization'); 6 | let token; 7 | 8 | if ( 9 | (header && header.split(' ')[0] === 'Token') || 10 | (header && header.split(' ')[0] === 'Bearer') 11 | ) { 12 | token = header.split(' ')[1]; 13 | } 14 | 15 | if (!token && mode === 'required') { 16 | throw createError({ 17 | status: 401, 18 | statusMessage: 'Unauthorized', 19 | message: 'Missing authentication token' 20 | }); 21 | } 22 | 23 | 24 | if (token) { 25 | const verified = jwt.verify(token, process.env.JWT_SECRET); 26 | 27 | if (!verified) { 28 | throw createError({ 29 | status: 403, 30 | statusMessage: 'Unauthorized', 31 | message: 'Invalid authentication token' 32 | }); 33 | } 34 | 35 | event.context.user = verified.user; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature request 2 | description: Suggest a feature for RealWorld project 3 | title: '[Feature Request]:' 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: '# Feature Request' 8 | - type: dropdown 9 | attributes: 10 | label: Relevant Scope 11 | description: What is the scope of this request? 12 | options: 13 | - Frontend specs 14 | - Backend specs 15 | - 'Other: describe below' 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: Description 21 | description: ' ' 22 | validations: 23 | required: true 24 | - type: textarea 25 | attributes: 26 | label: Describe the solution you'd like 27 | description: If you have a solution in mind, please describe it. 28 | - type: textarea 29 | attributes: 30 | label: Describe alternatives you've considered 31 | description: Have you considered any alternative solutions or workarounds? 32 | - type: markdown 33 | attributes: 34 | value: >- 35 | This template was generated with [Issue Forms 36 | Creator](https://www.issue-forms-creator.app/) 37 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/user/index.put.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from 'bcryptjs'; 2 | import {definePrivateEventHandler} from "~/auth-event-handler"; 3 | 4 | export default definePrivateEventHandler(async (event, {auth}) => { 5 | const {user} = await readBody(event); 6 | 7 | const {email, username, password, image, bio} = user; 8 | let hashedPassword; 9 | 10 | if (password) { 11 | hashedPassword = await bcrypt.hash(password, 10); 12 | } 13 | 14 | const updatedUser = await usePrisma().user.update({ 15 | where: { 16 | id: auth.id, 17 | }, 18 | data: { 19 | ...(email ? {email} : {}), 20 | ...(username ? {username} : {}), 21 | ...(password ? {password: hashedPassword} : {}), 22 | ...(image ? {image} : {}), 23 | ...(bio ? {bio} : {}), 24 | }, 25 | select: { 26 | id: true, 27 | email: true, 28 | username: true, 29 | bio: true, 30 | image: true, 31 | }, 32 | }); 33 | 34 | return { 35 | user: { 36 | ...updatedUser, 37 | token: useGenerateToken(updatedUser.id), 38 | } 39 | }; 40 | }); 41 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/v2/auth/login.post.ts: -------------------------------------------------------------------------------- 1 | import {z} from "zod"; 2 | import {useDecrypt} from "~/utils/hash-password"; 3 | 4 | const userSchema = z.object({ 5 | email: z.string().email("This is not a valid email"), 6 | password: z.string().min(8).max(20), 7 | }); 8 | 9 | export default defineEventHandler(async (event) => { 10 | const {email, password} = await readValidatedBody(event, userSchema.parse); 11 | 12 | const user = await usePrisma().user.findUnique({ 13 | where: { 14 | email, 15 | }, 16 | select: { 17 | id: true, 18 | email: true, 19 | username: true, 20 | password: true, 21 | image: true, 22 | }, 23 | }); 24 | 25 | if (user) { 26 | const match = await useDecrypt(password, user.password); 27 | 28 | if (match) { 29 | setCookie(event, 'auth_token', useGenerateToken(user.id), { 30 | secure: true, 31 | httpOnly: true, 32 | sameSite: 'none', 33 | }); 34 | 35 | return { 36 | email: user.email, 37 | username: user.username, 38 | bio: user.bio, 39 | image: user.image, 40 | }; 41 | } 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/articles/[slug]/index.get.ts: -------------------------------------------------------------------------------- 1 | import HttpException from "~/models/http-exception.model"; 2 | import articleMapper from "~/utils/article.mapper"; 3 | import {definePrivateEventHandler} from "~/auth-event-handler"; 4 | 5 | export default definePrivateEventHandler(async (event, {auth}) => { 6 | const slug = getRouterParam(event, 'slug'); 7 | 8 | const article = await usePrisma().article.findUnique({ 9 | where: { 10 | slug, 11 | }, 12 | include: { 13 | tagList: { 14 | select: { 15 | name: true, 16 | }, 17 | }, 18 | author: { 19 | select: { 20 | username: true, 21 | bio: true, 22 | image: true, 23 | followedBy: true, 24 | }, 25 | }, 26 | favoritedBy: true, 27 | _count: { 28 | select: { 29 | favoritedBy: true, 30 | }, 31 | }, 32 | }, 33 | }); 34 | 35 | if (!article) { 36 | throw new HttpException(404, { errors: { article: ['not found'] } }); 37 | } 38 | 39 | return {article: articleMapper(article, auth.id)}; 40 | }, {requireAuth: false}); 41 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/community/special-thanks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Special thanks 3 | --- 4 | 5 | RealWorld would not be possible without the open source community's assistance in reviewing codebases, developing new app implementations, and a variety of other duties that help the project progress. We'd like to thank the following OSS leaders for their contributions to RealWorld: 6 | 7 | - **Dan Abramov** (creator of Redux) for helping [spark the initial idea](https://twitter.com/dan_abramov/status/692009757775896577), [getting the Redux community involved](https://github.com/reactjs/redux/issues/1353), as well as graciously taking the time to provide feedback on the Redux codebase 8 | - **Max Lynch** (creator of Ionic) for taking the time to provide guidance in the early days of this project 9 | - **Addy Osmani** (creator of TodoMVC) for helping [spark the initial idea](https://twitter.com/addyosmani/status/762828483433144320) and his amazing work with TodoMVC 10 | - **TodoMVC** ([team & contributors](https://github.com/tastejs/todomvc#team)) for their exemplary & successful work; their project & org has been an invaluable analogy for us as we've built out RealWorld 11 | - **James Brewer** (docs contributor to Django) for countless brainstorming sessions, helping name this project, and creating the Django codebase + tutorial 12 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/users/login.post.ts: -------------------------------------------------------------------------------- 1 | import HttpException from "~/models/http-exception.model"; 2 | import {default as bcrypt} from 'bcryptjs'; 3 | 4 | export default defineEventHandler(async (event) => { 5 | const {user} = await readBody(event); 6 | 7 | const email = user.email?.trim(); 8 | const password = user.password?.trim(); 9 | 10 | if (!email) { 11 | throw new HttpException(422, {errors: {email: ["can't be blank"]}}); 12 | } 13 | 14 | if (!password) { 15 | throw new HttpException(422, {errors: {password: ["can't be blank"]}}); 16 | } 17 | 18 | const foundUser = await usePrisma().user.findUnique({ 19 | where: { 20 | email, 21 | }, 22 | select: { 23 | id: true, 24 | email: true, 25 | username: true, 26 | password: true, 27 | bio: true, 28 | image: true, 29 | }, 30 | }); 31 | 32 | if (foundUser) { 33 | const match = await bcrypt.compare(password, foundUser.password); 34 | 35 | if (match) { 36 | return { 37 | user: { 38 | email: foundUser.email, 39 | username: foundUser.username, 40 | bio: foundUser.bio, 41 | image: foundUser.image, 42 | token: useGenerateToken(foundUser.id), 43 | } 44 | }; 45 | } 46 | } 47 | 48 | throw new HttpException(403, { 49 | errors: { 50 | 'email or password': ['is invalid'], 51 | }, 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/v2/auth/signup.post.ts: -------------------------------------------------------------------------------- 1 | import {z} from "zod"; 2 | 3 | const userSchema = z.object({ 4 | username: z.string().min(3).max(20), 5 | email: z.string().email("This is not a valid email"), 6 | password: z.string().min(8).max(20), 7 | }); 8 | 9 | export default defineEventHandler(async (event) => { 10 | const {username, email, password} = await readValidatedBody(event, userSchema.parse); 11 | 12 | const existingUser = await usePrisma().user.findUnique({ 13 | where: { 14 | OR: [ 15 | { email }, 16 | { username }, 17 | ] 18 | }, 19 | select: { 20 | id: true, 21 | }, 22 | }); 23 | 24 | if (existingUser) { 25 | return createError({ 26 | status: 422, 27 | statusMessage: 'Unprocessable Content', 28 | data: "User already exists" 29 | }); 30 | } 31 | 32 | const hashedPassword = await useHashPassword(password); 33 | 34 | const newUser = await usePrisma().user.create({ 35 | data: { 36 | username, 37 | email, 38 | password: hashedPassword 39 | }, 40 | select: { 41 | id: true, 42 | email: true, 43 | username: true, 44 | image: true 45 | } 46 | }); 47 | 48 | setCookie(event, 'auth_token', useGenerateToken(newUser.id)); 49 | setResponseStatus(event, 201); 50 | 51 | return { 52 | id: newUser.id, 53 | username: newUser.username, 54 | email: newUser.email, 55 | image: newUser.image, 56 | } 57 | }); 58 | 59 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/articles/[slug]/favorite/index.post.ts: -------------------------------------------------------------------------------- 1 | import profileMapper from "~/utils/profile.utils"; 2 | import {Tag} from "~/models/tag.model"; 3 | import {definePrivateEventHandler} from "~/auth-event-handler"; 4 | 5 | export default definePrivateEventHandler(async (event, {auth}) => { 6 | const slug = getRouterParam(event, "slug"); 7 | 8 | const { _count, ...article } = await usePrisma().article.update({ 9 | where: { 10 | slug, 11 | }, 12 | data: { 13 | favoritedBy: { 14 | connect: { 15 | id: auth.id, 16 | }, 17 | }, 18 | }, 19 | include: { 20 | tagList: { 21 | select: { 22 | name: true, 23 | }, 24 | }, 25 | author: { 26 | select: { 27 | username: true, 28 | bio: true, 29 | image: true, 30 | followedBy: true, 31 | }, 32 | }, 33 | favoritedBy: true, 34 | _count: { 35 | select: { 36 | favoritedBy: true, 37 | }, 38 | }, 39 | }, 40 | }); 41 | 42 | const result = { 43 | ...article, 44 | author: profileMapper(article.author, auth.id), 45 | tagList: article?.tagList.map((tag: Tag) => tag.name), 46 | favorited: article.favoritedBy.some((favorited: any) => favorited.id === auth.id), 47 | favoritesCount: _count?.favoritedBy, 48 | }; 49 | 50 | return {article: result}; 51 | }); 52 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/articles/[slug]/favorite/index.delete.ts: -------------------------------------------------------------------------------- 1 | import profileMapper from "~/utils/profile.utils"; 2 | import {Tag} from "~/models/tag.model"; 3 | import {definePrivateEventHandler} from "~/auth-event-handler"; 4 | 5 | export default definePrivateEventHandler(async (event, {auth}) => { 6 | const slug = getRouterParam(event, "slug"); 7 | 8 | const { _count, ...article } = await usePrisma().article.update({ 9 | where: { 10 | slug, 11 | }, 12 | data: { 13 | favoritedBy: { 14 | disconnect: { 15 | id: auth.id, 16 | }, 17 | }, 18 | }, 19 | include: { 20 | tagList: { 21 | select: { 22 | name: true, 23 | }, 24 | }, 25 | author: { 26 | select: { 27 | username: true, 28 | bio: true, 29 | image: true, 30 | followedBy: true, 31 | }, 32 | }, 33 | favoritedBy: true, 34 | _count: { 35 | select: { 36 | favoritedBy: true, 37 | }, 38 | }, 39 | }, 40 | }); 41 | 42 | const result = { 43 | ...article, 44 | author: profileMapper(article.author, auth.id), 45 | tagList: article?.tagList.map((tag: Tag) => tag.name), 46 | favorited: article.favoritedBy.some((favorited: any) => favorited.id === auth.id), 47 | favoritesCount: _count?.favoritedBy, 48 | }; 49 | 50 | return {article: result}; 51 | }); 52 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/articles/feed.get.ts: -------------------------------------------------------------------------------- 1 | import articleMapper from "~/utils/article.mapper"; 2 | import {definePrivateEventHandler} from "~/auth-event-handler"; 3 | 4 | export default definePrivateEventHandler(async (event, {auth}) => { 5 | const query = getQuery(event); 6 | const articlesCount = await usePrisma().article.count({ 7 | where: { 8 | author: { 9 | followedBy: { some: { id: auth.id } }, 10 | }, 11 | }, 12 | }); 13 | 14 | // TODO fix query 15 | const articles = await usePrisma().article.findMany({ 16 | where: { 17 | author: { 18 | followedBy: { some: { id: auth.id } }, 19 | }, 20 | }, 21 | orderBy: { 22 | createdAt: 'desc', 23 | }, 24 | skip: Number(query.offset) || 0, 25 | take: Number(query.limit) || 10, 26 | omit: { 27 | body: true, 28 | updatedAt: true, 29 | }, 30 | include: { 31 | tagList: { 32 | select: { 33 | name: true, 34 | }, 35 | }, 36 | author: { 37 | select: { 38 | username: true, 39 | image: true, 40 | followedBy: true, 41 | }, 42 | }, 43 | favoritedBy: true, 44 | _count: { 45 | select: { 46 | favoritedBy: true, 47 | }, 48 | }, 49 | }, 50 | }); 51 | 52 | return { 53 | articles: articles.map((article: any) => articleMapper(article, auth.id)), 54 | articlesCount, 55 | }; 56 | }); 57 | -------------------------------------------------------------------------------- /apps/api/server/auth-event-handler.ts: -------------------------------------------------------------------------------- 1 | import {default as jwt} from "jsonwebtoken"; 2 | 3 | export interface PrivateContext { 4 | auth: { 5 | id: number; 6 | } 7 | } 8 | 9 | export function definePrivateEventHandler( 10 | handler: (event: H3Event, cxt: PrivateContext) => T, 11 | options: { requireAuth: boolean } = {requireAuth: true} 12 | ) { 13 | return defineEventHandler(async (event) => { 14 | // you can check request hmac, user, token, etc.. 15 | const header = getHeader(event, 'authorization'); 16 | let token; 17 | 18 | if ( 19 | (header && header.split(' ')[0] === 'Token') || 20 | (header && header.split(' ')[0] === 'Bearer') 21 | ) { 22 | token = header.split(' ')[1]; 23 | } 24 | 25 | if (options.requireAuth && !token) { 26 | throw createError({ 27 | status: 401, 28 | statusMessage: 'Unauthorized', 29 | message: 'Missing authentication token' 30 | }); 31 | } 32 | 33 | if (token) { 34 | const verified = jwt.verify(token, process.env.JWT_SECRET); 35 | 36 | if (!verified) { 37 | throw createError({ 38 | status: 403, 39 | statusMessage: 'Unauthorized', 40 | message: 'Invalid authentication token' 41 | }); 42 | } 43 | 44 | return handler(event, { 45 | auth: { 46 | id: Number(verified.user.id) 47 | }, 48 | }) 49 | } else { 50 | return handler(event, { 51 | auth: null, 52 | }) 53 | } 54 | 55 | 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /media/mobile_icons/watchkit/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "size": "24x24", 5 | "idiom": "watch", 6 | "scale": "2x", 7 | "filename": "Icon-24@2x.png", 8 | "role": "notificationCenter", 9 | "subtype": "38mm" 10 | }, 11 | { 12 | "size": "27.5x27.5", 13 | "idiom": "watch", 14 | "scale": "2x", 15 | "filename": "Icon-27.5@2x.png", 16 | "role": "notificationCenter", 17 | "subtype": "42mm" 18 | }, 19 | { 20 | "size": "29x29", 21 | "idiom": "watch", 22 | "scale": "2x", 23 | "filename": "Icon-29@2x.png", 24 | "role": "companionSettings" 25 | }, 26 | { 27 | "size": "29x29", 28 | "idiom": "watch", 29 | "scale": "3x", 30 | "filename": "Icon-29@3x.png", 31 | "role": "companionSettings" 32 | }, 33 | { 34 | "size": "40x40", 35 | "idiom": "watch", 36 | "scale": "2x", 37 | "filename": "Icon-40@2x.png", 38 | "role": "appLauncher", 39 | "subtype": "38mm" 40 | }, 41 | { 42 | "size": "44x44", 43 | "idiom": "watch", 44 | "scale": "2x", 45 | "filename": "Icon-44@2x.png", 46 | "role": "longLook", 47 | "subtype": "42mm" 48 | }, 49 | { 50 | "size": "86x86", 51 | "idiom": "watch", 52 | "scale": "2x", 53 | "filename": "Icon-86@2x.png", 54 | "role": "quickLook", 55 | "subtype": "38mm" 56 | }, 57 | { 58 | "size": "98x98", 59 | "idiom": "watch", 60 | "scale": "2x", 61 | "filename": "Icon-98@2x.png", 62 | "role": "quickLook", 63 | "subtype": "42mm" 64 | } 65 | ], 66 | "info": { 67 | "version": 1, 68 | "author": "makeappicon" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/articles/[slug]/comments/index.get.ts: -------------------------------------------------------------------------------- 1 | import {definePrivateEventHandler} from "~/auth-event-handler"; 2 | 3 | export default definePrivateEventHandler(async (event, {auth}) => { 4 | const slug = getRouterParam(event, 'slug'); 5 | 6 | const queries = []; 7 | 8 | queries.push({ 9 | author: { 10 | demo: true, 11 | }, 12 | }); 13 | 14 | if (auth?.id) { 15 | queries.push({ 16 | author: { 17 | id: auth.id, 18 | }, 19 | }); 20 | } 21 | 22 | const comments = await usePrisma().article.findUnique({ 23 | where: { 24 | slug, 25 | }, 26 | include: { 27 | comments: { 28 | where: { 29 | OR: queries, 30 | }, 31 | select: { 32 | id: true, 33 | createdAt: true, 34 | updatedAt: true, 35 | body: true, 36 | author: { 37 | select: { 38 | username: true, 39 | bio: true, 40 | image: true, 41 | followedBy: true, 42 | }, 43 | }, 44 | }, 45 | }, 46 | }, 47 | }); 48 | 49 | const result = comments?.comments.map((comment: any) => ({ 50 | ...comment, 51 | author: { 52 | username: comment.author.username, 53 | bio: comment.author.bio, 54 | image: comment.author.image, 55 | following: comment.author.followedBy.some((follow: any) => follow.id === auth.id), 56 | }, 57 | })); 58 | 59 | return {comments: result}; 60 | }, {requireAuth: false}); 61 | -------------------------------------------------------------------------------- /apps/api/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "sqlite" 3 | url = "file:./dev.db" 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | previewFeatures = ["omitApi"] 9 | } 10 | 11 | model Article { 12 | id Int @id @default(autoincrement()) 13 | slug String @unique 14 | title String 15 | description String 16 | body String 17 | createdAt DateTime @default(now()) 18 | updatedAt DateTime @default(now()) 19 | tagList Tag[] 20 | author User @relation("UserArticles", fields: [authorId], onDelete: Cascade, references: [id]) 21 | authorId Int 22 | favoritedBy User[] @relation("UserFavorites") 23 | comments Comment[] 24 | } 25 | 26 | model Comment { 27 | id Int @id @default(autoincrement()) 28 | createdAt DateTime @default(now()) 29 | updatedAt DateTime @default(now()) 30 | body String 31 | article Article @relation(fields: [articleId], references: [id], onDelete: Cascade) 32 | articleId Int 33 | author User @relation(fields: [authorId], references: [id], onDelete: Cascade) 34 | authorId Int 35 | } 36 | 37 | model Tag { 38 | id Int @id @default(autoincrement()) 39 | name String @unique 40 | articles Article[] 41 | } 42 | 43 | model User { 44 | id Int @id @default(autoincrement()) 45 | email String @unique 46 | username String @unique 47 | password String 48 | image String? @default("https://api.realworld.io/images/smiley-cyrus.jpeg") 49 | bio String? 50 | articles Article[] @relation("UserArticles") 51 | favorites Article[] @relation("UserFavorites") 52 | followedBy User[] @relation("UserFollows") 53 | following User[] @relation("UserFollows") 54 | comments Comment[] 55 | demo Boolean @default(false) 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![RealWorld Example Applications](media/realworld-dual-mode.png) 2 | 3 |

4 | 5 |

6 | 7 | 8 | While most "todo" demos provide an excellent cursory glance at a framework's capabilities, they typically don't convey the knowledge & perspective required to actually build _real_ applications with it. 9 | 10 | **RealWorld** solves this by allowing you to choose any frontend (React, Angular, & more) and any backend (Node, Django, & more). 11 | 12 | _Read the [full blog post announcing RealWorld on Medium.](https://medium.com/@ericsimons/introducing-realworld-6016654d36b5)_ 13 | 14 | Join us on [GitHub Discussions!](https://github.com/gothinkster/realworld/discussions) 🎉 15 | 16 | # Implementations 17 | 18 | Over 100 implementations have been created using various languages, libraries, and frameworks. 19 | 20 | Explore them on [**CodebaseShow**](https://codebase.show/projects/realworld). 21 | 22 | # Create a new implementation 23 | 24 | [**Create a new implementation >>>**](https://realworld-docs.netlify.app/implementation-creation/introduction) 25 | 26 | Or you can [view upcoming implementations (WIPs)](https://github.com/gothinkster/realworld/discussions/categories/wip-implementations). 27 | 28 | # Learn more 29 | 30 | - ["Introducing RealWorld 🙌"](https://medium.com/@ericsimons/introducing-realworld-6016654d36b5) 31 | - Every tutorial is built against the same [API spec](api/) to ensure modularity of every frontend & backend 32 | - Every frontend utilizes the same handcrafted [Bootstrap 4 theme](https://github.com/gothinkster/conduit-bootstrap-template) for identical UI/UX 33 | - There is a hosted version of the backend API available for public usage, no API keys are required 34 | - Interested in creating a new RealWorld stack? View our [starter guide & spec](https://realworld-docs.netlify.app/implementation-creation/introduction) 35 | 36 | 37 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/articles/[slug]/comments/index.post.ts: -------------------------------------------------------------------------------- 1 | import HttpException from "~/models/http-exception.model"; 2 | import {definePrivateEventHandler} from "~/auth-event-handler"; 3 | 4 | export default definePrivateEventHandler(async (event, {auth}) => { 5 | const {comment} = await readBody(event); 6 | const slug = getRouterParam(event, 'slug'); 7 | 8 | if (!comment.body) { 9 | throw new HttpException(422, {errors: {body: ["can't be blank"]}}); 10 | } 11 | 12 | const article = await usePrisma().article.findUnique({ 13 | where: { 14 | slug, 15 | }, 16 | select: { 17 | id: true, 18 | }, 19 | }); 20 | 21 | const createdComment = await usePrisma().comment.create({ 22 | data: { 23 | body: comment.body, 24 | article: { 25 | connect: { 26 | id: article?.id, 27 | }, 28 | }, 29 | author: { 30 | connect: { 31 | id: auth.id, 32 | }, 33 | }, 34 | }, 35 | include: { 36 | author: { 37 | select: { 38 | username: true, 39 | bio: true, 40 | image: true, 41 | followedBy: true, 42 | }, 43 | }, 44 | }, 45 | }); 46 | 47 | return { 48 | comment: { 49 | id: createdComment.id, 50 | createdAt: createdComment.createdAt, 51 | updatedAt: createdComment.updatedAt, 52 | body: createdComment.body, 53 | author: { 54 | username: createdComment.author.username, 55 | bio: createdComment.author.bio, 56 | image: createdComment.author.image, 57 | following: createdComment.author.followedBy.some((follow: any) => follow.id === auth.id), 58 | }, 59 | } 60 | }; 61 | }); 62 | -------------------------------------------------------------------------------- /apps/api/prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { 2 | randEmail, 3 | randFullName, 4 | randLines, 5 | randParagraph, 6 | randPassword, 7 | randPhrase, 8 | randWord, 9 | } from '@ngneat/falso'; 10 | import { PrismaClient } from '@prisma/client'; 11 | import { RegisteredUser } from '../app/models/registered-user.model'; 12 | import { createUser } from '../app/services/auth.service'; 13 | import { addComment, createArticle } from '../app/services/article.service'; 14 | 15 | const prisma = new PrismaClient(); 16 | 17 | export const generateUser = async (): Promise => 18 | createUser({ 19 | username: randFullName(), 20 | email: randEmail(), 21 | password: randPassword(), 22 | image: 'https://api.realworld.io/images/demo-avatar.png', 23 | demo: true, 24 | }); 25 | 26 | export const generateArticle = async (id: number) => 27 | createArticle( 28 | { 29 | title: randPhrase(), 30 | description: randParagraph(), 31 | body: randLines({ length: 10 }).join(' '), 32 | tagList: randWord({ length: 4 }), 33 | }, 34 | id, 35 | ); 36 | 37 | export const generateComment = async (id: number, slug: string) => 38 | addComment(randParagraph(), slug, id); 39 | 40 | export const main = async () => { 41 | const users = await Promise.all(Array.from({ length: 3 }, () => generateUser())); 42 | users?.map(user => user); 43 | 44 | // eslint-disable-next-line no-restricted-syntax 45 | for await (const user of users) { 46 | const articles = await Promise.all(Array.from({ length: 2 }, () => generateArticle(user.id))); 47 | 48 | // eslint-disable-next-line no-restricted-syntax 49 | for await (const article of articles) { 50 | await Promise.all(users.map(userItem => generateComment(userItem.id, article.slug))); 51 | } 52 | } 53 | }; 54 | 55 | main() 56 | .then(async () => { 57 | await prisma.$disconnect(); 58 | }) 59 | .catch(async (err) => { 60 | console.log(err) 61 | await prisma.$disconnect(); 62 | process.exit(1); 63 | }); 64 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | --- 4 | 5 | # Introduction 6 | 7 | 8 | 9 | 10 | 11 | > See how _the exact same_ Medium.com clone is built using different [frontends](https://codebase.show/projects/realworld?category=frontend) and [backends](https://codebase.show/projects/realworld?category=backend). Yes, you can mix and match them, because **they all adhere to the same [API spec](specs/backend-specs/introduction)** 😮😎 12 | 13 | While most "todo" demos provide an excellent cursory glance at a framework's capabilities, they typically don't convey the knowledge & perspective required to actually build _real_ applications with it. 14 | 15 | **RealWorld** solves this by allowing you to choose any frontend (React, Angular, & more) and any backend (Node, Django, & more). 16 | 17 | _Read the [full blog post announcing RealWorld on Medium.](https://medium.com/@ericsimons/introducing-realworld-6016654d36b5)_ 18 | 19 | Join us on [GitHub Discussions!](https://github.com/gothinkster/realworld/discussions) 🎉 20 | 21 | ## Implementations 22 | 23 | Over 150 implementations have been created using various languages, libraries, and frameworks. 24 | 25 | Explore them on [**CodebaseShow**](https://codebase.show/projects/realworld). 26 | 27 | ## Create a new implementation 28 | 29 | [**Create a new implementation >>>**](implementation-creation/introduction) 30 | 31 | Or you can [view upcoming implementations (WIPs)](https://github.com/gothinkster/realworld/discussions/categories/wip-implementations). 32 | 33 | ## Learn more 34 | 35 | - ["Introducing RealWorld 🙌"](https://medium.com/@ericsimons/introducing-realworld-6016654d36b5) by Eric Simons 36 | - Every tutorial is built against the same [API spec](/specifications/backend/introduction) to ensure modularity of every frontend & backend 37 | - Every frontend utilizes the same hand crafted [Bootstrap 4 theme](https://github.com/gothinkster/conduit-bootstrap-template) for identical UI/UX 38 | - There is a [hosted version](https://realworld-docs.netlify.app/docs/specs/frontend-specs/api#demo-api) of the backend API available for public usage, no API keys are required 39 | - Interested in creating a new RealWorld stack? View our [starter guide & spec](implementation-creation/introduction) 40 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/users/index.post.ts: -------------------------------------------------------------------------------- 1 | import HttpException from "~/models/http-exception.model"; 2 | import {default as bcrypt} from 'bcryptjs'; 3 | 4 | export default defineEventHandler(async (event) => { 5 | const {user} = await readBody(event); 6 | 7 | const email = user.email?.trim(); 8 | const username = user.username?.trim(); 9 | const password = user.password?.trim(); 10 | const {image, bio, demo} = user; 11 | 12 | if (!email) { 13 | throw new HttpException(422, {errors: {email: ["can't be blank"]}}); 14 | } 15 | 16 | if (!username) { 17 | throw new HttpException(422, {errors: {username: ["can't be blank"]}}); 18 | } 19 | 20 | if (!password) { 21 | throw new HttpException(422, {errors: {password: ["can't be blank"]}}); 22 | } 23 | 24 | await checkUserUniqueness(email, username); 25 | 26 | const hashedPassword = await bcrypt.hash(password, 10); 27 | 28 | const createdUser = await usePrisma().user.create({ 29 | data: { 30 | username, 31 | email, 32 | password: hashedPassword, 33 | ...(image ? {image} : {}), 34 | ...(bio ? {bio} : {}), 35 | ...(demo ? {demo} : {}), 36 | }, 37 | select: { 38 | id: true, 39 | email: true, 40 | username: true, 41 | bio: true, 42 | image: true, 43 | }, 44 | }); 45 | 46 | return { 47 | user: { 48 | ...createdUser, 49 | token: useGenerateToken(createdUser.id), 50 | } 51 | }; 52 | }); 53 | 54 | const checkUserUniqueness = async (email: string, username: string) => { 55 | const existingUserByEmail = await usePrisma().user.findUnique({ 56 | where: { 57 | email, 58 | }, 59 | select: { 60 | id: true, 61 | }, 62 | }); 63 | 64 | const existingUserByUsername = await usePrisma().user.findUnique({ 65 | where: { 66 | username, 67 | }, 68 | select: { 69 | id: true, 70 | }, 71 | }); 72 | 73 | if (existingUserByEmail || existingUserByUsername) { 74 | throw new HttpException(422, { 75 | errors: { 76 | ...(existingUserByEmail ? {email: ['has already been taken']} : {}), 77 | ...(existingUserByUsername ? {username: ['has already been taken']} : {}), 78 | }, 79 | }); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/specifications/frontend/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API 3 | --- 4 | 5 | This project provides you different solutions to test your frontend implementation with an API by: 6 | 7 | - [running our official backend implementation locally](#run-the-official-backend-implementation-locally) 8 | - [hosting your own API](#host-your-own-api) 9 | - [using the API deployed for the official demo](#demo-api) 10 | 11 | ## Run the official backend implementation locally 12 | 13 | The official backend implementation is open-sourced. 14 | You can find the GitHub repository [here](https://github.com/gothinkster/node-express-prisma-v1-official-app). 15 | The Readme will provide you guidances to start the server locally. 16 | 17 | :::info 18 | We encourage you to use this implementation's **main** branch for local tests as the **limited** one includes [limitations](#api-limitations) aimed to protect public-hosted APIs. 19 | ::: 20 | 21 | ## Host your own API 22 | 23 | The official backend implementation includes a [**Deploy to Heroku** button](https://github.com/gothinkster/node-express-prisma-v1-official-app#deploy-to-heroku). 24 | This button provides you a quick and easy way to deploy the API on Heroku for your frontend implementation. 25 | 26 | :::caution 27 | The official backend implementation repository includes two branches: 28 | 29 | - the **main** branch which adheres to the RealWorld backend specs 30 | - the **limited** branch which includes limitations for public-hosted APIs 31 | 32 | The **limited** branch will be more suitable if you plan to host your implementation. 33 | [Here](#api-limitations) is the list of the limitations. 34 | ::: 35 | 36 | ## Demo API 37 | 38 | This project provides you with a public hosted API to test your frontend implementations. 39 | Point your API requests to `https://api.realworld.io/api` and you're good to go! 40 | 41 | ### API Usage 42 | 43 | The API is freely available for public usage but its access is limited to RealWorld usage only: you won't be able to t consume it on its own but with a frontend application. 44 | 45 | ## API Limitations 46 | 47 | :::info 48 | To provide everyone a **safe** and **healthy** experience by not exposing free speech created content, the following limitations have been introduced in 2021 49 | ::: 50 | 51 | The visibility of user content is limited : 52 | 53 | - logged out users see only content created by demo accounts 54 | - logged in users see only their content and the content created by demo accounts 55 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/articles/index.post.ts: -------------------------------------------------------------------------------- 1 | import articleMapper from "~/utils/article.mapper"; 2 | import HttpException from "~/models/http-exception.model"; 3 | import slugify from 'slugify'; 4 | import {definePrivateEventHandler} from "~/auth-event-handler"; 5 | 6 | export default definePrivateEventHandler(async (event, {auth}) => { 7 | const {article} = await readBody(event); 8 | 9 | const { title, description, body, tagList } = article; 10 | const tags = Array.isArray(tagList) ? tagList : []; 11 | 12 | if (!title) { 13 | throw new HttpException(422, { errors: { title: ["can't be blank"] } }); 14 | } 15 | 16 | if (!description) { 17 | throw new HttpException(422, { errors: { description: ["can't be blank"] } }); 18 | } 19 | 20 | if (!body) { 21 | throw new HttpException(422, { errors: { body: ["can't be blank"] } }); 22 | } 23 | 24 | const slug = `${slugify(title)}-${auth.id}`; 25 | 26 | const existingTitle = await usePrisma().article.findUnique({ 27 | where: { 28 | slug, 29 | }, 30 | select: { 31 | slug: true, 32 | }, 33 | }); 34 | 35 | if (existingTitle) { 36 | throw new HttpException(422, { errors: { title: ['must be unique'] } }); 37 | } 38 | 39 | const { 40 | authorId, 41 | id: articleId, 42 | ...createdArticle 43 | } = await usePrisma().article.create({ 44 | data: { 45 | title, 46 | description, 47 | body, 48 | slug, 49 | tagList: { 50 | connectOrCreate: tags.map((tag: string) => ({ 51 | create: { name: tag }, 52 | where: { name: tag }, 53 | })), 54 | }, 55 | author: { 56 | connect: { 57 | id: auth.id, 58 | }, 59 | }, 60 | }, 61 | include: { 62 | tagList: { 63 | select: { 64 | name: true, 65 | }, 66 | }, 67 | author: { 68 | select: { 69 | username: true, 70 | bio: true, 71 | image: true, 72 | followedBy: true, 73 | }, 74 | }, 75 | favoritedBy: true, 76 | _count: { 77 | select: { 78 | favoritedBy: true, 79 | }, 80 | }, 81 | }, 82 | }); 83 | 84 | return {article: articleMapper(createdArticle, auth.id)}; 85 | }); 86 | -------------------------------------------------------------------------------- /apps/documentation/README.md: -------------------------------------------------------------------------------- 1 | # Starlight Starter Kit: Basics 2 | 3 | [![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build) 4 | 5 | ``` 6 | npm create astro@latest -- --template starlight 7 | ``` 8 | 9 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics) 10 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics) 11 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics) 12 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs) 13 | 14 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 15 | 16 | ## 🚀 Project Structure 17 | 18 | Inside of your Astro + Starlight project, you'll see the following folders and files: 19 | 20 | ``` 21 | . 22 | ├── public/ 23 | ├── src/ 24 | │ ├── assets/ 25 | │ ├── content/ 26 | │ │ ├── docs/ 27 | │ │ └── config.ts 28 | │ └── env.d.ts 29 | ├── astro.config.mjs 30 | ├── package.json 31 | └── tsconfig.json 32 | ``` 33 | 34 | Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name. 35 | 36 | Images can be added to `src/assets/` and embedded in Markdown with a relative link. 37 | 38 | Static assets, like favicons, can be placed in the `public/` directory. 39 | 40 | ## 🧞 Commands 41 | 42 | All commands are run from the root of the project, from a terminal: 43 | 44 | | Command | Action | 45 | | :------------------------ | :----------------------------------------------- | 46 | | `npm install` | Installs dependencies | 47 | | `npm run dev` | Starts local dev server at `localhost:4321` | 48 | | `npm run build` | Build your production site to `./dist/` | 49 | | `npm run preview` | Preview your build locally, before deploying | 50 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 51 | | `npm run astro -- --help` | Get help using the Astro CLI | 52 | 53 | ## 👀 Want to learn more? 54 | 55 | Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat). 56 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/implementation-creation/expectations.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Expectations 3 | --- 4 | 5 | ## Remember: Keep your codebases _simple_, yet _robust_. 6 | 7 | If a new developer to your framework comes along and takes longer than 10 minutes to grasp the high-level architecture, it's likely that you went a little overboard in the engineering department. 8 | 9 | Alternatively, you should _never_ forgo following fundamental best practices for the sake of simplicity, lest we teach that same newbie dev the _wrong_ way of doing things. 10 | 11 | The quality & architecture of Conduit implementations should reflect something similar to an early-stage startup's MVP: functionally complete & stable, but not unnecessarily over-engineered. 12 | 13 | ## To write tests, or to not write tests? 14 | 15 | **TL;DR** — we require a minimum of **one** unit test with every repo, but we'd definitely prefer all of them to include excellent testing coverage if the maintainers are willing to add it (or if someone in the community is kind enough to make a pull request :) 16 | 17 | We believe that tests are a good concept, and we are big supporters of TDD in general. Building Conduit implementations without complete testing coverage, on the other hand, is a significant time commitment in and of itself, therefore we didn't include it in the spec at first since we believed that if people wanted it, it would be a fantastic "extra credit" aim for the repo. For example, a request for unit tests was made in our Angular 2 repo, and several fantastic community members are presently working on a PR to address it. 18 | 19 | Another reason we didn’t include them in the spec is from the "Golden Rule" above: 20 | 21 | > The quality & architecture of Conduit implementations should reflect something similar to an early-stage startup's MVP: functionally complete & stable, but not unnecessarily over-engineered. 22 | 23 | Most startups we know that work in consumer-facing apps (like Conduit) don’t apply TDD/testing until they have a solid product-market fit, which is smart because they then spend most of their time iterating on product & UI and thus are far more likely to find PMF. 24 | 25 | This doesn’t mean that TDD/testing === over-engineering, but in certain circumstances that statement does evaluate true (ex: consumer product finding PMF, side-projects, robust prototypes, etc). 26 | 27 | That said, we do _prefer_ that every repo includes excellent tests that are exemplary of TDD/testing with that framework 👍 28 | 29 | ## Other Expectations 30 | 31 | - All the required features (see specs) should be implemented. 32 | - You should publish your implementation on a dedicated GitHub repository with the "Issues" section open. 33 | - You should provide a README that presents an overview of your implementation and explains how to run it locally. 34 | - The library/framework you are using should have at least 300 GitHub stars. 35 | - You should do your best to keep your implementation up to date. 36 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/articles/index.get.ts: -------------------------------------------------------------------------------- 1 | import articleMapper from "~/utils/article.mapper"; 2 | import {definePrivateEventHandler} from "~/auth-event-handler"; 3 | 4 | export default definePrivateEventHandler(async (event, {auth}) => { 5 | const query = getQuery(event); 6 | 7 | const andQueries = buildFindAllQuery(query, auth); 8 | const articlesCount = await usePrisma().article.count({ 9 | where: { 10 | AND: andQueries, 11 | }, 12 | }); 13 | 14 | const articles = await usePrisma().article.findMany({ 15 | omit: { 16 | body: true, 17 | updatedAt: true, 18 | }, 19 | where: { AND: andQueries }, 20 | orderBy: { 21 | createdAt: 'desc', 22 | }, 23 | skip: Number(query.offset) || 0, 24 | take: Number(query.limit) || 10, 25 | include: { 26 | tagList: { 27 | select: { 28 | name: true, 29 | }, 30 | }, 31 | author: { 32 | select: { 33 | username: true, 34 | image: true, 35 | followedBy: true, 36 | }, 37 | }, 38 | favoritedBy: true, 39 | _count: { 40 | select: { 41 | favoritedBy: true, 42 | }, 43 | }, 44 | }, 45 | }); 46 | 47 | return { 48 | articles: articles.map((article: any) => articleMapper(article, auth.id)), 49 | articlesCount, 50 | }; 51 | }, {requireAuth: false}); 52 | 53 | const buildFindAllQuery = (query: any, auth: {id: number} | undefined) => { 54 | const queries: any = []; 55 | const orAuthorQuery = []; 56 | const andAuthorQuery = []; 57 | 58 | orAuthorQuery.push({ 59 | demo: { 60 | equals: true, 61 | }, 62 | }); 63 | 64 | if (auth?.id) { 65 | orAuthorQuery.push({ 66 | id: { 67 | equals: auth?.id, 68 | }, 69 | }); 70 | } 71 | 72 | if ('author' in query) { 73 | andAuthorQuery.push({ 74 | username: { 75 | equals: query.author, 76 | }, 77 | }); 78 | } 79 | 80 | const authorQuery = { 81 | author: { 82 | OR: orAuthorQuery, 83 | AND: andAuthorQuery, 84 | }, 85 | }; 86 | 87 | queries.push(authorQuery); 88 | 89 | if ('tag' in query) { 90 | queries.push({ 91 | tagList: { 92 | some: { 93 | name: query.tag, 94 | }, 95 | }, 96 | }); 97 | } 98 | 99 | if ('favorited' in query) { 100 | queries.push({ 101 | favoritedBy: { 102 | some: { 103 | username: { 104 | equals: query.favorited, 105 | }, 106 | }, 107 | }, 108 | }); 109 | } 110 | 111 | return queries; 112 | }; 113 | -------------------------------------------------------------------------------- /apps/api/server/routes/api/articles/[slug]/index.put.ts: -------------------------------------------------------------------------------- 1 | import HttpException from "~/models/http-exception.model"; 2 | import articleMapper from "~/utils/article.mapper"; 3 | import slugify from 'slugify'; 4 | import {definePrivateEventHandler} from "~/auth-event-handler"; 5 | 6 | export default definePrivateEventHandler(async (event, {auth}) => { 7 | const {article} = await readBody(event); 8 | const slug = getRouterParam(event, 'slug'); 9 | 10 | let newSlug = null; 11 | 12 | const existingArticle = await usePrisma().article.findFirst({ 13 | where: { 14 | slug, 15 | }, 16 | select: { 17 | author: { 18 | select: { 19 | id: true, 20 | username: true, 21 | }, 22 | }, 23 | }, 24 | }); 25 | 26 | if (!existingArticle) { 27 | throw new HttpException(404, {}); 28 | } 29 | 30 | if (existingArticle.author.id !== auth.id) { 31 | throw new HttpException(403, { 32 | message: 'You are not authorized to update this article', 33 | }); 34 | } 35 | 36 | if (article.title) { 37 | newSlug = `${slugify(article.title)}-${auth.id}`; 38 | 39 | if (newSlug !== slug) { 40 | const existingTitle = await usePrisma().article.findFirst({ 41 | where: { 42 | slug: newSlug, 43 | }, 44 | select: { 45 | slug: true, 46 | }, 47 | }); 48 | 49 | if (existingTitle) { 50 | throw new HttpException(422, { errors: { title: ['must be unique'] } }); 51 | } 52 | } 53 | } 54 | 55 | const tagList = 56 | Array.isArray(article.tagList) && article.tagList?.length 57 | ? article.tagList.map((tag: string) => ({ 58 | create: { name: tag }, 59 | where: { name: tag }, 60 | })) 61 | : []; 62 | 63 | await disconnectArticlesTags(slug); 64 | 65 | const updatedArticle = await usePrisma().article.update({ 66 | where: { 67 | slug, 68 | }, 69 | data: { 70 | ...(article.title ? { title: article.title } : {}), 71 | ...(article.body ? { body: article.body } : {}), 72 | ...(article.description ? { description: article.description } : {}), 73 | ...(newSlug ? { slug: newSlug } : {}), 74 | updatedAt: new Date(), 75 | tagList: { 76 | connectOrCreate: tagList, 77 | }, 78 | }, 79 | include: { 80 | tagList: { 81 | select: { 82 | name: true, 83 | }, 84 | }, 85 | author: { 86 | select: { 87 | username: true, 88 | bio: true, 89 | image: true, 90 | followedBy: true, 91 | }, 92 | }, 93 | favoritedBy: true, 94 | _count: { 95 | select: { 96 | favoritedBy: true, 97 | }, 98 | }, 99 | }, 100 | }); 101 | 102 | return {article: articleMapper(updatedArticle, auth.id)}; 103 | }); 104 | 105 | const disconnectArticlesTags = async (slug: string) => { 106 | await usePrisma().article.update({ 107 | where: { 108 | slug, 109 | }, 110 | data: { 111 | tagList: { 112 | set: [], 113 | }, 114 | }, 115 | }); 116 | }; 117 | -------------------------------------------------------------------------------- /apps/api/prisma/migrations/20241009081140_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Article" ( 3 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 4 | "slug" TEXT NOT NULL, 5 | "title" TEXT NOT NULL, 6 | "description" TEXT NOT NULL, 7 | "body" TEXT NOT NULL, 8 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | "authorId" INTEGER NOT NULL, 11 | CONSTRAINT "Article_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE 12 | ); 13 | 14 | -- CreateTable 15 | CREATE TABLE "Comment" ( 16 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 17 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 18 | "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 19 | "body" TEXT NOT NULL, 20 | "articleId" INTEGER NOT NULL, 21 | "authorId" INTEGER NOT NULL, 22 | CONSTRAINT "Comment_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article" ("id") ON DELETE CASCADE ON UPDATE CASCADE, 23 | CONSTRAINT "Comment_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE 24 | ); 25 | 26 | -- CreateTable 27 | CREATE TABLE "Tag" ( 28 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 29 | "name" TEXT NOT NULL 30 | ); 31 | 32 | -- CreateTable 33 | CREATE TABLE "User" ( 34 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 35 | "email" TEXT NOT NULL, 36 | "username" TEXT NOT NULL, 37 | "password" TEXT NOT NULL, 38 | "image" TEXT DEFAULT 'https://api.realworld.io/images/smiley-cyrus.jpeg', 39 | "bio" TEXT, 40 | "demo" BOOLEAN NOT NULL DEFAULT false 41 | ); 42 | 43 | -- CreateTable 44 | CREATE TABLE "_ArticleToTag" ( 45 | "A" INTEGER NOT NULL, 46 | "B" INTEGER NOT NULL, 47 | CONSTRAINT "_ArticleToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "Article" ("id") ON DELETE CASCADE ON UPDATE CASCADE, 48 | CONSTRAINT "_ArticleToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag" ("id") ON DELETE CASCADE ON UPDATE CASCADE 49 | ); 50 | 51 | -- CreateTable 52 | CREATE TABLE "_UserFavorites" ( 53 | "A" INTEGER NOT NULL, 54 | "B" INTEGER NOT NULL, 55 | CONSTRAINT "_UserFavorites_A_fkey" FOREIGN KEY ("A") REFERENCES "Article" ("id") ON DELETE CASCADE ON UPDATE CASCADE, 56 | CONSTRAINT "_UserFavorites_B_fkey" FOREIGN KEY ("B") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE 57 | ); 58 | 59 | -- CreateTable 60 | CREATE TABLE "_UserFollows" ( 61 | "A" INTEGER NOT NULL, 62 | "B" INTEGER NOT NULL, 63 | CONSTRAINT "_UserFollows_A_fkey" FOREIGN KEY ("A") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE, 64 | CONSTRAINT "_UserFollows_B_fkey" FOREIGN KEY ("B") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE 65 | ); 66 | 67 | -- CreateIndex 68 | CREATE UNIQUE INDEX "Article_slug_key" ON "Article"("slug"); 69 | 70 | -- CreateIndex 71 | CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name"); 72 | 73 | -- CreateIndex 74 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 75 | 76 | -- CreateIndex 77 | CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); 78 | 79 | -- CreateIndex 80 | CREATE UNIQUE INDEX "_ArticleToTag_AB_unique" ON "_ArticleToTag"("A", "B"); 81 | 82 | -- CreateIndex 83 | CREATE INDEX "_ArticleToTag_B_index" ON "_ArticleToTag"("B"); 84 | 85 | -- CreateIndex 86 | CREATE UNIQUE INDEX "_UserFavorites_AB_unique" ON "_UserFavorites"("A", "B"); 87 | 88 | -- CreateIndex 89 | CREATE INDEX "_UserFavorites_B_index" ON "_UserFavorites"("B"); 90 | 91 | -- CreateIndex 92 | CREATE UNIQUE INDEX "_UserFollows_AB_unique" ON "_UserFollows"("A", "B"); 93 | 94 | -- CreateIndex 95 | CREATE INDEX "_UserFollows_B_index" ON "_UserFollows"("B"); 96 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/community/authors.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Authors 3 | --- 4 | 5 | # Who currently maintain the project 6 | 7 | #### [Gérôme Grignon](https://github.com/geromegrignon) - Maintainer 8 | 9 | 10 | 11 | Gérôme is a Software Engineer at Lucca. He's an open-source enthusiast.

12 | 13 | #### [Manuel Vila](https://github.com/mvila) - Maintainer 14 | 15 | 16 | 17 | Manuel is an independent Software Engineer, creator of the [Layr framework](https://layrjs.com) and the [CodebaseShow website](https://codebase.show/).

18 | 19 | 20 | 21 | # Who create it 22 | 23 | RealWorld would not be possible without the [open source community](https://realworld-docs.netlify.app/docs/community/special-thanks) continuously helping push the project forward. In addition, the former team was composed of: 24 | 25 | #### [Anish Karandikar](https://github.com/anishkny) - Core Maintainer 26 | 27 | 28 | 29 | MathWorker, ex-Google, ex-Computational Fluid Dynamicist, forever lover of tech & humanities ❤️ 30 | 31 | #### [Cameron Chapman](https://github.com/Cameron-C-Chapman) - Core Maintainer 32 | 33 | 34 | 35 | Cameron Chapman is a Software Engineer at FanThreeSixty. He's an open source enthusiast and is helping to teach a local web development boot camp at Kansas University. 36 | 37 | #### [Eric Simons](https://twitter.com/ericsimons40) - Founder/Maintainer 38 | 39 | 40 | 41 | Eric is a Software Engineer, UI Designer, and author of many technical books & tutorials. He oversees the project direction, maintenance and organizes the planning and development efforts of the team. 42 | 43 | #### [Albert Pai](https://twitter.com/iamalbertpai) - Founder/Maintainer 44 | 45 | 46 | 47 | Albert is a Software Engineer, DevOps ninja, and author of many technical books & tutorials. He oversees the project direction, maintenance and organizes the planning and development efforts of the team. 48 | 49 | #### [Thinkster](https://twitter.com/gothinkster) - Funded 50 | 51 | 52 | 53 | [Thinkster](https://thinkster.io) creates high quality resources that help Javascript developers succeed. The RealWorld project wouldn't exist without their funding, so please consider investing in [a Pro subscription](https://thinkster.io/pro) to help support us! 54 | 55 | #### [James Brewer](https://twitter.com/brwr_) - Admin 56 | 57 | 58 | 59 | James is a Software Engineer at Square and a contributor to the Django project. He created & maintained the RW Django codebase and continually provides guidance for the RealWorld project itself. 60 | 61 | #### [Sandeesh S.](https://github.com/sandeesh) - Admin 62 | 63 | 64 | 65 | Full stack developer, Laravel enthusiast, Digital marketing specialist and an avid gamer. 66 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/specifications/backend/api-response-format.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API response format 3 | --- 4 | 5 | ## JSON Objects returned by API: 6 | 7 | Make sure the right content type like `Content-Type: application/json; charset=utf-8` is correctly returned. 8 | 9 | ### Users (for authentication) 10 | 11 | ```JSON 12 | { 13 | "user": { 14 | "email": "jake@jake.jake", 15 | "token": "jwt.token.here", 16 | "username": "jake", 17 | "bio": "I work at statefarm", 18 | "image": null 19 | } 20 | } 21 | ``` 22 | 23 | ### Profile 24 | 25 | ```JSON 26 | { 27 | "profile": { 28 | "username": "jake", 29 | "bio": "I work at statefarm", 30 | "image": "https://api.realworld.io/images/smiley-cyrus.jpg", 31 | "following": false 32 | } 33 | } 34 | ``` 35 | 36 | ### Single Article 37 | 38 | ```JSON 39 | { 40 | "article": { 41 | "slug": "how-to-train-your-dragon", 42 | "title": "How to train your dragon", 43 | "description": "Ever wonder how?", 44 | "body": "It takes a Jacobian", 45 | "tagList": ["dragons", "training"], 46 | "createdAt": "2016-02-18T03:22:56.637Z", 47 | "updatedAt": "2016-02-18T03:48:35.824Z", 48 | "favorited": false, 49 | "favoritesCount": 0, 50 | "author": { 51 | "username": "jake", 52 | "bio": "I work at statefarm", 53 | "image": "https://i.stack.imgur.com/xHWG8.jpg", 54 | "following": false 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | ### Multiple Articles 61 | 62 | :::caution 63 | Starting from the 2024/08/16, the endpoints retrieving a list of articles do no longer return the body of an article for performance reasons. 64 | It affcts: 65 | - `GET /api/articles` 66 | - `GET /api/articles/feed` 67 | ::: 68 | 69 | ```JSON 70 | { 71 | "articles":[{ 72 | "slug": "how-to-train-your-dragon", 73 | "title": "How to train your dragon", 74 | "description": "Ever wonder how?", 75 | "tagList": ["dragons", "training"], 76 | "createdAt": "2016-02-18T03:22:56.637Z", 77 | "updatedAt": "2016-02-18T03:48:35.824Z", 78 | "favorited": false, 79 | "favoritesCount": 0, 80 | "author": { 81 | "username": "jake", 82 | "bio": "I work at statefarm", 83 | "image": "https://i.stack.imgur.com/xHWG8.jpg", 84 | "following": false 85 | } 86 | }, { 87 | "slug": "how-to-train-your-dragon-2", 88 | "title": "How to train your dragon 2", 89 | "description": "So toothless", 90 | "tagList": ["dragons", "training"], 91 | "createdAt": "2016-02-18T03:22:56.637Z", 92 | "updatedAt": "2016-02-18T03:48:35.824Z", 93 | "favorited": false, 94 | "favoritesCount": 0, 95 | "author": { 96 | "username": "jake", 97 | "bio": "I work at statefarm", 98 | "image": "https://i.stack.imgur.com/xHWG8.jpg", 99 | "following": false 100 | } 101 | }], 102 | "articlesCount": 2 103 | } 104 | ``` 105 | 106 | ### Single Comment 107 | 108 | ```JSON 109 | { 110 | "comment": { 111 | "id": 1, 112 | "createdAt": "2016-02-18T03:22:56.637Z", 113 | "updatedAt": "2016-02-18T03:22:56.637Z", 114 | "body": "It takes a Jacobian", 115 | "author": { 116 | "username": "jake", 117 | "bio": "I work at statefarm", 118 | "image": "https://i.stack.imgur.com/xHWG8.jpg", 119 | "following": false 120 | } 121 | } 122 | } 123 | ``` 124 | 125 | ### Multiple Comments 126 | 127 | ```JSON 128 | { 129 | "comments": [{ 130 | "id": 1, 131 | "createdAt": "2016-02-18T03:22:56.637Z", 132 | "updatedAt": "2016-02-18T03:22:56.637Z", 133 | "body": "It takes a Jacobian", 134 | "author": { 135 | "username": "jake", 136 | "bio": "I work at statefarm", 137 | "image": "https://i.stack.imgur.com/xHWG8.jpg", 138 | "following": false 139 | } 140 | }] 141 | } 142 | ``` 143 | 144 | ### List of Tags 145 | 146 | ```JSON 147 | { 148 | "tags": [ 149 | "reactjs", 150 | "angularjs" 151 | ] 152 | } 153 | ``` 154 | -------------------------------------------------------------------------------- /media/mobile_icons/ios/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "iphone", 5 | "size": "20x20", 6 | "scale": "2x", 7 | "filename": "Icon-App-20x20@2x.png" 8 | }, 9 | { 10 | "idiom": "iphone", 11 | "size": "20x20", 12 | "scale": "3x", 13 | "filename": "Icon-App-20x20@3x.png" 14 | }, 15 | { 16 | "idiom": "iphone", 17 | "size": "29x29", 18 | "scale": "1x", 19 | "filename": "Icon-App-29x29@1x.png" 20 | }, 21 | { 22 | "idiom": "iphone", 23 | "size": "29x29", 24 | "scale": "2x", 25 | "filename": "Icon-App-29x29@2x.png" 26 | }, 27 | { 28 | "idiom": "iphone", 29 | "size": "29x29", 30 | "scale": "3x", 31 | "filename": "Icon-App-29x29@3x.png" 32 | }, 33 | { 34 | "idiom": "iphone", 35 | "size": "40x40", 36 | "scale": "1x", 37 | "filename": "Icon-App-40x40@1x.png" 38 | }, 39 | { 40 | "idiom": "iphone", 41 | "size": "40x40", 42 | "scale": "2x", 43 | "filename": "Icon-App-40x40@2x.png" 44 | }, 45 | { 46 | "idiom": "iphone", 47 | "size": "40x40", 48 | "scale": "3x", 49 | "filename": "Icon-App-40x40@3x.png" 50 | }, 51 | { 52 | "idiom": "iphone", 53 | "size": "57x57", 54 | "scale": "1x", 55 | "filename": "Icon-App-57x57@1x.png" 56 | }, 57 | { 58 | "idiom": "iphone", 59 | "size": "57x57", 60 | "scale": "2x", 61 | "filename": "Icon-App-57x57@2x.png" 62 | }, 63 | { 64 | "idiom": "iphone", 65 | "size": "60x60", 66 | "scale": "1x", 67 | "filename": "Icon-App-60x60@1x.png" 68 | }, 69 | { 70 | "idiom": "iphone", 71 | "size": "60x60", 72 | "scale": "2x", 73 | "filename": "Icon-App-60x60@2x.png" 74 | }, 75 | { 76 | "idiom": "iphone", 77 | "size": "60x60", 78 | "scale": "3x", 79 | "filename": "Icon-App-60x60@3x.png" 80 | }, 81 | { 82 | "idiom": "iphone", 83 | "size": "76x76", 84 | "scale": "1x", 85 | "filename": "Icon-App-76x76@1x.png" 86 | }, 87 | { 88 | "idiom": "ipad", 89 | "size": "20x20", 90 | "scale": "1x", 91 | "filename": "Icon-App-20x20@1x.png" 92 | }, 93 | { 94 | "idiom": "ipad", 95 | "size": "20x20", 96 | "scale": "2x", 97 | "filename": "Icon-App-20x20@2x.png" 98 | }, 99 | { 100 | "idiom": "ipad", 101 | "size": "29x29", 102 | "scale": "1x", 103 | "filename": "Icon-App-29x29@1x.png" 104 | }, 105 | { 106 | "idiom": "ipad", 107 | "size": "29x29", 108 | "scale": "2x", 109 | "filename": "Icon-App-29x29@2x.png" 110 | }, 111 | { 112 | "idiom": "ipad", 113 | "size": "40x40", 114 | "scale": "1x", 115 | "filename": "Icon-App-40x40@1x.png" 116 | }, 117 | { 118 | "idiom": "ipad", 119 | "size": "40x40", 120 | "scale": "2x", 121 | "filename": "Icon-App-40x40@2x.png" 122 | }, 123 | { 124 | "size": "50x50", 125 | "idiom": "ipad", 126 | "filename": "Icon-Small-50x50@1x.png", 127 | "scale": "1x" 128 | }, 129 | { 130 | "size": "50x50", 131 | "idiom": "ipad", 132 | "filename": "Icon-Small-50x50@2x.png", 133 | "scale": "2x" 134 | }, 135 | { 136 | "idiom": "ipad", 137 | "size": "72x72", 138 | "scale": "1x", 139 | "filename": "Icon-App-72x72@1x.png" 140 | }, 141 | { 142 | "idiom": "ipad", 143 | "size": "72x72", 144 | "scale": "2x", 145 | "filename": "Icon-App-72x72@2x.png" 146 | }, 147 | { 148 | "idiom": "ipad", 149 | "size": "76x76", 150 | "scale": "1x", 151 | "filename": "Icon-App-76x76@1x.png" 152 | }, 153 | { 154 | "idiom": "ipad", 155 | "size": "76x76", 156 | "scale": "2x", 157 | "filename": "Icon-App-76x76@2x.png" 158 | }, 159 | { 160 | "idiom": "ipad", 161 | "size": "76x76", 162 | "scale": "3x", 163 | "filename": "Icon-App-76x76@3x.png" 164 | }, 165 | { 166 | "idiom": "ipad", 167 | "size": "83.5x83.5", 168 | "scale": "2x", 169 | "filename": "Icon-App-83.5x83.5@2x.png" 170 | } 171 | ], 172 | "info": { 173 | "version": 1, 174 | "author": "makeappicon" 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | hello@thinkster.io. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /apps/documentation/src/assets/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/documentation/src/content/docs/specifications/backend/endpoints.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Endpoints 3 | --- 4 | 5 | ### Authentication Header: 6 | 7 | You can read the authentication header from the headers of the request 8 | 9 | `Authorization: Token jwt.token.here` 10 | 11 | ### Authentication: 12 | 13 | `POST /api/users/login` 14 | 15 | Example request body: 16 | 17 | ```JSON 18 | { 19 | "user":{ 20 | "email": "jake@jake.jake", 21 | "password": "jakejake" 22 | } 23 | } 24 | ``` 25 | 26 | No authentication required, returns a [User](/specifications/backend/api-response-format.md#users-for-authentication) 27 | 28 | Required fields: `email`, `password` 29 | 30 | ### Registration: 31 | 32 | `POST /api/users` 33 | 34 | Example request body: 35 | 36 | ```JSON 37 | { 38 | "user":{ 39 | "username": "Jacob", 40 | "email": "jake@jake.jake", 41 | "password": "jakejake" 42 | } 43 | } 44 | ``` 45 | 46 | No authentication required, returns a [User](/specifications/backend/api-response-format.md#users-for-authentication) 47 | 48 | Required fields: `email`, `username`, `password` 49 | 50 | ### Get Current User 51 | 52 | `GET /api/user` 53 | 54 | Authentication required, returns a [User](/specifications/backend/api-response-format.md#users-for-authentication) that's the current user 55 | 56 | ### Update User 57 | 58 | `PUT /api/user` 59 | 60 | Example request body: 61 | 62 | ```JSON 63 | { 64 | "user":{ 65 | "email": "jake@jake.jake", 66 | "bio": "I like to skateboard", 67 | "image": "https://i.stack.imgur.com/xHWG8.jpg" 68 | } 69 | } 70 | ``` 71 | 72 | Authentication required, returns the [User](/specifications/backend/api-response-format.md#users-for-authentication) 73 | 74 | Accepted fields: `email`, `username`, `password`, `image`, `bio` 75 | 76 | ### Get Profile 77 | 78 | `GET /api/profiles/:username` 79 | 80 | Authentication optional, returns a [Profile](/specifications/backend/api-response-format.md#profile) 81 | 82 | ### Follow user 83 | 84 | `POST /api/profiles/:username/follow` 85 | 86 | Authentication required, returns a [Profile](/specifications/backend/api-response-format.md#profile) 87 | 88 | No additional parameters required 89 | 90 | ### Unfollow user 91 | 92 | `DELETE /api/profiles/:username/follow` 93 | 94 | Authentication required, returns a [Profile](/specifications/backend/api-response-format.md#profile) 95 | 96 | No additional parameters required 97 | 98 | ### List Articles 99 | 100 | `GET /api/articles` 101 | 102 | Returns most recent articles globally by default, provide `tag`, `author` or `favorited` query parameter to filter results 103 | 104 | Query Parameters: 105 | 106 | Filter by tag: 107 | 108 | `?tag=AngularJS` 109 | 110 | Filter by author: 111 | 112 | `?author=jake` 113 | 114 | Favorited by user: 115 | 116 | `?favorited=jake` 117 | 118 | Limit number of articles (default is 20): 119 | 120 | `?limit=20` 121 | 122 | Offset/skip number of articles (default is 0): 123 | 124 | `?offset=0` 125 | 126 | Authentication optional, will return [multiple articles](/specifications/backend/api-response-format.md#multiple-articles), ordered by most recent first 127 | 128 | ### Feed Articles 129 | 130 | `GET /api/articles/feed` 131 | 132 | Can also take `limit` and `offset` query parameters like [List Articles](/specifications/backend/api-response-format.md#list-articles) 133 | 134 | Authentication required, will return [multiple articles](/specifications/backend/api-response-format.md#multiple-articles) created by followed users, ordered by most recent first. 135 | 136 | ### Get Article 137 | 138 | `GET /api/articles/:slug` 139 | 140 | No authentication required, will return [single article](/specifications/backend/api-response-format.md#single-article) 141 | 142 | ### Create Article 143 | 144 | `POST /api/articles` 145 | 146 | Example request body: 147 | 148 | ```JSON 149 | { 150 | "article": { 151 | "title": "How to train your dragon", 152 | "description": "Ever wonder how?", 153 | "body": "You have to believe", 154 | "tagList": ["reactjs", "angularjs", "dragons"] 155 | } 156 | } 157 | ``` 158 | 159 | Authentication required, will return an [Article](/specifications/backend/api-response-format.md#single-article) 160 | 161 | Required fields: `title`, `description`, `body` 162 | 163 | Optional fields: `tagList` as an array of Strings 164 | 165 | ### Update Article 166 | 167 | `PUT /api/articles/:slug` 168 | 169 | Example request body: 170 | 171 | ```JSON 172 | { 173 | "article": { 174 | "title": "Did you train your dragon?" 175 | } 176 | } 177 | ``` 178 | 179 | Authentication required, returns the updated [Article](/specifications/backend/api-response-format.md#single-article) 180 | 181 | Optional fields: `title`, `description`, `body` 182 | 183 | The `slug` also gets updated when the `title` is changed 184 | 185 | ### Delete Article 186 | 187 | `DELETE /api/articles/:slug` 188 | 189 | Authentication required 190 | 191 | ### Add Comments to an Article 192 | 193 | `POST /api/articles/:slug/comments` 194 | 195 | Example request body: 196 | 197 | ```JSON 198 | { 199 | "comment": { 200 | "body": "His name was my name too." 201 | } 202 | } 203 | ``` 204 | 205 | Authentication required, returns the created [Comment](/specifications/backend/api-response-format.md#single-comment) 206 | 207 | Required field: `body` 208 | 209 | ### Get Comments from an Article 210 | 211 | `GET /api/articles/:slug/comments` 212 | 213 | Authentication optional, returns [multiple comments](/specifications/backend/api-response-format.md#multiple-comments) 214 | 215 | ### Delete Comment 216 | 217 | `DELETE /api/articles/:slug/comments/:id` 218 | 219 | Authentication required 220 | 221 | ### Favorite Article 222 | 223 | `POST /api/articles/:slug/favorite` 224 | 225 | Authentication required, returns the [Article](/specifications/backend/api-response-format.md#single-article) 226 | 227 | No additional parameters required 228 | 229 | ### Unfavorite Article 230 | 231 | `DELETE /api/articles/:slug/favorite` 232 | 233 | Authentication required, returns the [Article](/specifications/backend/api-response-format.md#single-article) 234 | 235 | No additional parameters required 236 | 237 | ### Get Tags 238 | 239 | `GET /api/tags` 240 | 241 | No authentication required, returns a [List of Tags](/specifications/backend/api-response-format.md#list-of-tags) 242 | -------------------------------------------------------------------------------- /apps/documentation/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'astro/config'; 2 | import starlight from '@astrojs/starlight'; 3 | import tailwind from "@astrojs/tailwind"; 4 | import react from "@astrojs/react"; 5 | 6 | /** 7 | * A Vite plugin that removes `.md` extensions from URLs during the build process. 8 | * 9 | * This plugin is useful when working with Markdown files in a static site generator 10 | * like Astro or Vite. It allows URLs to be served without the `.md` extension in 11 | * the final build, making the URLs cleaner (e.g., `/docs/page` instead of `/docs/page.md`). 12 | * 13 | * ## Example Usage 14 | * ```js 15 | * import { defineConfig } from 'astro/config'; 16 | * 17 | * export default defineConfig({ 18 | * vite: { 19 | * plugins: [removeMdExtension()], 20 | * }, 21 | * }); 22 | * ``` 23 | * 24 | * @typedef {object} VitePlugin 25 | * @property {string} name - The name of the plugin, in this case, `remove-md-extension`. 26 | * @property {string} enforce - Specifies the plugin's enforcement stage, set to `pre` 27 | * to ensure that this plugin runs before other transformations during the build. 28 | * @property {function} transform - The function that processes each file, removing 29 | * `.md` extensions from the file content. It is called on every file during the build process. 30 | * 31 | * @returns {VitePlugin} A Vite-compatible plugin object that contains the `name`, 32 | * `enforce`, and `transform` properties, implementing the plugin functionality. 33 | * 34 | * ## Vite Plugin Object Structure 35 | * - `name`: The name of the plugin (`remove-md-extension`). 36 | * - `enforce`: Ensures the plugin runs early (`pre` stage). 37 | * - `transform(code: string, id: string): string`: The function that processes the content of `.md` files. 38 | * 39 | * @param {string} code - The content of the file being processed (e.g., the raw Markdown). 40 | * @param {string} id - The file identifier (usually the file path), used to check if the file ends in `.md`. 41 | * @returns {string} The modified file content, with any `.md` extensions in URLs removed. 42 | */ 43 | function removeMdExtension() { 44 | return { 45 | name: 'remove-md-extension', 46 | enforce: 'pre', 47 | transform(code, id) { 48 | if (id.endsWith('.md')) { 49 | return code.replace(/\.md/g, ''); 50 | } 51 | return code; 52 | }, 53 | }; 54 | }; 55 | 56 | // https://astro.build/config 57 | export default defineConfig({ 58 | integrations: [starlight({ 59 | title: 'RealWorld', 60 | social: { 61 | github: 'https://github.com/gothinkster/realworld' 62 | }, 63 | customCss: [ 64 | './src/tailwind.css', 65 | ], 66 | sidebar: [ 67 | { 68 | label: 'Implementation creation', 69 | items: [ 70 | { 71 | label: 'Introduction', 72 | slug: 'implementation-creation/introduction', 73 | }, 74 | { 75 | label: 'Features', 76 | slug: 'implementation-creation/features', 77 | }, 78 | { 79 | label: 'Expectations', 80 | slug: 'implementation-creation/expectations', 81 | } 82 | 83 | ] 84 | }, 85 | { 86 | label: 'Specifications', 87 | items: [ 88 | { 89 | label: 'Frontend specifications', 90 | items: [ 91 | { 92 | label: 'Templates', 93 | slug: 'specifications/frontend/templates', 94 | }, 95 | { 96 | label: 'Styles', 97 | slug: 'specifications/frontend/styles', 98 | }, 99 | { 100 | label: 'Routing', 101 | slug: 'specifications/frontend/routing', 102 | }, 103 | { 104 | label: 'API', 105 | slug: 'specifications/frontend/api', 106 | }, 107 | { 108 | label: 'Tests', 109 | slug: 'specifications/frontend/tests', 110 | } 111 | ] 112 | }, 113 | { 114 | label: 'Backend specifications', 115 | items: [ 116 | { 117 | label: 'Introduction', 118 | slug: 'specifications/backend/introduction', 119 | }, 120 | { 121 | label: 'Endpoints', 122 | slug: 'specifications/backend/endpoints', 123 | }, 124 | { 125 | label: 'API response format', 126 | slug: 'specifications/backend/api-response-format', 127 | }, 128 | { 129 | label: 'CORS', 130 | slug: 'specifications/backend/cors', 131 | }, 132 | { 133 | label: 'Error handling', 134 | slug: 'specifications/backend/error-handling', 135 | }, 136 | { 137 | label: 'Postman', 138 | slug: 'specifications/backend/postman', 139 | }, 140 | { 141 | label: 'Tests', 142 | slug: 'specifications/backend/tests', 143 | } 144 | ] 145 | }, 146 | { 147 | label: 'Mobile specifications', 148 | slug: 'specifications/mobile-specs/introduction' 149 | } 150 | ] 151 | }, 152 | { 153 | label: 'Community', 154 | items: [ 155 | // Each item here is one entry in the navigation menu. 156 | { 157 | label: 'Authors', 158 | slug: 'community/authors', 159 | }, 160 | { 161 | label: 'Resources', 162 | slug: 'community/resources', 163 | }, 164 | { 165 | label: 'Special Thanks', 166 | slug: 'community/special-thanks', 167 | } 168 | ] 169 | } 170 | ] 171 | }), tailwind({ 172 | applyBaseStyles: false, 173 | })], 174 | vite: { 175 | plugins: [removeMdExtension()], 176 | } 177 | }); 178 | -------------------------------------------------------------------------------- /apps/documentation/src/assets/img/codebaseshow-logo.svg: -------------------------------------------------------------------------------- 1 | logo -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to RealWorld 2 | 3 | We would love for you to contribute to RealWorld and help make it even better than it is 4 | today! As a contributor, here are the guidelines we would like you to follow: 5 | 6 | - [Code of Conduct](#coc) 7 | - [Question or Problem?](#question) 8 | - [Issues and Bugs](#issue) 9 | - [Feature Requests](#feature) 10 | - [Submission Guidelines](#submit) 11 | - [Coding Rules](#rules) 12 | - [Commit Message Guidelines](#commit) 13 | 14 | ## Code of Conduct 15 | 16 | Help us keep RealWorld open and inclusive. Please read and follow our [Code of Conduct][coc]. 17 | 18 | ## Got a Question or Problem? 19 | 20 | Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests. 21 | For open discussions, we encourage you to use the [Github Discussions][github-discussions] channels. 22 | 23 | ## Interested in creating Conduit for your framework? 24 | 25 | To create an official implementation of Conduit, check out our [Github Discussions](https://github.com/gothinkster/realworld/discussions/categories/wip-implementations) and see if anyone else has requested and/or is already working on your framework. 26 | If not, feel free to start working on one! 27 | 28 | Start [here][github-spec]! 29 | 30 | ## Found a Bug? 31 | 32 | If you find a bug in the project, you can help us by 33 | [submitting an issue][github-issue] to our [GitHub Repository][github]. Even better, you can 34 | [submit a Pull Request](#submit-pr) with a fix. 35 | 36 | ## Missing a Feature? 37 | 38 | You can _request_ a new feature by [submitting an issue](#submit-issue) to our GitHub 39 | repository. 40 | 41 | If you would like to _implement_ a new feature, please submit an issue with 42 | a proposal for your work **FIRST**, to be sure that we can use it. 43 | Please consider what kind of change it is: 44 | 45 | - For a **Major Feature**, first open an issue and outline your proposal so that it can be 46 | discussed. This will also allow us to better coordinate our efforts, prevent duplication of work, 47 | and help you to craft the change so that it is successfully accepted into the project. 48 | - **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 49 | 50 | ## Submission Guidelines 51 | 52 | ### Submitting an Issue 53 | 54 | Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available. 55 | 56 | You can file new issues by selecting from our [new issue templates][github-choose] and filling out the issue template. 57 | 58 | ### Submitting a Pull Request (PR) 59 | 60 | Before you submit your Pull Request (PR) consider the following guidelines: 61 | 62 | 1. Search [GitHub](https://github.com/gothinkster/realworld/pulls) for an open or closed PR 63 | that relates to your submission. You don't want to duplicate effort. 64 | 1. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add. 65 | Discussing the design up front helps to ensure that we're ready to accept your work. 66 | 1. Fork the gothinkster/realworld repo. 67 | 1. Make your changes in a new git branch: 68 | 69 | ```bash 70 | git checkout -b my-fix-branch master 71 | ``` 72 | 73 | 1. Create your patch. 74 | 75 | 1. Commit your changes using a descriptive commit message that follows our 76 | [commit message conventions](#commit). 77 | 78 | 1. Push your branch to GitHub: 79 | 80 | ```bash 81 | git push origin my-fix-branch 82 | ``` 83 | 84 | 1. In GitHub, send a pull request to `realworld:master`. 85 | 86 | - If we suggest changes then: 87 | 88 | - Make the required updates. 89 | - Rebase your branch and force push to your GitHub repository (this will update your Pull Request): 90 | 91 | ```bash 92 | git rebase master -i 93 | git push -f 94 | ``` 95 | 96 | That's it! Thank you for your contribution! 97 | 98 | #### After your pull request is merged 99 | 100 | After your pull request is merged, you can safely delete your branch and pull the changes 101 | from the master (upstream) repository: 102 | 103 | - Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows: 104 | 105 | ```bash 106 | git push origin --delete my-fix-branch 107 | ``` 108 | 109 | - Check out the master branch: 110 | 111 | ```bash 112 | git checkout master -f 113 | ``` 114 | 115 | - Delete the local branch: 116 | 117 | ```bash 118 | git branch -D my-fix-branch 119 | ``` 120 | 121 | - Update your master with the latest upstream version: 122 | 123 | ```bash 124 | git pull --ff upstream master 125 | ``` 126 | 127 | ## Commit Message Guidelines 128 | 129 | > These guidelines have been added to the project starting from 130 | 131 | We have very precise rules over how our git commit messages can be formatted. This leads to **more 132 | readable messages** that are easy to follow when looking through the **project history**. 133 | 134 | ### Commit Message Format 135 | 136 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special 137 | format that includes a **type**, a **scope** and a **subject**: 138 | 139 | ``` 140 | (): 141 | 142 | 143 | 144 |