├── .dockerignore ├── .env.example ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── refactor.md ├── pull_request_template.md └── workflows │ ├── build-and-push-images.yaml │ ├── deploy-to-vps.old.yaml │ ├── deploy-with-docker-dev.yaml │ └── deploy-with-docker-prod.yaml ├── .gitignore ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── biome.json ├── custom-incremental-cache-handler.mjs ├── dc-build-local.sh ├── docker-compose.yaml ├── docker ├── Dockerfile.dev ├── Dockerfile.prod ├── Dockerfile.redis ├── docker-stack.local.yaml ├── docker-stack.prod.yaml └── webdis.json ├── drizzle.config.ts ├── drizzle ├── 0000_awesome_nicolaos.sql ├── 0001_living_morlocks.sql ├── 0002_clever_glorian.sql ├── 0003_zippy_black_tarantula.sql ├── 0004_gifted_exodus.sql ├── 0005_pretty_crusher_hogan.sql ├── 0006_confused_the_watchers.sql ├── 0007_certain_skrulls.sql ├── 0008_free_ghost_rider.sql ├── 0009_lean_hammerhead.sql ├── 0010_nosy_toad.sql ├── 0011_mixed_falcon.sql ├── 0012_brainy_invisible_woman.sql ├── 0013_rainy_princess_powerful.sql ├── 0014_tricky_scalphunter.sql ├── 0015_lucky_sentinels.sql ├── 0016_glorious_mariko_yashida.sql ├── 0017_burly_nitro.sql ├── 0018_gifted_kate_bishop.sql ├── 0019_real_william_stryker.sql ├── 0020_swift_exodus.sql ├── 0021_curly_captain_universe.sql ├── 0022_early_rick_jones.sql ├── 0023_bored_texas_twister.sql ├── 0024_reflective_zzzax.sql ├── 0025_mute_wilson_fisk.sql ├── 0026_first_whizzer.sql ├── 0027_spicy_meltdown.sql ├── 0028_needy_katie_power.sql ├── 0029_nasty_triathlon.sql ├── 0030_yielding_blonde_phantom.sql ├── 0031_ambiguous_wasp.sql ├── 0032_cold_tattoo.sql ├── 0033_naive_landau.sql ├── 0034_rainy_ezekiel.sql ├── 0035_deep_mephisto.sql ├── 0036_careful_freak.sql ├── 0037_cynical_peter_parker.sql ├── 0038_common_hiroim.sql ├── 0039_marvelous_mercury.sql ├── 0040_awesome_leech.sql ├── 0041_damp_jack_murdock.sql ├── 0042_gorgeous_doctor_spectrum.sql ├── 0043_blue_tusk.sql ├── 0044_brainy_jazinda.sql ├── 0045_bent_roulette.sql ├── 0046_square_ikaris.sql ├── 0047_same_stark_industries.sql ├── 0048_dusty_leper_queen.sql ├── 0049_flowery_ikaris.sql ├── 0050_special_blue_blade.sql ├── 0051_tricky_tempest.sql ├── 0052_goofy_prism.sql ├── 0053_familiar_dragon_man.sql ├── 0054_marvelous_living_lightning.sql ├── 0055_nasty_toad_men.sql ├── 0056_kind_whirlwind.sql ├── 0057_goofy_mephisto.sql ├── 0058_flimsy_ender_wiggin.sql ├── 0059_cuddly_grim_reaper.sql ├── 0060_wealthy_chimera.sql ├── 0061_striped_monster_badoon.sql ├── 0062_naive_jubilee.sql ├── 0063_brief_colossus.sql ├── 0064_faithful_ben_parker.sql ├── 0065_sad_dagger.sql ├── 0066_hot_quasimodo.sql ├── 0067_rapid_thunderbolt_ross.sql ├── 0068_wakeful_skaar.sql ├── 0069_petite_shriek.sql ├── 0070_even_princess_powerful.sql ├── 0071_swift_captain_cross.sql ├── 0072_oval_fallen_one.sql ├── 0073_sad_crusher_hogan.sql ├── 0074_dear_prowler.sql ├── 0075_volatile_harry_osborn.sql └── meta │ ├── 0000_snapshot.json │ ├── 0001_snapshot.json │ ├── 0002_snapshot.json │ ├── 0003_snapshot.json │ ├── 0004_snapshot.json │ ├── 0005_snapshot.json │ ├── 0006_snapshot.json │ ├── 0007_snapshot.json │ ├── 0008_snapshot.json │ ├── 0009_snapshot.json │ ├── 0010_snapshot.json │ ├── 0011_snapshot.json │ ├── 0012_snapshot.json │ ├── 0013_snapshot.json │ ├── 0014_snapshot.json │ ├── 0015_snapshot.json │ ├── 0016_snapshot.json │ ├── 0017_snapshot.json │ ├── 0018_snapshot.json │ ├── 0019_snapshot.json │ ├── 0020_snapshot.json │ ├── 0021_snapshot.json │ ├── 0022_snapshot.json │ ├── 0023_snapshot.json │ ├── 0024_snapshot.json │ ├── 0025_snapshot.json │ ├── 0026_snapshot.json │ ├── 0027_snapshot.json │ ├── 0028_snapshot.json │ ├── 0029_snapshot.json │ ├── 0030_snapshot.json │ ├── 0031_snapshot.json │ ├── 0032_snapshot.json │ ├── 0033_snapshot.json │ ├── 0034_snapshot.json │ ├── 0035_snapshot.json │ ├── 0036_snapshot.json │ ├── 0037_snapshot.json │ ├── 0038_snapshot.json │ ├── 0039_snapshot.json │ ├── 0040_snapshot.json │ ├── 0041_snapshot.json │ ├── 0042_snapshot.json │ ├── 0043_snapshot.json │ ├── 0044_snapshot.json │ ├── 0045_snapshot.json │ ├── 0046_snapshot.json │ ├── 0047_snapshot.json │ ├── 0048_snapshot.json │ ├── 0049_snapshot.json │ ├── 0050_snapshot.json │ ├── 0051_snapshot.json │ ├── 0052_snapshot.json │ ├── 0053_snapshot.json │ ├── 0054_snapshot.json │ ├── 0055_snapshot.json │ ├── 0056_snapshot.json │ ├── 0057_snapshot.json │ ├── 0058_snapshot.json │ ├── 0059_snapshot.json │ ├── 0060_snapshot.json │ ├── 0061_snapshot.json │ ├── 0062_snapshot.json │ ├── 0063_snapshot.json │ ├── 0064_snapshot.json │ ├── 0065_snapshot.json │ ├── 0066_snapshot.json │ ├── 0067_snapshot.json │ ├── 0068_snapshot.json │ ├── 0069_snapshot.json │ ├── 0070_snapshot.json │ ├── 0071_snapshot.json │ ├── 0072_snapshot.json │ ├── 0073_snapshot.json │ ├── 0074_snapshot.json │ ├── 0075_snapshot.json │ └── _journal.json ├── ecosystem.config.cjs ├── globals.d.ts ├── latency.ts ├── migrate.mjs ├── next.config.mjs ├── package.json ├── patches └── remark-github@12.0.0.patch ├── pnpm-lock.yaml ├── postcss.config.cjs ├── pr-preview-workflow ├── .gitignore ├── README.md ├── add-caddyfile.ts ├── add-docker-app.ts ├── bun.lockb ├── index.ts ├── package.json └── tsconfig.json ├── prettier.config.cjs ├── public ├── 404-text.png ├── dark_theme_preview.svg ├── favicon-dark.png ├── favicon-dark.svg ├── favicon.png ├── favicon.svg ├── guirlande1.png ├── guirlande2.png ├── guirlande3.png ├── light_theme_preview.svg ├── next.svg ├── octoshadow.png ├── octostar.png ├── robots.txt ├── spaceship-shadow.png └── spaceship.png ├── rsdw.d.ts ├── scripts ├── fetchIssues.ts ├── fetchUsers.ts ├── github-action-list-element.ts └── github-query-builder-element.ts ├── searching.md ├── src ├── actions │ ├── auth.action.ts │ ├── github.action.ts │ ├── issue.action.tsx │ ├── markdown.action.tsx │ ├── middlewares.ts │ ├── session.action.ts │ ├── theme.action.ts │ └── user.action.ts ├── app │ ├── (app) │ │ ├── @header_subnav │ │ │ ├── [user] │ │ │ │ ├── [repository] │ │ │ │ │ ├── [...pages] │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── default.tsx │ │ │ ├── notifications │ │ │ │ └── page.tsx │ │ │ └── settings │ │ │ │ ├── [...pages] │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ ├── @page_title │ │ │ ├── [user] │ │ │ │ ├── [repository] │ │ │ │ │ ├── [...pages] │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── default.tsx │ │ │ ├── notifications │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── settings │ │ │ │ ├── [...pages] │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ ├── [user] │ │ │ ├── [repository] │ │ │ │ ├── actions │ │ │ │ │ └── page.tsx │ │ │ │ ├── issues │ │ │ │ │ ├── [number] │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── new │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── stargazers │ │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── notifications │ │ │ └── page.tsx │ │ └── settings │ │ │ ├── account │ │ │ └── page.tsx │ │ │ ├── appearance │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── sessions │ │ │ ├── [id] │ │ │ └── page.tsx │ │ │ └── page.tsx │ ├── api │ │ └── auth │ │ │ └── callback │ │ │ └── route.ts │ ├── client-providers.tsx │ ├── global-error.tsx │ ├── globals.css │ ├── layout.tsx │ ├── login │ │ └── page.tsx │ └── not-found.tsx ├── components │ ├── action-list.tsx │ ├── action-toolbar.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── cache.tsx │ ├── card.tsx │ ├── counter-badge.tsx │ ├── custom-rsc-renderer │ │ ├── load-client-references.ts │ │ ├── render-rsc-to-string.ts │ │ ├── rsc-client-renderer.tsx │ │ └── rsc-manifest.ts │ ├── dropdown.tsx │ ├── footer.tsx │ ├── header │ │ ├── header-navlinks.tsx │ │ └── header.tsx │ ├── hovercard │ │ ├── hovercard.tsx │ │ ├── issue-hovercard-contents.tsx │ │ ├── issue-hovercard-link.tsx │ │ └── user-hovercard-contents.tsx │ ├── icon-switcher.tsx │ ├── input.tsx │ ├── issues │ │ ├── clear-search-button.tsx │ │ ├── issue-assignee-filter-action-list.tsx │ │ ├── issue-author-filter-action-list.tsx │ │ ├── issue-label-filter-action-list.tsx │ │ ├── issue-list-search-input.tsx │ │ ├── issue-list-skeleton.tsx │ │ ├── issue-list.tsx │ │ ├── issue-row-avatar-stack.tsx │ │ ├── issue-row-skeleton.tsx │ │ ├── issue-row.tsx │ │ ├── issue-search-link.tsx │ │ ├── issue-sort-action-list.tsx │ │ ├── issues-list-header-form.tsx │ │ ├── new-issue-form.tsx │ │ ├── use-issue-assignee-list-query.ts │ │ ├── use-issue-author-list-by-name-query.ts │ │ ├── use-issue-author-list-query.ts │ │ ├── use-issue-label-list-query.ts │ │ └── use-search-input-tokens.tsx │ ├── label-badge.tsx │ ├── loading-indicator.tsx │ ├── markdown-editor │ │ ├── markdown-editor-preview.tsx │ │ ├── markdown-editor-toolbar.tsx │ │ └── markdown-editor.tsx │ ├── markdown │ │ ├── markdown-a.tsx │ │ ├── markdown-code-block.tsx │ │ ├── markdown-error-boundary.tsx │ │ ├── markdown-h.tsx │ │ ├── markdown-skeleton.tsx │ │ ├── markdown-title.tsx │ │ └── markdown.tsx │ ├── nav-link.tsx │ ├── pagination.tsx │ ├── react-query-provider.tsx │ ├── segmented-layout.tsx │ ├── settings-vertical-navlist.tsx │ ├── skeleton.tsx │ ├── skip-to-main-button.tsx │ ├── submit-button.tsx │ ├── tailwind-indicator.tsx │ ├── textarea.tsx │ ├── theme-card.tsx │ ├── theme-form.tsx │ ├── toast │ │ ├── toast.tsx │ │ ├── toaster.client.tsx │ │ └── toaster.server.tsx │ ├── tooltip.tsx │ ├── top-loader.tsx │ ├── update-user-infos-form.tsx │ ├── user-dropdown │ │ ├── user-dropdown.client.tsx │ │ └── user-dropdown.server.tsx │ ├── vertical-nav-link.tsx │ └── x-mas-decorations.tsx ├── env-config.mjs ├── env.ts ├── lib │ ├── client │ │ ├── hooks │ │ │ ├── use-active-link.ts │ │ │ ├── use-media-query.ts │ │ │ ├── use-pagination.ts │ │ │ └── use-typed-params.ts │ │ └── pauseable-timeout.ts │ ├── server │ │ ├── db │ │ │ ├── index.server.ts │ │ │ └── schema │ │ │ │ ├── comment.sql.ts │ │ │ │ ├── event.sql.ts │ │ │ │ ├── index.sql.ts │ │ │ │ ├── issue.sql.ts │ │ │ │ ├── label.sql.ts │ │ │ │ ├── mention.sql.ts │ │ │ │ ├── reaction.sql.ts │ │ │ │ ├── repository.sql.ts │ │ │ │ └── user.sql.ts │ │ ├── kv │ │ │ ├── cloudfare.ts │ │ │ ├── http.ts │ │ │ ├── index.server.ts │ │ │ ├── sqlite │ │ │ │ ├── config.ts │ │ │ │ ├── index.ts │ │ │ │ └── kv-entry.sql.ts │ │ │ └── webdis.server.mjs │ │ ├── rsc-utils.server.ts │ │ ├── session.server.ts │ │ ├── themes │ │ │ ├── github-dark.json │ │ │ └── github-light.json │ │ └── utils.server.ts │ ├── shared │ │ ├── cache-keys.shared.ts │ │ ├── constants.ts │ │ ├── lifetime-cache.ts │ │ └── utils.shared.ts │ └── types.ts ├── middleware.ts └── models │ ├── dto │ ├── issue-search-output-validator.ts │ ├── public-user-output-validator.ts │ ├── repository-output-validator.ts │ └── update-profile-info-input-validator.ts │ ├── issues │ ├── index.ts │ └── search.ts │ ├── label.ts │ ├── repository.ts │ └── user.ts ├── tailwind.config.cjs ├── tsconfig.json └── tsconfig.script.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env.local 3 | Dockerfile 4 | .dockerignore 5 | node_modules 6 | npm-debug.log 7 | README.md 8 | .git -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SESSION_SECRET="" 2 | GITHUB_CLIENT_ID= 3 | GITHUB_REDIRECT_URI="http://localhost:3000/api/auth/callback" 4 | GITHUB_SECRET= 5 | GITHUB_PERSONAL_ACCESS_TOKEN="" 6 | REDIS_HTTP_URL="http://127.0.0.1:6380" 7 | REDIS_HTTP_USERNAME="user" 8 | REDIS_HTTP_PASSWORD="password" 9 | NEXT_PUBLIC_VERCEL_URL="localhost:3000" 10 | DATABASE_URL="postgresql://postgres:password@localhost:5433/gh_next" 11 | KV_PREFIX= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[\U0001F41B bug] your-issue" 5 | labels: bug, need-triage 6 | assignees: "" 7 | --- 8 | 9 | ## Describe the bug 10 | A clear and concise description of what the bug is. 11 | 12 | ## To Reproduce 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ## Expected behavior 21 | A clear and concise description of what you expected to happen. 22 | 23 | ## Screenshots or Video 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | ## Additional context 27 | Add any other context about the problem here for example if the issue only appears in one browser or OS. 28 | 29 | - [ ] Are you willing to make a PR for this ? 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[✨ feature] i want this" 5 | labels: feature, need-triage 6 | assignees: "" 7 | --- 8 | 9 | ## Is your feature request related to a problem? Please describe. 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | ## Describe the solution you'd like 13 | A clear and concise description of what you want to happen. 14 | 15 | ## Additional context 16 | Add any other context or screenshots about the feature request here. 17 | 18 | - [ ] Are you willing to make a PR for this ? 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/refactor.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Refactor 3 | about: Suggest a new refactor task to the project 4 | title: "[♻️ refactor] We need to do..." 5 | labels: refactor, need-triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## What is the reason to do this refactor ?. 11 | A clear and concise description of why this refactor is needed and/or what advantages does it provide. 12 | 13 | ## Describe the work that needs to be done 14 | A clear and concise description of the work that needs to be done. 15 | 16 | ## Additional context 17 | Add any other context or screenshots about the issue here. 18 | 19 | - [ ] Are you willing to make a PR for this ? 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please provide a brief summary of the changes. If it fixes a bug or resolves a feature request, be sure to link to that issue. 4 | 5 | fixes # 6 | 7 | ## Type of Change 8 | 9 | - [ ] Bug fix (non-breaking change which fixes an issue) 10 | - [ ] New feature (non-breaking change which adds functionality) 11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | 13 | -------------------------------------------------------------------------------- /.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 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | 38 | # cloudfare pages 39 | .wrangler/ 40 | /cache/ 41 | .idea/ 42 | 43 | # Docker 44 | docker-stack.pr.yaml 45 | caddyfile.pr 46 | pr.caddyfile 47 | *.bak 48 | *.log 49 | notes.md 50 | certificates -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.defaultFormatter": "biomejs.biome" 4 | }, 5 | "[typescript]": { 6 | "editor.defaultFormatter": "biomejs.biome" 7 | }, 8 | "[typescriptreact]": { 9 | "editor.defaultFormatter": "biomejs.biome" 10 | }, 11 | "editor.defaultFormatter": "biomejs.biome", 12 | "typescript.preferences.importModuleSpecifier": "non-relative", 13 | "typescript.tsdk": "node_modules/typescript/lib", 14 | "[json]": { 15 | "editor.defaultFormatter": "biomejs.biome" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Thank you for your interest in contributing to our project! We welcome and appreciate all contributions. This document provides guidelines and steps for contributing. 4 | 5 | ## Code of Conduct 6 | 7 | All contributors are expected to follow our [Code of Conduct](./CODE_OF_CONDUCT.md). Please ensure you are welcoming and friendly in all of our spaces. 8 | 9 | ## Getting Started 10 | 11 | 1. Fork the repository on GitHub. 12 | 2. Clone the forked repository to your machine. 13 | 3. Create a new branch for your work. 14 | 4. Make your changes. 15 | 5. Commit your changes. 16 | 6. Push your changes to your fork on GitHub. 17 | 7. Create a pull request against the main branch of the original repository. 18 | 19 | ## Pull Request Guidelines 20 | 21 | - Ensure any install or build dependencies are removed before the end of the layer when doing a build. 22 | - Update the README.md with details of changes to the interface, this includes new environment variables, exposed ports, useful file locations, and container parameters. 23 | - Ensure your PR has a meaningful title. 24 | 25 | ## Reporting Issues 26 | 27 | If you believe you found a bug, please report it using the issue tracker. When filing an issue: 28 | 29 | - Provide a quick summary. 30 | - Include reproduction steps, environment details, and any other relevant information. 31 | - Attach logs or screenshots if possible. 32 | 33 | ## Suggesting Enhancements 34 | 35 | We love feedback. If you have a suggestion for improving the project, please open an issue for discussion. 36 | 37 | ## Discussions 38 | 39 | If you have questions, suggestions, or want to discuss topics related to the project, please open a discussion in the [Discussions tab](LINK_TO_GITHUB_DISCUSSIONS). 40 | 41 | ## License 42 | 43 | By contributing, you agree that your contributions will be licensed under the same license as the original project. 44 | 45 | Thank you for your contribution! 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Adrien KISSIE 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 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "linter": { 3 | "enabled": false 4 | }, 5 | "formatter": { 6 | "enabled": true, 7 | "formatWithErrors": true, 8 | "indentStyle": "tab", 9 | "indentWidth": 2, 10 | "lineWidth": 80, 11 | "ignore": [] 12 | }, 13 | "javascript": { 14 | "formatter": { 15 | "arrowParentheses": "always", 16 | "indentStyle": "space", 17 | "trailingComma": "none", 18 | "indentWidth": 2 19 | } 20 | }, 21 | "json": { 22 | "formatter": { 23 | "indentWidth": 2, 24 | "indentStyle": "space" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /dc-build-local.sh: -------------------------------------------------------------------------------- 1 | while read -r line; do 2 | build_args="$build_args --build-arg $line" 3 | done < .env.docker.local 4 | echo args="'$build_args'" 5 | docker buildx build -t dcr.fredkiss.dev/gh-next:dev -f docker/Dockerfile.dev $build_args . -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.1" 2 | 3 | services: 4 | webdis: 5 | image: nicolas/webdis:latest 6 | volumes: # mount volume containing the config file 7 | - ./docker/webdis.json:/etc/webdis.prod.json 8 | ports: 9 | - "6380:7379" 10 | db: 11 | image: postgres:12-alpine 12 | restart: always 13 | volumes: 14 | - db-data:/var/lib/postgresql/data 15 | environment: 16 | POSTGRES_USER: postgres 17 | POSTGRES_PASSWORD: password 18 | POSTGRES_DB: gh_next 19 | ports: 20 | - "5433:5432" 21 | adminer: 22 | image: adminer 23 | restart: always 24 | ports: 25 | - 8081:8080 26 | 27 | volumes: 28 | db-data: 29 | -------------------------------------------------------------------------------- /docker/Dockerfile.prod: -------------------------------------------------------------------------------- 1 | ##### DEPENDENCIES 2 | 3 | FROM node:20-alpine3.19 AS deps 4 | RUN apk add --no-cache libc6-compat 5 | RUN apk update && apk upgrade openssl 6 | WORKDIR /app 7 | 8 | # Install dependencies based on the preferred package manager 9 | 10 | COPY package.json pnpm-lock.yaml ./ 11 | COPY ./patches ./patches 12 | 13 | RUN yarn global add pnpm@8 && pnpm install --shamefully-hoist --strict-peer-dependencies=false --frozen-lockfile 14 | 15 | ##### RUNNER 16 | 17 | FROM node:20-alpine3.19 AS runner 18 | WORKDIR /app 19 | 20 | ENV REDIS_HTTP_USERNAME=user 21 | ENV REDIS_HTTP_PASSWORD=password 22 | 23 | 24 | ENV NODE_ENV=production 25 | 26 | ENV NEXT_TELEMETRY_DISABLED=1 27 | 28 | RUN addgroup --system --gid 1001 nodejs 29 | RUN adduser --system --uid 1001 nextjs 30 | 31 | COPY --from=deps /app/node_modules ./node_modules 32 | 33 | COPY ./public/ ./public/ 34 | COPY ./drizzle/ ./drizzle/ 35 | 36 | COPY ./migrate.mjs . 37 | COPY ./next.config.mjs . 38 | COPY ./package.json . 39 | COPY ./custom-incremental-cache-handler.mjs . 40 | 41 | COPY --chown=nextjs:nodejs .next/standalone ./ 42 | COPY --chown=nextjs:nodejs .next/static ./.next/static 43 | 44 | 45 | USER nextjs 46 | EXPOSE 80 47 | ENV PORT=80 48 | ENV NODE_ENV=production 49 | ENV HOSTNAME=0.0.0.0 50 | 51 | CMD ["sh", "-c", "node migrate.mjs && node server.js"] 52 | -------------------------------------------------------------------------------- /docker/Dockerfile.redis: -------------------------------------------------------------------------------- 1 | FROM nicolas/webdis:0.1.22 2 | 3 | COPY ./docker/webdis.json /etc/webdis.prod.json -------------------------------------------------------------------------------- /docker/docker-stack.local.yaml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | 3 | services: 4 | app: 5 | image: dcr.fredkiss.dev/gh-next:latest 6 | ports: 7 | - "3000:3000" 8 | deploy: 9 | replicas: 1 10 | update_config: 11 | parallelism: 1 12 | delay: 5s 13 | order: start-first 14 | failure_action: rollback 15 | restart_policy: 16 | condition: on-failure 17 | delay: 5s 18 | max_attempts: 3 19 | window: 120s 20 | depends_on: 21 | - db 22 | - webdis 23 | webdis: 24 | image: nicolas/webdis:latest 25 | volumes: # mount volume containing the config file 26 | - ./webdis.json:/etc/webdis.prod.json 27 | ports: 28 | - "6380:7379" 29 | adminer: 30 | image: adminer 31 | ports: 32 | - 8081:8080 33 | db: 34 | image: postgres:12-alpine 35 | volumes: 36 | - db-data:/var/lib/postgresql/data 37 | environment: 38 | POSTGRES_USER: postgres 39 | POSTGRES_PASSWORD: password 40 | POSTGRES_DB: gh_next 41 | ports: 42 | - "5433:5432" 43 | volumes: 44 | db-data: 45 | -------------------------------------------------------------------------------- /docker/docker-stack.prod.yaml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | 3 | services: 4 | gh-next-prod: 5 | image: dcr.fredkiss.dev/gh-next:latest 6 | deploy: 7 | replicas: 2 8 | update_config: 9 | parallelism: 1 10 | delay: 5s 11 | order: start-first 12 | failure_action: rollback 13 | restart_policy: 14 | condition: on-failure 15 | delay: 5s 16 | max_attempts: 3 17 | window: 120s 18 | networks: 19 | - gh-next 20 | networks: 21 | gh-next: 22 | external: true 23 | -------------------------------------------------------------------------------- /docker/webdis.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_host": "0.0.0.0", 3 | "http_port": 7379, 4 | "redis_host": "127.0.0.1", 5 | "redis_port": 6379, 6 | 7 | "acl": [ 8 | { 9 | "disabled": ["*"] 10 | }, 11 | 12 | { 13 | "http_basic_auth": "user:password", 14 | "enabled": [ 15 | "GET", 16 | "SET", 17 | "DEL", 18 | "SETEX", 19 | "HSET", 20 | "HGETALL", 21 | "HGET", 22 | "SREM", 23 | "PING", 24 | "SADD", 25 | "SMEMBERS" 26 | ] 27 | } 28 | ], 29 | 30 | "verbosity": 4, 31 | "logfile": "/dev/stderr" 32 | } 33 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | 3 | export default { 4 | schema: "./src/lib/server/db/schema/*.sql.ts", 5 | out: "./drizzle", 6 | driver: "pg", 7 | dbCredentials: { 8 | connectionString: process.env.DATABASE_URL! 9 | } 10 | } satisfies Config; 11 | -------------------------------------------------------------------------------- /drizzle/0001_living_morlocks.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_users" ADD COLUMN "company" varchar(255); -------------------------------------------------------------------------------- /drizzle/0002_clever_glorian.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues" RENAME COLUMN "description" TO "body"; -------------------------------------------------------------------------------- /drizzle/0003_zippy_black_tarantula.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues" DROP CONSTRAINT "gh_next_issues_assignee_id_gh_next_users_id_fk"; 2 | --> statement-breakpoint 3 | ALTER TABLE "gh_next_issues" DROP COLUMN IF EXISTS "assignee_id"; -------------------------------------------------------------------------------- /drizzle/0004_gifted_exodus.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues" ADD COLUMN "author_username" varchar(255) NOT NULL;--> statement-breakpoint 2 | ALTER TABLE "gh_next_issues" ADD COLUMN "author_avatar_url" varchar(255) NOT NULL; -------------------------------------------------------------------------------- /drizzle/0005_pretty_crusher_hogan.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_comments" ADD COLUMN "author_username" varchar(255) NOT NULL;--> statement-breakpoint 2 | ALTER TABLE "gh_next_comments" ADD COLUMN "author_avatar_url" varchar(255) NOT NULL; -------------------------------------------------------------------------------- /drizzle/0006_confused_the_watchers.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issue_revisions" DROP CONSTRAINT "gh_next_issue_revisions_revised_by_id_gh_next_users_id_fk"; 2 | --> statement-breakpoint 3 | ALTER TABLE "gh_next_issue_revisions" ADD COLUMN "revised_by_username" varchar(255) NOT NULL;--> statement-breakpoint 4 | ALTER TABLE "gh_next_issue_revisions" ADD COLUMN "revised_by_avatar_url" varchar(255) NOT NULL;--> statement-breakpoint 5 | ALTER TABLE "gh_next_issues" ADD COLUMN "number" integer;--> statement-breakpoint 6 | ALTER TABLE "gh_next_issue_revisions" DROP COLUMN IF EXISTS "revised_by_id"; -------------------------------------------------------------------------------- /drizzle/0007_certain_skrulls.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues" ALTER COLUMN "number" SET NOT NULL; -------------------------------------------------------------------------------- /drizzle/0008_free_ghost_rider.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues" ADD CONSTRAINT "gh_next_issues_number_unique" UNIQUE("number"); -------------------------------------------------------------------------------- /drizzle/0009_lean_hammerhead.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_labels" ADD CONSTRAINT "gh_next_labels_name_unique" UNIQUE("name"); -------------------------------------------------------------------------------- /drizzle/0010_nosy_toad.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues_to_assignees" DROP CONSTRAINT "gh_next_issues_to_assignees_issue_id_assignee_id";--> statement-breakpoint 2 | ALTER TABLE "gh_next_issues_to_assignees" DROP CONSTRAINT "gh_next_issues_to_assignees_assignee_id_gh_next_users_id_fk"; 3 | --> statement-breakpoint 4 | ALTER TABLE "gh_next_issues_to_assignees" ADD COLUMN "assignee_username" varchar(255) NOT NULL;--> statement-breakpoint 5 | ALTER TABLE "gh_next_issues_to_assignees" ADD COLUMN "assignee_avatar_url" varchar(255) NOT NULL;--> statement-breakpoint 6 | ALTER TABLE "gh_next_issues_to_assignees" DROP COLUMN IF EXISTS "assignee_id";--> statement-breakpoint 7 | ALTER TABLE "gh_next_issues_to_assignees" ADD CONSTRAINT "gh_next_issues_to_assignees_issue_id" PRIMARY KEY("issue_id"); -------------------------------------------------------------------------------- /drizzle/0012_brainy_invisible_woman.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues_to_assignees" DROP CONSTRAINT "gh_next_issues_to_assignees_issue_id";--> statement-breakpoint 2 | ALTER TABLE "gh_next_issues_to_assignees" ADD COLUMN "id" serial NOT NULL;--> statement-breakpoint 3 | ALTER TABLE "gh_next_issues_to_assignees" ADD COLUMN "assignee_id" integer;--> statement-breakpoint 4 | DO $$ BEGIN 5 | ALTER TABLE "gh_next_issues_to_assignees" ADD CONSTRAINT "gh_next_issues_to_assignees_assignee_id_gh_next_users_id_fk" FOREIGN KEY ("assignee_id") REFERENCES "gh_next_users"("id") ON DELETE cascade ON UPDATE no action; 6 | EXCEPTION 7 | WHEN duplicate_object THEN null; 8 | END $$; 9 | -------------------------------------------------------------------------------- /drizzle/0013_rainy_princess_powerful.sql: -------------------------------------------------------------------------------- 1 | ALTER TYPE "event_type" ADD VALUE 'ADD_COMMENT';--> statement-breakpoint 2 | ALTER TABLE "gh_next_issue_events" ADD COLUMN "comment_id" integer;--> statement-breakpoint 3 | DO $$ BEGIN 4 | ALTER TABLE "gh_next_issue_events" ADD CONSTRAINT "gh_next_issue_events_comment_id_gh_next_comments_id_fk" FOREIGN KEY ("comment_id") REFERENCES "gh_next_comments"("id") ON DELETE cascade ON UPDATE no action; 5 | EXCEPTION 6 | WHEN duplicate_object THEN null; 7 | END $$; 8 | -------------------------------------------------------------------------------- /drizzle/0014_tricky_scalphunter.sql: -------------------------------------------------------------------------------- 1 | DO $$ BEGIN 2 | CREATE TYPE "issue_lock_reason" AS ENUM('OFF_TOPIC', 'TOO_HEATED', 'RESOLVED', 'SPAM'); 3 | EXCEPTION 4 | WHEN duplicate_object THEN null; 5 | END $$; 6 | --> statement-breakpoint 7 | ALTER TYPE "event_type" ADD VALUE 'ISSUE_LOCK';--> statement-breakpoint 8 | ALTER TABLE "gh_next_issue_events" ADD COLUMN "lock_reason" "issue_lock_reason"; -------------------------------------------------------------------------------- /drizzle/0015_lucky_sentinels.sql: -------------------------------------------------------------------------------- 1 | DO $$ BEGIN 2 | CREATE TYPE "comment_hide_reason" AS ENUM('ABUSE', 'OFF_TOPIC', 'OUTDATED', 'RESOLVED', 'DUPLICATE', 'SPAM'); 3 | EXCEPTION 4 | WHEN duplicate_object THEN null; 5 | END $$; 6 | --> statement-breakpoint 7 | ALTER TABLE "gh_next_comments" ADD COLUMN "hidden" boolean DEFAULT false NOT NULL;--> statement-breakpoint 8 | ALTER TABLE "gh_next_comments" ADD COLUMN "hidden_reason" "comment_hide_reason"; -------------------------------------------------------------------------------- /drizzle/0016_glorious_mariko_yashida.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issue_events" ADD COLUMN "assignee_username" varchar(255);--> statement-breakpoint 2 | ALTER TABLE "gh_next_issue_events" ADD COLUMN "assignee_avatar_url" varchar(255); -------------------------------------------------------------------------------- /drizzle/0017_burly_nitro.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issue_events" DROP CONSTRAINT "gh_next_issue_events_assignee_id_gh_next_users_id_fk"; 2 | --> statement-breakpoint 3 | ALTER TABLE "gh_next_issue_events" DROP COLUMN IF EXISTS "assignee_id"; -------------------------------------------------------------------------------- /drizzle/0018_gifted_kate_bishop.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues" ADD COLUMN "lock_reason" "issue_lock_reason";--> statement-breakpoint 2 | ALTER TABLE "gh_next_issue_events" DROP COLUMN IF EXISTS "lock_reason"; -------------------------------------------------------------------------------- /drizzle/0019_real_william_stryker.sql: -------------------------------------------------------------------------------- 1 | ALTER TYPE event_type RENAME TO event_type_old; 2 | CREATE TYPE event_type AS ENUM('CHANGE_TITLE', 'TOGGLE_STATUS', 'ISSUE_MENTION', 'ASSIGN_USER', 'ADD_LABEL', 'REMOVE_LABEL', 'ADD_COMMENT'); 3 | ALTER TABLE gh_next_issue_events ALTER COLUMN type TYPE event_type USING type::text::event_type; -------------------------------------------------------------------------------- /drizzle/0020_swift_exodus.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX IF NOT EXISTS "title_idx" ON "gh_next_issues" ("title"); -------------------------------------------------------------------------------- /drizzle/0021_curly_captain_universe.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues" ADD COLUMN "status_updated_at" timestamp DEFAULT now() NOT NULL; -------------------------------------------------------------------------------- /drizzle/0022_early_rick_jones.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues" ADD COLUMN "updated_at" timestamp DEFAULT now() NOT NULL; -------------------------------------------------------------------------------- /drizzle/0023_bored_texas_twister.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_comments" ADD COLUMN "content_search_vector" "tsvector";--> statement-breakpoint 2 | ALTER TABLE "gh_next_issues" ADD COLUMN "body_search_vector" "tsvector";--> statement-breakpoint 3 | 4 | UPDATE gh_next_issues SET body_search_vector = to_tsvector('english', body); 5 | UPDATE gh_next_comments SET content_search_vector = to_tsvector('english', content); 6 | 7 | CREATE INDEX IF NOT EXISTS "content_search_vector_idex" ON "gh_next_comments" using gin("content_search_vector");--> statement-breakpoint 8 | CREATE INDEX IF NOT EXISTS "body_search_vector_idex" ON "gh_next_issues" using gin("body_search_vector"); -------------------------------------------------------------------------------- /drizzle/0024_reflective_zzzax.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX IF EXISTS "content_search_vector_idex";--> statement-breakpoint 2 | DROP INDEX IF EXISTS "body_search_vector_idex";--> statement-breakpoint 3 | ALTER TABLE "gh_next_comments" DROP COLUMN IF EXISTS "content_search_vector";--> statement-breakpoint 4 | ALTER TABLE "gh_next_issues" DROP COLUMN IF EXISTS "body_search_vector"; -------------------------------------------------------------------------------- /drizzle/0025_mute_wilson_fisk.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "gh_next_issue_user_mentions" ( 2 | "id" serial PRIMARY KEY NOT NULL, 3 | "username" varchar(255) NOT NULL, 4 | "issue_id" integer NOT NULL, 5 | "comment_id" integer 6 | ); 7 | --> statement-breakpoint 8 | DO $$ BEGIN 9 | ALTER TABLE "gh_next_issue_user_mentions" ADD CONSTRAINT "gh_next_issue_user_mentions_issue_id_gh_next_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "gh_next_issues"("id") ON DELETE cascade ON UPDATE no action; 10 | EXCEPTION 11 | WHEN duplicate_object THEN null; 12 | END $$; 13 | --> statement-breakpoint 14 | DO $$ BEGIN 15 | ALTER TABLE "gh_next_issue_user_mentions" ADD CONSTRAINT "gh_next_issue_user_mentions_comment_id_gh_next_comments_id_fk" FOREIGN KEY ("comment_id") REFERENCES "gh_next_comments"("id") ON DELETE cascade ON UPDATE no action; 16 | EXCEPTION 17 | WHEN duplicate_object THEN null; 18 | END $$; 19 | -------------------------------------------------------------------------------- /drizzle/0026_first_whizzer.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issue_user_mentions" ADD CONSTRAINT "gh_next_issue_user_mentions_username_issue_id_comment_id_unique" UNIQUE("username","issue_id","comment_id"); -------------------------------------------------------------------------------- /drizzle/0027_spicy_meltdown.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues" ADD COLUMN "body_search_vector" tsvector generated always as (to_tsvector('english',body)) stored;--> statement-breakpoint 2 | ALTER TABLE "gh_next_issues" ADD COLUMN "title_search_vector" tsvector generated always as ( 3 | setweight(to_tsvector('simple',title), 'A') || ' ' || 4 | setweight(to_tsvector('english',title), 'B')) stored;--> statement-breakpoint 5 | CREATE INDEX IF NOT EXISTS "body_search_vector_idx" ON "gh_next_issues" using gin("body_search_vector");--> statement-breakpoint 6 | CREATE INDEX IF NOT EXISTS "title_search_vector_idx" ON "gh_next_issues" using gin("title_search_vector"); -------------------------------------------------------------------------------- /drizzle/0028_needy_katie_power.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_comments" ADD COLUMN "content_search_vector" tsvector generated always as (to_tsvector('english',content)) stored;--> statement-breakpoint 2 | CREATE INDEX IF NOT EXISTS "content_search_vector_idex" ON "gh_next_comments" using gin("content_search_vector"); -------------------------------------------------------------------------------- /drizzle/0029_nasty_triathlon.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX IF EXISTS "content_search_vector_idex";--> statement-breakpoint 2 | DROP INDEX IF EXISTS "body_search_vector_idx";--> statement-breakpoint 3 | DROP INDEX IF EXISTS "title_search_vector_idx";--> statement-breakpoint 4 | ALTER TABLE "gh_next_comments" DROP COLUMN IF EXISTS "content_search_vector";--> statement-breakpoint 5 | ALTER TABLE "gh_next_issues" DROP COLUMN IF EXISTS "body_search_vector";--> statement-breakpoint 6 | ALTER TABLE "gh_next_issues" DROP COLUMN IF EXISTS "title_search_vector"; -------------------------------------------------------------------------------- /drizzle/0030_yielding_blonde_phantom.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "gh_next_repositories" ( 2 | "id" serial PRIMARY KEY NOT NULL, 3 | "name" varchar(255) NOT NULL, 4 | "created_at" timestamp DEFAULT now() NOT NULL, 5 | "creator_id" integer, 6 | "is_archived" boolean DEFAULT false, 7 | CONSTRAINT "gh_next_repositories_name_unique" UNIQUE("name") 8 | ); 9 | --> statement-breakpoint 10 | ALTER TABLE "gh_next_issues" ADD COLUMN "repository_id" integer;--> statement-breakpoint 11 | 12 | --> make it so that there is no issue without a repo attached 13 | WITH inserted AS ( 14 | INSERT INTO gh_next_repositories (name, creator_id) 15 | VALUES ( 16 | 'gh-next', 17 | ( 18 | SELECT id FROM gh_next_users WHERE username = 'Fredkiss3' LIMIT 1 19 | ) 20 | ) 21 | ON CONFLICT (name) DO NOTHING 22 | RETURNING id 23 | ) 24 | UPDATE gh_next_issues SET repository_id=(SELECT id FROM inserted) WHERE 1=1;--> statement-breakpoint 25 | 26 | CREATE INDEX IF NOT EXISTS "name_idx" ON "gh_next_repositories" ("name");--> statement-breakpoint 27 | DO $$ BEGIN 28 | ALTER TABLE "gh_next_issues" ADD CONSTRAINT "gh_next_issues_repository_id_gh_next_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "gh_next_repositories"("id") ON DELETE cascade ON UPDATE no action; 29 | EXCEPTION 30 | WHEN duplicate_object THEN null; 31 | END $$; 32 | --> statement-breakpoint 33 | DO $$ BEGIN 34 | ALTER TABLE "gh_next_repositories" ADD CONSTRAINT "gh_next_repositories_creator_id_gh_next_users_id_fk" FOREIGN KEY ("creator_id") REFERENCES "gh_next_users"("id") ON DELETE cascade ON UPDATE no action; 35 | EXCEPTION 36 | WHEN duplicate_object THEN null; 37 | END $$; 38 | -------------------------------------------------------------------------------- /drizzle/0031_ambiguous_wasp.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues" DROP CONSTRAINT "gh_next_issues_number_unique";--> statement-breakpoint 2 | ALTER TABLE "gh_next_issues" ADD CONSTRAINT "uniq_number_idx" UNIQUE("repository_id","number"); -------------------------------------------------------------------------------- /drizzle/0032_cold_tattoo.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues" ALTER COLUMN "repository_id" SET NOT NULL; -------------------------------------------------------------------------------- /drizzle/0033_naive_landau.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues" ALTER COLUMN "title" SET DATA TYPE text; -------------------------------------------------------------------------------- /drizzle/0034_rainy_ezekiel.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_repositories" ADD COLUMN "is_public" boolean DEFAULT true; -------------------------------------------------------------------------------- /drizzle/0035_deep_mephisto.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX IF NOT EXISTS "author_fk_idx" ON "gh_next_comments" ("author_id");--> statement-breakpoint 2 | CREATE INDEX IF NOT EXISTS "issue_fk_idx" ON "gh_next_comments" ("issue_id");--> statement-breakpoint 3 | CREATE INDEX IF NOT EXISTS "repository_fk_idx" ON "gh_next_issues" ("repository_id");--> statement-breakpoint 4 | CREATE INDEX IF NOT EXISTS "author_fk_idx" ON "gh_next_issues" ("author_id");--> statement-breakpoint 5 | CREATE INDEX IF NOT EXISTS "creator_fk_idx" ON "gh_next_repositories" ("creator_id"); -------------------------------------------------------------------------------- /drizzle/0036_careful_freak.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX IF NOT EXISTS "author_fk_idx" ON "gh_next_reactions" ("author_id");--> statement-breakpoint 2 | CREATE INDEX IF NOT EXISTS "issue_fk_idx" ON "gh_next_reactions" ("issue_id");--> statement-breakpoint 3 | CREATE INDEX IF NOT EXISTS "comment_fk_idx" ON "gh_next_reactions" ("comment_id"); -------------------------------------------------------------------------------- /drizzle/0037_cynical_peter_parker.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX IF EXISTS "author_fk_idx";--> statement-breakpoint 2 | DROP INDEX IF EXISTS "issue_fk_idx";--> statement-breakpoint 3 | DROP INDEX IF EXISTS "comment_fk_idx";--> statement-breakpoint 4 | CREATE INDEX IF NOT EXISTS "rakt_author_fk_idx" ON "gh_next_reactions" ("author_id");--> statement-breakpoint 5 | CREATE INDEX IF NOT EXISTS "rakt_issue_fk_idx" ON "gh_next_reactions" ("issue_id");--> statement-breakpoint 6 | CREATE INDEX IF NOT EXISTS "rakt_comment_fk_idx" ON "gh_next_reactions" ("comment_id"); -------------------------------------------------------------------------------- /drizzle/0038_common_hiroim.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues" DROP CONSTRAINT "uniq_number_idx";--> statement-breakpoint 2 | DROP INDEX IF EXISTS "author_fk_idx";--> statement-breakpoint 3 | DROP INDEX IF EXISTS "issue_fk_idx";--> statement-breakpoint 4 | DROP INDEX IF EXISTS "repository_fk_idx";--> statement-breakpoint 5 | DROP INDEX IF EXISTS "name_idx";--> statement-breakpoint 6 | DROP INDEX IF EXISTS "creator_fk_idx";--> statement-breakpoint 7 | CREATE INDEX IF NOT EXISTS "com_author_fk_idx" ON "gh_next_comments" ("author_id");--> statement-breakpoint 8 | CREATE INDEX IF NOT EXISTS "com_issue_fk_idx" ON "gh_next_comments" ("issue_id");--> statement-breakpoint 9 | CREATE INDEX IF NOT EXISTS "iss_repo_fk_idx" ON "gh_next_issues" ("repository_id");--> statement-breakpoint 10 | CREATE INDEX IF NOT EXISTS "iss_author_fk_idx" ON "gh_next_issues" ("author_id");--> statement-breakpoint 11 | CREATE INDEX IF NOT EXISTS "ment_username_idx" ON "gh_next_issue_user_mentions" ("username");--> statement-breakpoint 12 | CREATE INDEX IF NOT EXISTS "repo_name_idx" ON "gh_next_repositories" ("name");--> statement-breakpoint 13 | CREATE INDEX IF NOT EXISTS "repo_creator_fk_idx" ON "gh_next_repositories" ("creator_id");--> statement-breakpoint 14 | ALTER TABLE "gh_next_issues" ADD CONSTRAINT "iss_uniq_number_idx" UNIQUE("repository_id","number"); -------------------------------------------------------------------------------- /drizzle/0039_marvelous_mercury.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX IF NOT EXISTS "iss_status_idx" ON "gh_next_issues" ("status"); -------------------------------------------------------------------------------- /drizzle/0040_awesome_leech.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues" ALTER COLUMN "title" SET DATA TYPE varchar(500); -------------------------------------------------------------------------------- /drizzle/0041_damp_jack_murdock.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX IF NOT EXISTS "com_author_uname_idx" ON "gh_next_comments" ("author_username");--> statement-breakpoint 2 | CREATE INDEX IF NOT EXISTS "iss_author_uname_idx" ON "gh_next_issues" ("author_username"); -------------------------------------------------------------------------------- /drizzle/0042_gorgeous_doctor_spectrum.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX IF NOT EXISTS "rakt_type_idx" ON "gh_next_reactions" ("author_id"); -------------------------------------------------------------------------------- /drizzle/0043_blue_tusk.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX IF EXISTS "rakt_type_idx";--> statement-breakpoint 2 | CREATE INDEX IF NOT EXISTS "rakt_type_idx" ON "gh_next_reactions" ("type"); -------------------------------------------------------------------------------- /drizzle/0044_brainy_jazinda.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX IF NOT EXISTS "ment_issue_idx" ON "gh_next_issue_user_mentions" ("issue_id"); -------------------------------------------------------------------------------- /drizzle/0045_bent_roulette.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX IF EXISTS "title_idx";--> statement-breakpoint 2 | CREATE INDEX IF NOT EXISTS "iss_title_idx" ON "gh_next_issues" ("title");--> statement-breakpoint 3 | CREATE INDEX IF NOT EXISTS "iss_created_idx" ON "gh_next_issues" ("created_at");--> statement-breakpoint 4 | CREATE INDEX IF NOT EXISTS "iss_updated_idx" ON "gh_next_issues" ("updated_at"); -------------------------------------------------------------------------------- /drizzle/0046_square_ikaris.sql: -------------------------------------------------------------------------------- 1 | -- Custom SQL migration file, put you code below! -- 2 | CREATE MATERIALIZED VIEW comment_count_per_issue AS 3 | SELECT 4 | issue_id, 5 | COUNT(id) AS comment_count 6 | FROM 7 | gh_next_comments 8 | GROUP BY 9 | issue_id; --> statement-breakpoint 10 | 11 | CREATE MATERIALIZED VIEW reaction_count_per_issue AS 12 | SELECT 13 | issue_id, 14 | COALESCE(SUM(CASE WHEN type = 'PLUS_ONE' THEN 1 ELSE 0 END), 0) AS plus_one_count, 15 | COALESCE(SUM(CASE WHEN type = 'MINUS_ONE' THEN 1 ELSE 0 END), 0) AS minus_one_count, 16 | COALESCE(SUM(CASE WHEN type = 'CONFUSED' THEN 1 ELSE 0 END), 0) AS confused_count, 17 | COALESCE(SUM(CASE WHEN type = 'EYES' THEN 1 ELSE 0 END), 0) AS eyes_count, 18 | COALESCE(SUM(CASE WHEN type = 'HEART' THEN 1 ELSE 0 END), 0) AS heart_count, 19 | COALESCE(SUM(CASE WHEN type = 'HOORAY' THEN 1 ELSE 0 END), 0) AS hooray_count, 20 | COALESCE(SUM(CASE WHEN type = 'LAUGH' THEN 1 ELSE 0 END), 0) AS laugh_count, 21 | COALESCE(SUM(CASE WHEN type = 'ROCKET' THEN 1 ELSE 0 END), 0) AS rocket_count 22 | FROM 23 | gh_next_reactions 24 | GROUP BY 25 | issue_id; 26 | -------------------------------------------------------------------------------- /drizzle/0047_same_stark_industries.sql: -------------------------------------------------------------------------------- 1 | -- Custom SQL migration file, put you code below! -- 2 | CREATE INDEX IF NOT EXISTS "comment_count_issue_id_idx" ON "comment_count_per_issue" ("issue_id");--> statement-breakpoint 3 | CREATE INDEX IF NOT EXISTS "reaction_count_issue_id_idx" ON "reaction_count_per_issue" ("issue_id"); 4 | -------------------------------------------------------------------------------- /drizzle/0048_dusty_leper_queen.sql: -------------------------------------------------------------------------------- 1 | -- Custom SQL migration file, put you code below! -- 2 | CREATE OR REPLACE FUNCTION refresh_comment_count_per_issue() 3 | RETURNS TRIGGER AS $$ 4 | BEGIN 5 | REFRESH MATERIALIZED VIEW CONCURRENTLY comment_count_per_issue; 6 | RETURN NEW; 7 | END; 8 | $$ LANGUAGE plpgsql;--> statement-breakpoint 9 | 10 | CREATE OR REPLACE FUNCTION refresh_reaction_count_per_issue() 11 | RETURNS TRIGGER AS $$ 12 | BEGIN 13 | REFRESH MATERIALIZED VIEW CONCURRENTLY reaction_count_per_issue; 14 | RETURN NEW; 15 | END; 16 | $$ LANGUAGE plpgsql;--> statement-breakpoint 17 | 18 | 19 | CREATE TRIGGER trigger_refresh_comment_count 20 | AFTER INSERT OR DELETE 21 | ON gh_next_comments 22 | FOR EACH ROW 23 | EXECUTE FUNCTION refresh_comment_count_per_issue();--> statement-breakpoint 24 | 25 | CREATE TRIGGER trigger_refresh_reaction_count 26 | AFTER INSERT OR DELETE 27 | ON gh_next_reactions 28 | FOR EACH ROW 29 | EXECUTE FUNCTION refresh_reaction_count_per_issue(); 30 | -------------------------------------------------------------------------------- /drizzle/0049_flowery_ikaris.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX IF NOT EXISTS "is_2_ass_issue_fk_index" ON "gh_next_issues_to_assignees" ("issue_id");--> statement-breakpoint 2 | CREATE INDEX IF NOT EXISTS "is_2_ass_assignee_fk_index" ON "gh_next_issues_to_assignees" ("assignee_id"); -------------------------------------------------------------------------------- /drizzle/0050_special_blue_blade.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX IF NOT EXISTS "lbl_2_iss_issue_fk_index" ON "gh_next_labels_to_issues" ("issue_id");--> statement-breakpoint 2 | CREATE INDEX IF NOT EXISTS "lbl_2_iss_assignee_fk_index" ON "gh_next_labels_to_issues" ("label_id"); -------------------------------------------------------------------------------- /drizzle/0051_tricky_tempest.sql: -------------------------------------------------------------------------------- 1 | -- Custom SQL migration file, put you code below! -- 2 | -- Custom SQL migration file, put you code below! -- 3 | CREATE OR REPLACE FUNCTION refresh_comment_count_per_issue() 4 | RETURNS TRIGGER AS $$ 5 | BEGIN 6 | REFRESH MATERIALIZED VIEW comment_count_per_issue; 7 | RETURN NEW; 8 | END; 9 | $$ LANGUAGE plpgsql;--> statement-breakpoint 10 | 11 | CREATE OR REPLACE FUNCTION refresh_reaction_count_per_issue() 12 | RETURNS TRIGGER AS $$ 13 | BEGIN 14 | REFRESH MATERIALIZED VIEW reaction_count_per_issue; 15 | RETURN NEW; 16 | END; 17 | $$ LANGUAGE plpgsql;--> statement-breakpoint 18 | -------------------------------------------------------------------------------- /drizzle/0052_goofy_prism.sql: -------------------------------------------------------------------------------- 1 | -- Custom SQL migration file, put you code below! -- 2 | CREATE COLLATION IF NOT EXISTS ci (provider = 'icu', locale = 'en-US-u-ks-level2', deterministic = false); -------------------------------------------------------------------------------- /drizzle/0053_familiar_dragon_man.sql: -------------------------------------------------------------------------------- 1 | -- ADD A case insensitive COLUMN for the username 2 | ALTER TABLE "gh_next_issues" ADD COLUMN "author_username_ci" varchar(255) COLLATE "ci"; 3 | -- migrate data from the old column to the new column 4 | UPDATE "gh_next_issues" SET "author_username_ci" = "author_username"; 5 | 6 | ALTER TABLE "gh_next_issues" DROP COLUMN "author_username"; -- remove the old column 7 | ALTER TABLE "gh_next_issues" RENAME COLUMN "author_username_ci" TO "author_username"; -- rename the new column to the old column 8 | ALTER TABLE "gh_next_issues" ALTER COLUMN "author_username" SET NOT NULL; -- add the not-null constraint 9 | 10 | CREATE INDEX "iss_author_uname_idx" ON "gh_next_issues" ("author_username"); -- readd the index -------------------------------------------------------------------------------- /drizzle/0054_marvelous_living_lightning.sql: -------------------------------------------------------------------------------- 1 | -- ADD A case insensitive COLUMN for the username 2 | ALTER TABLE "gh_next_users" ADD COLUMN "username_ci" varchar(255) COLLATE "ci"; 3 | -- migrate data from the old column to the new column 4 | UPDATE "gh_next_users" SET "username_ci" = "username"; 5 | 6 | ALTER TABLE "gh_next_users" DROP COLUMN "username"; -- remove the old column 7 | ALTER TABLE "gh_next_users" RENAME COLUMN "username_ci" TO "username"; -- rename the new column to the old column 8 | ALTER TABLE "gh_next_users" ALTER COLUMN "username" SET NOT NULL; -- add the not-null constraint 9 | 10 | CREATE UNIQUE INDEX "uname_uniq_idx" ON "gh_next_users" ("username"); -- readd the index-- Custom SQL migration file, put you code below! -- -------------------------------------------------------------------------------- /drizzle/0055_nasty_toad_men.sql: -------------------------------------------------------------------------------- 1 | -- ADD A case insensitive COLUMN for the username 2 | ALTER TABLE "gh_next_comments" ADD COLUMN "author_username_ci" varchar(255) COLLATE "ci"; 3 | -- migrate data from the old column to the new column 4 | UPDATE "gh_next_comments" SET "author_username_ci" = "author_username"; 5 | 6 | ALTER TABLE "gh_next_comments" DROP COLUMN "author_username"; -- remove the old column 7 | ALTER TABLE "gh_next_comments" RENAME COLUMN "author_username_ci" TO "author_username"; -- rename the new column to the old column 8 | ALTER TABLE "gh_next_comments" ALTER COLUMN "author_username" SET NOT NULL; -- add the not-null constraint 9 | 10 | CREATE INDEX "com_author_uname_idx" ON "gh_next_comments" ("author_username"); -- readd the index -------------------------------------------------------------------------------- /drizzle/0056_kind_whirlwind.sql: -------------------------------------------------------------------------------- 1 | -- ADD A case insensitive COLUMN for the username 2 | ALTER TABLE "gh_next_issue_events" ADD COLUMN "initiator_username_ci" varchar(255) COLLATE "ci"; 3 | -- migrate data from the old column to the new column 4 | UPDATE "gh_next_issue_events" SET "initiator_username_ci" = "initiator_username"; 5 | 6 | ALTER TABLE "gh_next_issue_events" DROP COLUMN "initiator_username"; -- remove the old column 7 | ALTER TABLE "gh_next_issue_events" RENAME COLUMN "initiator_username_ci" TO "initiator_username"; -- rename the new column to the old column 8 | ALTER TABLE "gh_next_issue_events" ALTER COLUMN "initiator_username" SET NOT NULL; -- add the not-null constraint 9 | -------------------------------------------------------------------------------- /drizzle/0057_goofy_mephisto.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX IF NOT EXISTS "ev_initiator_uname_idx" ON "gh_next_issue_events" ("initiator_username"); -------------------------------------------------------------------------------- /drizzle/0058_flimsy_ender_wiggin.sql: -------------------------------------------------------------------------------- 1 | -- ADD A case insensitive COLUMN for the username 2 | ALTER TABLE "gh_next_issue_events" ADD COLUMN "assignee_username_ci" varchar(255) COLLATE "ci"; 3 | -- migrate data from the old column to the new column 4 | UPDATE "gh_next_issue_events" SET "assignee_username_ci" = "assignee_username"; 5 | 6 | ALTER TABLE "gh_next_issue_events" DROP COLUMN "assignee_username"; -- remove the old column 7 | ALTER TABLE "gh_next_issue_events" RENAME COLUMN "assignee_username_ci" TO "assignee_username"; -- rename the new column to the old column 8 | -------------------------------------------------------------------------------- /drizzle/0059_cuddly_grim_reaper.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX IF NOT EXISTS "ev_assignee_uname_idx" ON "gh_next_issue_events" ("assignee_username"); -------------------------------------------------------------------------------- /drizzle/0060_wealthy_chimera.sql: -------------------------------------------------------------------------------- 1 | -- ADD A case insensitive COLUMN for the username 2 | ALTER TABLE "gh_next_issue_user_mentions" ADD COLUMN "username_ci" varchar(255) COLLATE "ci"; 3 | -- migrate data from the old column to the new column 4 | UPDATE "gh_next_issue_user_mentions" SET "username_ci" = "username"; 5 | 6 | ALTER TABLE "gh_next_issue_user_mentions" DROP COLUMN "username"; -- remove the old column 7 | ALTER TABLE "gh_next_issue_user_mentions" RENAME COLUMN "username_ci" TO "username"; -- rename the new column to the old column 8 | ALTER TABLE "gh_next_issue_user_mentions" ALTER COLUMN "username" SET NOT NULL; -- add the not-null constraint 9 | 10 | CREATE INDEX "ment_username_idx" ON "gh_next_issue_user_mentions" ("username"); -- readd the index -------------------------------------------------------------------------------- /drizzle/0061_striped_monster_badoon.sql: -------------------------------------------------------------------------------- 1 | -- ADD A case insensitive COLUMN for the username 2 | ALTER TABLE "gh_next_issues_to_assignees" ADD COLUMN "assignee_username_ci" varchar(255) COLLATE "ci"; 3 | -- migrate data from the old column to the new column 4 | UPDATE "gh_next_issues_to_assignees" SET "assignee_username_ci" = "assignee_username"; 5 | 6 | ALTER TABLE "gh_next_issues_to_assignees" DROP COLUMN "assignee_username"; -- remove the old column 7 | ALTER TABLE "gh_next_issues_to_assignees" RENAME COLUMN "assignee_username_ci" TO "assignee_username"; -- rename the new column to the old column 8 | ALTER TABLE "gh_next_issues_to_assignees" ALTER COLUMN "assignee_username" SET NOT NULL; -- add the not-null constraint 9 | -------------------------------------------------------------------------------- /drizzle/0062_naive_jubilee.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX IF NOT EXISTS "is_2_ass_assignee_uname_idx" ON "gh_next_issues_to_assignees" ("assignee_username"); -------------------------------------------------------------------------------- /drizzle/0063_brief_colossus.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues_to_assignees" ADD COLUMN "assignee_username_cs" varchar(255); 2 | 3 | -- copy data from the ci column to the cs column 4 | UPDATE "gh_next_issues_to_assignees" SET "assignee_username_cs" = "assignee_username"; 5 | 6 | ALTER TABLE "gh_next_issues_to_assignees" ALTER COLUMN "assignee_username_cs" SET NOT NULL; -- add the not-null constraint -------------------------------------------------------------------------------- /drizzle/0064_faithful_ben_parker.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION sync_username_insert() 2 | RETURNS TRIGGER AS $$ 3 | BEGIN 4 | NEW.assignee_username_cs := NEW.assignee_username; 5 | RETURN NEW; 6 | END; 7 | $$ LANGUAGE plpgsql; 8 | 9 | CREATE TRIGGER username_insert_trigger 10 | BEFORE INSERT ON gh_next_issues_to_assignees 11 | FOR EACH ROW EXECUTE FUNCTION sync_username_insert(); 12 | 13 | CREATE OR REPLACE FUNCTION sync_username_update() 14 | RETURNS TRIGGER AS $$ 15 | BEGIN 16 | -- Sync from username to username_cs 17 | IF NEW.assignee_username IS DISTINCT FROM OLD.assignee_username THEN 18 | NEW.assignee_username_cs := NEW.assignee_username; 19 | END IF; 20 | 21 | RETURN NEW; 22 | END; 23 | $$ LANGUAGE plpgsql; 24 | 25 | CREATE TRIGGER username_update_trigger 26 | BEFORE UPDATE ON gh_next_issues_to_assignees 27 | FOR EACH ROW EXECUTE FUNCTION sync_username_update(); 28 | -------------------------------------------------------------------------------- /drizzle/0065_sad_dagger.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues_to_assignees" ALTER COLUMN "assignee_username_cs" DROP NOT NULL; -------------------------------------------------------------------------------- /drizzle/0066_hot_quasimodo.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues" ADD COLUMN "author_username_cs" varchar(255); -------------------------------------------------------------------------------- /drizzle/0067_rapid_thunderbolt_ross.sql: -------------------------------------------------------------------------------- 1 | 2 | -- copy data from the ci column to the cs column 3 | UPDATE "gh_next_issues" SET "author_username_cs" = "author_username"; 4 | 5 | ALTER TABLE "gh_next_issues" ALTER COLUMN "author_username_cs" SET NOT NULL; -- add the not-null constraint -------------------------------------------------------------------------------- /drizzle/0068_wakeful_skaar.sql: -------------------------------------------------------------------------------- 1 | DROP TRIGGER IF EXISTS "username_update_trigger" ON "gh_next_issues_to_assignees"; 2 | DROP TRIGGER IF EXISTS "username_insert_trigger" ON "gh_next_issues_to_assignees"; 3 | ALTER TABLE "gh_next_issues_to_assignees" DROP COLUMN IF EXISTS "assignee_username_cs"; -------------------------------------------------------------------------------- /drizzle/0069_petite_shriek.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_issues_to_assignees" ADD COLUMN "assignee_username_cs" varchar(255); -------------------------------------------------------------------------------- /drizzle/0070_even_princess_powerful.sql: -------------------------------------------------------------------------------- 1 | -- ALTER TABLE "gh_next_issues_to_assignees" ADD COLUMN "assignee_username_cs" varchar(255); 2 | 3 | -- copy data from the ci column to the cs column 4 | UPDATE "gh_next_issues_to_assignees" SET "assignee_username_cs" = "assignee_username"; 5 | -------------------------------------------------------------------------------- /drizzle/0071_swift_captain_cross.sql: -------------------------------------------------------------------------------- 1 | -- Custom SQL migration file, put you code below! -- 2 | CREATE TRIGGER iss_2_ass_assignee_username_update_trigger 3 | BEFORE UPDATE ON gh_next_issues_to_assignees 4 | FOR EACH ROW EXECUTE FUNCTION sync_username_update(); 5 | 6 | CREATE TRIGGER iss_2_ass_assignee_username_insert_trigger 7 | BEFORE INSERT ON gh_next_issues_to_assignees 8 | FOR EACH ROW EXECUTE FUNCTION sync_username_insert(); -------------------------------------------------------------------------------- /drizzle/0072_oval_fallen_one.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION sync_author_username_insert() 2 | RETURNS TRIGGER AS $$ 3 | BEGIN 4 | NEW.author_username_cs := NEW.author_username; 5 | RETURN NEW; 6 | END; 7 | $$ LANGUAGE plpgsql; 8 | 9 | CREATE TRIGGER iss_author_username_insert_trigger 10 | BEFORE INSERT ON gh_next_issues 11 | FOR EACH ROW EXECUTE FUNCTION sync_author_username_insert(); 12 | 13 | CREATE OR REPLACE FUNCTION sync_author_username_update() 14 | RETURNS TRIGGER AS $$ 15 | BEGIN 16 | -- Sync from username to username_cs 17 | IF NEW.author_username IS DISTINCT FROM OLD.author_username THEN 18 | NEW.author_username_cs := NEW.author_username; 19 | END IF; 20 | 21 | RETURN NEW; 22 | END; 23 | $$ LANGUAGE plpgsql; 24 | 25 | CREATE TRIGGER iss_author_username_update_trigger 26 | BEFORE UPDATE ON gh_next_issues 27 | FOR EACH ROW EXECUTE FUNCTION sync_author_username_update(); 28 | -------------------------------------------------------------------------------- /drizzle/0073_sad_crusher_hogan.sql: -------------------------------------------------------------------------------- 1 | -- ADD A case insensitive COLUMN for the username 2 | ALTER TABLE "gh_next_repositories" ADD COLUMN "name_ci" varchar(255) COLLATE "ci"; 3 | -- migrate data from the old column to the new column 4 | UPDATE "gh_next_repositories" SET "name_ci" = "name"; 5 | 6 | ALTER TABLE "gh_next_repositories" DROP COLUMN "name"; -- remove the old column 7 | ALTER TABLE "gh_next_repositories" RENAME COLUMN "name_ci" TO "name"; -- rename the new column to the old column 8 | ALTER TABLE "gh_next_repositories" ALTER COLUMN "name" SET NOT NULL; -- add the not-null constraint 9 | 10 | CREATE UNIQUE INDEX "repo_name_uniq_idx" ON "gh_next_repositories" ("name"); -- readd the unique constraint-- -------------------------------------------------------------------------------- /drizzle/0074_dear_prowler.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX IF EXISTS "repo_name_idx"; -------------------------------------------------------------------------------- /drizzle/0075_volatile_harry_osborn.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "gh_next_repositories" ADD COLUMN "description" text DEFAULT '' NOT NULL; 2 | UPDATE "gh_next_repositories" SET "description" = 'A minimal Github clone built on nextjs app router.' WHERE 1=1; -------------------------------------------------------------------------------- /ecosystem.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: "gh-clone", 5 | script: ".next/standalone/server.js", 6 | time: true, 7 | instances: 2, 8 | autorestart: true, 9 | max_restarts: 5, 10 | exec_mode: "cluster_mode", 11 | watch: false, 12 | max_memory_restart: "1G", 13 | wait_ready: true, 14 | listen_timeout: 10_000, 15 | increment_var: "PORT", 16 | env: { 17 | PORT: 8892, 18 | HOSTNAME: "0.0.0.0" 19 | } 20 | } 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /globals.d.ts: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from "react-dom"; 2 | import * as React from "react"; 3 | 4 | declare global { 5 | namespace NodeJS { 6 | interface Global { 7 | crypto: Crypto; 8 | } 9 | } 10 | 11 | export type ClientReferenceManifestEntry = { 12 | id: string; 13 | // chunks is a double indexed array of chunkId / chunkFilename pairs 14 | chunks: Array; 15 | name: string; 16 | }; 17 | 18 | export type ClientManifest = { 19 | [id: string]: ClientReferenceManifestEntry; 20 | }; 21 | 22 | type RSCManifest = { 23 | clientModules?: ClientManifest; 24 | moduleLoading?: Record; 25 | ssrModuleMapping?: Record; 26 | }; 27 | 28 | var __RSC_MANIFEST: Record | null; 29 | } 30 | 31 | declare module "react" { 32 | export function unstable_postpone(reason?: string): never; 33 | } 34 | -------------------------------------------------------------------------------- /latency.ts: -------------------------------------------------------------------------------- 1 | const numberOfRequests = 50; // Number of requests to send 2 | 3 | async function measureLatency(url: string, label: string) { 4 | let totalLatency = 0; 5 | 6 | for (let i = 0; i < numberOfRequests; i++) { 7 | const startTime = performance.now(); 8 | await fetch(url); 9 | const endTime = performance.now(); 10 | totalLatency += endTime - startTime; 11 | } 12 | 13 | const averageLatency = totalLatency / numberOfRequests; 14 | console.log( 15 | `\x1b[33m[${label}]\x1b[37m Average latency for ${url} (${numberOfRequests} requests) : ${averageLatency} ms` 16 | ); 17 | } 18 | 19 | Promise.all([ 20 | measureLatency( 21 | "https://gh.fredkiss.dev/Fredkiss3/gh-next/issues/58248", 22 | "production" 23 | ), 24 | measureLatency( 25 | "https://gh-dev.fredkiss.dev/Fredkiss3/gh-next/issues/58248", 26 | "staging" 27 | ) 28 | // measureLatency("http://localhost:3001/Fredkiss3/gh-next/issues", "docker"), 29 | // measureLatency( 30 | // "http://localhost:3000/Fredkiss3/gh-next/issues", 31 | // "docker swarm" 32 | // ), 33 | // measureLatency( 34 | // "http://localhost:3002/Fredkiss3/gh-next/issues", 35 | // "npm run start" 36 | // ) 37 | ]).catch(console.error); 38 | -------------------------------------------------------------------------------- /migrate.mjs: -------------------------------------------------------------------------------- 1 | import { migrate } from "drizzle-orm/postgres-js/migrator"; 2 | import postgres from "postgres"; 3 | import { drizzle } from "drizzle-orm/postgres-js"; 4 | 5 | const db = drizzle( 6 | postgres(process.env.REMOTE_DATABASE_URL ?? process.env.DATABASE_URL) 7 | ); 8 | 9 | async function main() { 10 | await migrate(db, { migrationsFolder: "drizzle" }); 11 | process.exit(0); 12 | } 13 | main(); 14 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import "./src/env-config.mjs"; 3 | 4 | /** @type {import('next').NextConfig} */ 5 | const nextConfig = { 6 | reactStrictMode: true, 7 | output: "standalone", 8 | cacheHandler: 9 | process.env.NODE_ENV === "production" 10 | ? "./custom-incremental-cache-handler.mjs" 11 | : undefined, 12 | cacheMaxMemorySize: 0, 13 | experimental: { 14 | taint: true 15 | }, 16 | logging: { 17 | fetches: { 18 | fullUrl: true 19 | } 20 | }, 21 | images: { 22 | remotePatterns: [ 23 | { 24 | protocol: "https", 25 | hostname: "avatars.githubusercontent.com" 26 | } 27 | ] 28 | } 29 | }; 30 | 31 | export default nextConfig; 32 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /pr-preview-workflow/README.md: -------------------------------------------------------------------------------- 1 | # preview-workflow 2 | 3 | To install dependencies: 4 | 5 | ```bash 6 | bun install 7 | ``` 8 | 9 | To run: 10 | 11 | ```bash 12 | bun run index.ts 13 | ``` 14 | 15 | This project was created using `bun init` in bun v1.0.25. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /pr-preview-workflow/add-caddyfile.ts: -------------------------------------------------------------------------------- 1 | import { $, file, write } from "bun"; 2 | 3 | export async function addCaddyfile( 4 | PR_ID: number, 5 | PR_BRANCH: string, 6 | CADDY_CONFIG_FOLDER_PATH: string, 7 | shouldReloadCaddy: boolean 8 | ) { 9 | await $`echo '[🔄 Caddy] adding preview environment config...'`; 10 | const caddyfileToAdd = file( 11 | `${pathWithoutSlash(CADDY_CONFIG_FOLDER_PATH)}/pull-request-${PR_ID}.caddy` 12 | ); 13 | 14 | if (await caddyfileToAdd.exists()) { 15 | await $`echo '[ℹ️ Caddy] Configuration for preview branch pull request #${PR_ID} already exists, skipping work.'`; 16 | return; 17 | } 18 | 19 | const CADDY_TEMPLATE_CONTENT = `gh-${PR_ID}.gh.fredkiss.dev, gh-${PR_BRANCH}.gh.fredkiss.dev { 20 | route { 21 | sablier { 22 | group gh-next-${PR_ID} 23 | session_duration 30m 24 | dynamic { 25 | theme ghost 26 | display_name preview environment ${PR_BRANCH} (pull request ID: ${PR_ID}) 27 | refresh_frequency 5s 28 | } 29 | } 30 | reverse_proxy http://gh-next-${PR_ID}:3000 { 31 | header_up Host {http.request.host} 32 | # disables buffering 33 | flush_interval -1 34 | } 35 | } 36 | log 37 | }`; 38 | 39 | await write(caddyfileToAdd, CADDY_TEMPLATE_CONTENT); 40 | await $`echo '[✅ caddy] config for pull request #${PR_ID} added successfully'`; 41 | if (shouldReloadCaddy) { 42 | // reload caddy service in docker 43 | await $`echo '[🔄 Caddy] reloading caddy server...'`; 44 | const { exitCode, stderr } = 45 | await $`docker exec $(docker ps -q -f name=caddy-stack_proxy) caddy reload -c /etc/caddy/Caddyfile`; 46 | if (exitCode !== 0) { 47 | await $`echo '[❌ Caddy] caddy service encountered an unexpected error : ${stderr.toString()}'`; 48 | process.exit(1); 49 | } 50 | await $`echo '[✅ Caddy] caddy server reloaded succesfully'`; 51 | } 52 | } 53 | 54 | function pathWithoutSlash(path: string) { 55 | if (path.endsWith("/")) { 56 | return path.substring(0, path.length - 1); 57 | } 58 | return path; 59 | } 60 | -------------------------------------------------------------------------------- /pr-preview-workflow/add-docker-app.ts: -------------------------------------------------------------------------------- 1 | import { $, file, write } from "bun"; 2 | 3 | export async function addDockerApp( 4 | PR_ID: number, 5 | shouldReloadDockerStack: boolean 6 | ) { 7 | await $`echo '[🔄 Docker] adding docker stack config...'`; 8 | const COMPOSE_FILE_PATH = `./docker/docker-stack.pr-${PR_ID}.yaml`; 9 | 10 | const composeFile = file(COMPOSE_FILE_PATH); 11 | 12 | if (await composeFile.exists()) { 13 | await $`echo '[ℹ️ Docker] docker stack config for pull request #${PR_ID} already exists, skipping work.'`; 14 | if (shouldReloadDockerStack) { 15 | await $`echo '[🔄 Docker] updating docker services...'`; 16 | const { exitCode, stderr } = 17 | await $`docker stack deploy --with-registry-auth --compose-file ${COMPOSE_FILE_PATH} gh-stack-pr-${PR_ID}`; 18 | 19 | if (exitCode !== 0) { 20 | await $`echo '[❌ Docker] docker services encountered an unexpected error : ${stderr.toString()}'`; 21 | process.exit(1); 22 | } 23 | await $`echo '[✅ Docker] docker services updated succesfully'`; 24 | } 25 | return; 26 | } 27 | 28 | // Placeholder text in the Docker Compose file 29 | const DOCKER_STACK_TEMPLATE = `# service configuration for pull request #132 30 | version: "3.4" 31 | 32 | services: 33 | gh-next-${PR_ID}: 34 | image: dcr.fredkiss.dev/gh-next:pr-${PR_ID} 35 | deploy: 36 | replicas: 0 37 | update_config: 38 | parallelism: 1 39 | delay: 5s 40 | order: start-first 41 | failure_action: rollback 42 | restart_policy: 43 | condition: on-failure 44 | delay: 5s 45 | max_attempts: 3 46 | window: 120s 47 | labels: 48 | - sablier.enable=true 49 | - sablier.group=gh-next-${PR_ID} 50 | networks: 51 | - gh-next 52 | networks: 53 | gh-next: 54 | external: true 55 | `; 56 | await write(composeFile, DOCKER_STACK_TEMPLATE); 57 | await $`echo '[✅ Docker] Added docker stack config file for pull request #${PR_ID}.'`; 58 | if (shouldReloadDockerStack) { 59 | await $`echo '[🔄 Docker] updating docker services...'`; 60 | const { exitCode, stderr } = 61 | await $`docker stack deploy --with-registry-auth --compose-file ${COMPOSE_FILE_PATH} gh-stack-pr-${PR_ID}`; 62 | 63 | if (exitCode !== 0) { 64 | await $`echo '[❌ Docker] docker services encountered an unexpected error : ${stderr.toString()}'`; 65 | process.exit(1); 66 | } 67 | await $`echo '[✅ Docker] docker services updated succesfully'`; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pr-preview-workflow/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fredkiss3/gh-next/178a7d3864ddcedb5d82d7a4c4691b7aa8759b25/pr-preview-workflow/bun.lockb -------------------------------------------------------------------------------- /pr-preview-workflow/index.ts: -------------------------------------------------------------------------------- 1 | import { parseArgs } from "node:util"; 2 | import { z } from "zod"; 3 | import { addDockerApp } from "./add-docker-app"; 4 | import { addCaddyfile } from "./add-caddyfile"; 5 | 6 | const argSchema = z.object({ 7 | "pr-id": z.coerce.number(), 8 | "pr-branch": z.string(), 9 | "caddy-config-path": z.string(), 10 | "reload-caddy": z.boolean(), 11 | "reload-docker": z.boolean() 12 | }); 13 | 14 | const { values } = parseArgs({ 15 | args: Bun.argv, 16 | options: { 17 | "pr-id": { 18 | type: "string" 19 | }, 20 | "pr-branch": { 21 | type: "string" 22 | }, 23 | "caddy-config-path": { 24 | type: "string", 25 | default: "./caddy-test" // for testing locally 26 | }, 27 | "reload-caddy": { 28 | type: "boolean", 29 | default: false 30 | }, 31 | "reload-docker": { 32 | type: "boolean", 33 | default: false 34 | } 35 | }, 36 | strict: true, 37 | allowPositionals: true 38 | }); 39 | 40 | const { 41 | "pr-id": PR_ID, 42 | "pr-branch": PR_BRANCH, 43 | "caddy-config-path": CADDY_CONFIG_FOLDER_PATH, 44 | "reload-caddy": shouldReloadCaddy, 45 | "reload-docker": shouldReloadDockerStack 46 | } = argSchema.parse(values); 47 | 48 | await addDockerApp(PR_ID, shouldReloadDockerStack); 49 | await addCaddyfile( 50 | PR_ID, 51 | PR_BRANCH, 52 | CADDY_CONFIG_FOLDER_PATH, 53 | shouldReloadCaddy 54 | ); 55 | -------------------------------------------------------------------------------- /pr-preview-workflow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preview-workflow", 3 | "module": "index.ts", 4 | "type": "module", 5 | "devDependencies": { 6 | "@types/bun": "latest" 7 | }, 8 | "peerDependencies": { 9 | "typescript": "^5.0.0" 10 | }, 11 | "dependencies": { 12 | "zod": "^3.22.4" 13 | } 14 | } -------------------------------------------------------------------------------- /pr-preview-workflow/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleDetection": "force", 7 | "jsx": "react-jsx", 8 | "allowJs": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "forceConsistentCasingInFileNames": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | module.exports = { 3 | tabWidth: 2, 4 | semi: true, 5 | arrowParens: "always", 6 | // plugins: ["prettier-plugin-tailwindcss"], 7 | trailingComma: "none" 8 | }; 9 | -------------------------------------------------------------------------------- /public/404-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fredkiss3/gh-next/178a7d3864ddcedb5d82d7a4c4691b7aa8759b25/public/404-text.png -------------------------------------------------------------------------------- /public/dark_theme_preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/favicon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fredkiss3/gh-next/178a7d3864ddcedb5d82d7a4c4691b7aa8759b25/public/favicon-dark.png -------------------------------------------------------------------------------- /public/favicon-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fredkiss3/gh-next/178a7d3864ddcedb5d82d7a4c4691b7aa8759b25/public/favicon.png -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/guirlande1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fredkiss3/gh-next/178a7d3864ddcedb5d82d7a4c4691b7aa8759b25/public/guirlande1.png -------------------------------------------------------------------------------- /public/guirlande2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fredkiss3/gh-next/178a7d3864ddcedb5d82d7a4c4691b7aa8759b25/public/guirlande2.png -------------------------------------------------------------------------------- /public/guirlande3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fredkiss3/gh-next/178a7d3864ddcedb5d82d7a4c4691b7aa8759b25/public/guirlande3.png -------------------------------------------------------------------------------- /public/light_theme_preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/octoshadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fredkiss3/gh-next/178a7d3864ddcedb5d82d7a4c4691b7aa8759b25/public/octoshadow.png -------------------------------------------------------------------------------- /public/octostar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fredkiss3/gh-next/178a7d3864ddcedb5d82d7a4c4691b7aa8759b25/public/octostar.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /public/spaceship-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fredkiss3/gh-next/178a7d3864ddcedb5d82d7a4c4691b7aa8759b25/public/spaceship-shadow.png -------------------------------------------------------------------------------- /public/spaceship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fredkiss3/gh-next/178a7d3864ddcedb5d82d7a4c4691b7aa8759b25/public/spaceship.png -------------------------------------------------------------------------------- /rsdw.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-server-dom-webpack/server.edge" { 2 | export type ReactClientValue = any; 3 | export type ClientReferenceManifestEntry = { 4 | id: string; 5 | // chunks is a double indexed array of chunkId / chunkFilename pairs 6 | chunks: Array; 7 | name: string; 8 | }; 9 | 10 | export type ClientManifest = { 11 | [id: string]: ClientReferenceManifestEntry; 12 | }; 13 | 14 | export type Options = { 15 | identifierPrefix?: string; 16 | signal?: AbortSignal; 17 | onError?: (error: unknown) => void; 18 | onPostpone?: (reason: string) => void; 19 | }; 20 | 21 | export function renderToReadableStream( 22 | model: ReactClientValue, 23 | webpackMap: ClientManifest, 24 | options?: Options 25 | ): ReadableStream; 26 | } 27 | 28 | declare module "react-server-dom-webpack/client.edge" { 29 | export function createFromReadableStream( 30 | stream: ReadableStream, 31 | options: { 32 | ssrManifest: { 33 | moduleLoading: any; 34 | moduleMap: any; 35 | }; 36 | } 37 | ): Promise; 38 | } 39 | 40 | declare module "react-server-dom-webpack/client" { 41 | export function createFromReadableStream( 42 | stream: ReadableStream, 43 | options?: Record 44 | ): Promise; 45 | } 46 | -------------------------------------------------------------------------------- /scripts/fetchUsers.ts: -------------------------------------------------------------------------------- 1 | import { users } from "~/lib/server/db/schema/user.sql"; 2 | import { fetchFromGithubAPI } from "~/lib/server/utils.server"; 3 | import { drizzle } from "drizzle-orm/postgres-js"; 4 | import postgres from "postgres"; 5 | import { eq } from "drizzle-orm"; 6 | 7 | const db = drizzle(postgres(process.env.DATABASE_URL!)); 8 | 9 | const dbUsers = await db.select().from(users); 10 | 11 | for (const user of dbUsers) { 12 | const userFromGithub = await fetchFromGithubAPI<{ 13 | user: { 14 | name: string; 15 | bio: string; 16 | location: string; 17 | company: string; 18 | }; 19 | }>( 20 | /* GraphQL */ ` 21 | query ($login: String!) { 22 | user(login: $login) { 23 | name 24 | bio 25 | location 26 | company 27 | } 28 | } 29 | `, 30 | { 31 | login: user.username 32 | } 33 | ).catch((error) => null); 34 | 35 | if (userFromGithub) { 36 | await db 37 | .update(users) 38 | .set(userFromGithub.user) 39 | .where(eq(users.username, user.username)); 40 | console.log(`updated user [${user.username}] with infos : `); 41 | console.dir(userFromGithub.user, { depth: null }); 42 | } 43 | } 44 | 45 | process.exit(); 46 | -------------------------------------------------------------------------------- /searching.md: -------------------------------------------------------------------------------- 1 | ## SEARCH 2 | 3 | - https://remarkjs.github.io/react-markdown/ 4 | - https://github.com/remarkjs/remark-rehype 5 | - https://github.com/remarkjs/remark-github 6 | - https://unifiedjs.com/explore/package/rehype-raw/ 7 | - https://github.com/remarkjs/react-markdown/pull/682 8 | - https://github.com/remarkjs/react-markdown/blob/main/lib/index.js 9 | 10 | 11 | ### Debugging 12 | 13 | - https://github.com/vercel/next.js/issues/48748 14 | 15 | ### Learning 16 | 17 | - https://github.github.com/gfm/#disallowed-raw-html-extension- 18 | -------------------------------------------------------------------------------- /src/actions/markdown.action.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { Markdown } from "~/components/markdown/markdown"; 4 | import { renderRSCtoString } from "~/components/custom-rsc-renderer/render-rsc-to-string"; 5 | 6 | export async function getMarkdownPreview( 7 | content: string, 8 | repositoryPath: `${string}/${string}` 9 | ) { 10 | return await renderRSCtoString( 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/actions/middlewares.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | import { revalidatePath } from "next/cache"; 3 | import { getAuthedUser } from "./auth.action"; 4 | import { getSession } from "./session.action"; 5 | import type { FunctionWithoutLastArg } from "~/lib/types"; 6 | import type { Session } from "~/lib/server/session.server"; 7 | import type { User } from "~/lib/server/db/schema/user.sql"; 8 | 9 | export type AuthState = { 10 | currentUser: User; 11 | session: Session; 12 | }; 13 | 14 | export type AuthError = { 15 | type: "AUTH_ERROR"; 16 | }; 17 | 18 | export type AuthedServerAction< 19 | Action extends (...args: [...any[], auth: AuthState]) => Promise 20 | > = ( 21 | ...args: Parameters> 22 | ) => Promise> | AuthError>; 23 | 24 | export function withAuth Promise>( 25 | action: Action 26 | ) { 27 | return (async (...args: Parameters>) => { 28 | const session = await getSession(); 29 | const currentUser = await getAuthedUser(); 30 | 31 | if (!currentUser) { 32 | await session.addFlash({ 33 | type: "warning", 34 | message: "You must be authenticated to do this action." 35 | }); 36 | 37 | revalidatePath("/"); 38 | return { 39 | type: "AUTH_ERROR" as const 40 | } satisfies AuthError; 41 | } 42 | 43 | return action(...args, { currentUser, session }); 44 | }) as AuthedServerAction; 45 | } 46 | -------------------------------------------------------------------------------- /src/actions/theme.action.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { z } from "zod"; 4 | import { getSession } from "./session.action"; 5 | import { cache } from "react"; 6 | import { updateUserTheme } from "~/models/user"; 7 | import { revalidatePath } from "next/cache"; 8 | 9 | import { users } from "~/lib/server/db/schema/user.sql"; 10 | import { createSelectSchema } from "drizzle-zod"; 11 | import { redirect } from "next/navigation"; 12 | import { withAuth, type AuthState } from "./middlewares"; 13 | 14 | const userThemeSchema = createSelectSchema(users).pick({ 15 | preferred_theme: true 16 | }); 17 | const themeSchema = userThemeSchema.shape.preferred_theme; 18 | 19 | export type Theme = z.TypeOf; 20 | 21 | export const getTheme = cache(async function getTheme() { 22 | const session = await getSession(); 23 | 24 | return session.user?.preferred_theme ?? "system"; 25 | }); 26 | 27 | export const updateTheme = withAuth(async function updateTheme( 28 | formData: FormData, 29 | { session, currentUser }: AuthState 30 | ) { 31 | const themeResult = themeSchema.safeParse(formData.get("theme")?.toString()); 32 | 33 | if (!themeResult.success) { 34 | revalidatePath("/settings/appearance"); 35 | await session.addFlash({ 36 | type: "warning", 37 | message: "Invalid theme provided, please retry" 38 | }); 39 | return; 40 | } 41 | 42 | const theme = themeResult.data; 43 | 44 | await updateUserTheme(theme, currentUser.id); 45 | await session.setUserTheme(theme); 46 | 47 | await session.addFlash({ 48 | type: "success", 49 | message: `Theme changed to ${theme}` 50 | }); 51 | 52 | revalidatePath("/settings/appearance"); 53 | redirect("/settings/appearance"); 54 | }); 55 | -------------------------------------------------------------------------------- /src/actions/user.action.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { revalidatePath } from "next/cache"; 4 | import { withAuth, type AuthError, type AuthState } from "./middlewares"; 5 | import { updateUserInfos } from "~/models/user"; 6 | import { 7 | updateUserProfileInfosInputValidator, 8 | type UpdateUserProfileInfosInput 9 | } from "~/models/dto/update-profile-info-input-validator"; 10 | 11 | import type { FormState } from "~/lib/types"; 12 | 13 | export const updateUserProfile = withAuth(async ( 14 | _previousState: FormState | AuthError, 15 | formData: FormData, 16 | { session, currentUser }: AuthState 17 | ): Promise> => { 18 | const result = updateUserProfileInfosInputValidator.safeParse( 19 | Object.fromEntries(formData) 20 | ); 21 | 22 | if (!result.success) { 23 | return { 24 | type: "error" as const, 25 | fieldErrors: result.error.flatten().fieldErrors, 26 | formData: { 27 | name: formData.get("name")?.toString() ?? null, 28 | bio: formData.get("bio")?.toString() ?? null, 29 | company: formData.get("company")?.toString() ?? null, 30 | location: formData.get("company")?.toString() ?? null 31 | } 32 | }; 33 | } 34 | 35 | await updateUserInfos(result.data, currentUser.id); 36 | 37 | await session.addFlash({ 38 | type: "success", 39 | message: "Profile updated successfully" 40 | }); 41 | 42 | revalidatePath(`/`); 43 | return { 44 | type: "success" as const, 45 | message: "Success" 46 | }; 47 | }); 48 | -------------------------------------------------------------------------------- /src/app/(app)/@header_subnav/[user]/[repository]/[...pages]/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { HeaderNavLinks } from "~/components/header/header-navlinks"; 3 | 4 | import type { PageProps } from "~/lib/types"; 5 | 6 | export default function RepositoryHeaderSubnav({ 7 | params 8 | }: PageProps<{ 9 | user: string; 10 | repository: string; 11 | }>) { 12 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/(app)/@header_subnav/[user]/[repository]/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { HeaderNavLinks } from "~/components/header/header-navlinks"; 3 | 4 | import type { PageProps } from "~/lib/types"; 5 | 6 | export default function RepositoryHeaderSubnav({ 7 | params 8 | }: PageProps<{ 9 | user: string; 10 | repository: string; 11 | }>) { 12 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/(app)/@header_subnav/[user]/page.tsx: -------------------------------------------------------------------------------- 1 | export default function UserHeaderSubNav() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/(app)/@header_subnav/default.tsx: -------------------------------------------------------------------------------- 1 | export default function DefaultHeaderSubNav() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/(app)/@header_subnav/notifications/page.tsx: -------------------------------------------------------------------------------- 1 | export default function SettingsHeaderSubNav() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/(app)/@header_subnav/settings/[...pages]/page.tsx: -------------------------------------------------------------------------------- 1 | export default function SettingsHeaderSubNav() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/(app)/@header_subnav/settings/page.tsx: -------------------------------------------------------------------------------- 1 | export default function SettingsHeaderSubNav() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/(app)/@page_title/[user]/[repository]/[...pages]/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | // components 3 | import Link from "next/link"; 4 | import { HoverCard } from "~/components/hovercard/hovercard"; 5 | import { UserHoverCardContents } from "~/components/hovercard/user-hovercard-contents"; 6 | 7 | // utils 8 | import { clsx } from "~/lib/shared/utils.shared"; 9 | import { getRepositoryByOwnerAndName } from "~/models/repository"; 10 | import { notFound } from "next/navigation"; 11 | 12 | // types 13 | import type { PageProps } from "~/lib/types"; 14 | 15 | export async function RepositoryPageTitle({ 16 | params 17 | }: PageProps<{ 18 | user: string; 19 | repository: string; 20 | }>) { 21 | const repository = await getRepositoryByOwnerAndName( 22 | params.user, 23 | params.repository 24 | ); 25 | 26 | if (repository === null) { 27 | notFound(); 28 | } 29 | 30 | return ( 31 |
32 | 44 | } 45 | > 46 | 55 | {repository.owner.username} 56 | 57 | 58 | / 59 | 66 | 67 | {repository.name} 68 | 69 | 70 |
71 | ); 72 | } 73 | 74 | export default RepositoryPageTitle; 75 | -------------------------------------------------------------------------------- /src/app/(app)/@page_title/[user]/[repository]/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | // types 3 | import { RepositoryPageTitle } from "~/app/(app)/@page_title/[user]/[repository]/[...pages]/page"; 4 | 5 | export default RepositoryPageTitle; 6 | -------------------------------------------------------------------------------- /src/app/(app)/@page_title/[user]/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | // components 3 | import Link from "next/link"; 4 | 5 | // utils 6 | import { clsx } from "~/lib/shared/utils.shared"; 7 | import { getUserByUsername } from "~/models/user"; 8 | import { notFound } from "next/navigation"; 9 | 10 | // types 11 | import type { PageProps } from "~/lib/types"; 12 | 13 | export default async function UserPageTitle({ 14 | params 15 | }: PageProps<{ 16 | user: string; 17 | }>) { 18 | const user = await getUserByUsername(params.user); 19 | 20 | if (user === null) { 21 | notFound(); 22 | } 23 | return ( 24 | 34 | {user.username} 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/app/(app)/@page_title/default.tsx: -------------------------------------------------------------------------------- 1 | export default function DefaultPageTitle() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/(app)/@page_title/notifications/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Link from "next/link"; 3 | import { clsx } from "~/lib/shared/utils.shared"; 4 | 5 | export default function SettingPageTitle() { 6 | return ( 7 | 16 | Notifications 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/(app)/@page_title/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Link from "next/link"; 3 | import { clsx } from "~/lib/shared/utils.shared"; 4 | 5 | export default function ExplorePageTitle() { 6 | return ( 7 | 16 | Explore 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/(app)/@page_title/settings/[...pages]/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Link from "next/link"; 3 | import { clsx } from "~/lib/shared/utils.shared"; 4 | 5 | export default function SettingPageTitle() { 6 | return ( 7 | 16 | Settings 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/(app)/@page_title/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Link from "next/link"; 3 | import { clsx } from "~/lib/shared/utils.shared"; 4 | 5 | export default function SettingPageTitle() { 6 | return ( 7 | 16 | Settings 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/(app)/[user]/[repository]/actions/page.tsx: -------------------------------------------------------------------------------- 1 | // components 2 | import { HomeIcon } from "@primer/octicons-react"; 3 | import { Button } from "~/components/button"; 4 | 5 | // utils 6 | import { clsx } from "~/lib/shared/utils.shared"; 7 | 8 | // types 9 | import type { Metadata } from "next"; 10 | import type { PageProps } from "~/lib/types"; 11 | 12 | export const metadata: Metadata = { 13 | title: "Actions" 14 | }; 15 | 16 | export default function ActionPage( 17 | props: PageProps<{ user: string; repository: string }> 18 | ) { 19 | return ( 20 |
27 |

28 | This page has not been implemented yet 29 |

30 | 31 |

Come back later when we implement this.

32 | 33 | 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/app/(app)/[user]/[repository]/issues/new/page.tsx: -------------------------------------------------------------------------------- 1 | // components 2 | import { NewIssueForm } from "~/components/issues/new-issue-form"; 3 | import { Markdown } from "~/components/markdown/markdown"; 4 | 5 | // utils 6 | import { notFound } from "next/navigation"; 7 | import { getUserOrRedirect } from "~/actions/auth.action"; 8 | import { getRepositoryByOwnerAndName } from "~/models/repository"; 9 | 10 | // types 11 | import type { Metadata } from "next"; 12 | import type { PageProps } from "~/lib/types"; 13 | 14 | type NewIssuePageProps = PageProps<{ 15 | user: string; 16 | repository: string; 17 | }>; 18 | 19 | export const metadata: Metadata = { 20 | title: "New Issue" 21 | }; 22 | 23 | export default async function NewIssuePage(props: NewIssuePageProps) { 24 | const [repository, currentUser] = await Promise.all([ 25 | getRepositoryByOwnerAndName(props.params.user, props.params.repository), 26 | getUserOrRedirect( 27 | `/${props.params.user}/${props.params.repository}/issues/new` 28 | ) 29 | ]); 30 | 31 | if (!repository) { 32 | notFound(); 33 | } 34 | 35 | return ( 36 | { 43 | "use server"; 44 | return ; 45 | }} 46 | /> 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/app/(app)/[user]/[repository]/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import * as React from "react"; 3 | import { getRepositoryByOwnerAndName } from "~/models/repository"; 4 | import type { LayoutProps, PageProps } from "~/lib/types"; 5 | 6 | export async function generateMetadata( 7 | props: LayoutProps<{ 8 | user: string; 9 | repository: string; 10 | }> 11 | ): Promise { 12 | const repository = await getRepositoryByOwnerAndName( 13 | props.params.user, 14 | props.params.repository 15 | ); 16 | 17 | if (!repository) { 18 | return { 19 | title: "Not-Found" 20 | }; 21 | } 22 | 23 | return { 24 | title: { 25 | template: `%s · ${repository.owner.username}/${repository.name}`, 26 | default: `${repository.owner.username}/${repository.name} · ${repository.description}` 27 | } 28 | }; 29 | } 30 | 31 | export default function RepositoryLayout({ 32 | children 33 | }: LayoutProps<{ 34 | user: string; 35 | repository: string; 36 | }>) { 37 | return children; 38 | } 39 | -------------------------------------------------------------------------------- /src/app/(app)/[user]/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { PageProps } from "~/lib/types"; 3 | 4 | export default async function UserPage({ 5 | params: { username } 6 | }: PageProps<{ username: string }>) { 7 | return ( 8 | <> 9 |

User {username}

10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/app/(app)/layout.tsx: -------------------------------------------------------------------------------- 1 | // components 2 | import { Footer } from "~/components/footer"; 3 | import { Header } from "~/components/header/header"; 4 | import { clsx } from "~/lib/shared/utils.shared"; 5 | 6 | export default async function AppLayout({ 7 | children, 8 | header_subnav, 9 | page_title 10 | }: { 11 | children: React.ReactNode; 12 | header_subnav: React.ReactNode; 13 | page_title: React.ReactNode; 14 | }) { 15 | return ( 16 | <> 17 |
{header_subnav}
18 |
22 | {children} 23 |
24 |