├── .dockerignore ├── .env.example ├── .eslintrc.js ├── .github └── workflows │ └── docker-image.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── apps ├── docs │ ├── README.md │ ├── _snippets │ │ └── snippet-example.mdx │ ├── api-reference │ │ ├── endpoint │ │ │ ├── changelog │ │ │ │ ├── delete-projects-changelogs.mdx │ │ │ │ ├── get-projects-changelogs.mdx │ │ │ │ ├── patch-projects-changelogs.mdx │ │ │ │ └── post-projects-changelogs.mdx │ │ │ ├── feedback-comments │ │ │ │ ├── delete-projects-feedback-comments.mdx │ │ │ │ └── get-projects-feedback-comments.mdx │ │ │ ├── feedback-tags │ │ │ │ ├── delete-projects-feedback-tags.mdx │ │ │ │ ├── get-projects-feedback-tags.mdx │ │ │ │ └── post-projects-feedback-tags.mdx │ │ │ ├── feedback │ │ │ │ ├── delete-projects-feedback.mdx │ │ │ │ ├── get-projects-feedback-all.mdx │ │ │ │ ├── get-projects-feedback-upvotes.mdx │ │ │ │ ├── get-projects-feedback.mdx │ │ │ │ ├── patch-projects-feedback.mdx │ │ │ │ └── post-projects-feedback.mdx │ │ │ ├── project-config │ │ │ │ ├── get-projects-config.mdx │ │ │ │ └── patch-projects-config.mdx │ │ │ ├── project-invites │ │ │ │ ├── delete-projects-invites.mdx │ │ │ │ ├── get-projects-invites.mdx │ │ │ │ └── post-projects-invites.mdx │ │ │ ├── project-members │ │ │ │ └── get-projects-members.mdx │ │ │ ├── project │ │ │ │ ├── delete-projects.mdx │ │ │ │ ├── get-projects.mdx │ │ │ │ └── patch-projects.mdx │ │ │ └── public │ │ │ │ ├── get--atom.mdx │ │ │ │ └── get--changelogs.mdx │ │ └── introduction.mdx │ ├── development │ │ ├── configuration.mdx │ │ └── installation.mdx │ ├── images │ │ └── hero-dark.svg │ ├── integrations │ │ └── sso.mdx │ ├── introduction.mdx │ ├── logo │ │ ├── dark.svg │ │ ├── favicon.svg │ │ └── light.svg │ ├── mint.json │ └── self-hosting │ │ ├── docker.mdx │ │ └── vercel.mdx └── web │ ├── .eslintrc.js │ ├── Dockerfile │ ├── app │ ├── [project] │ │ ├── changelog │ │ │ ├── [id] │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── loading.tsx │ │ │ ├── page.tsx │ │ │ └── unsubscribe │ │ │ │ └── page.tsx │ │ ├── feedback │ │ │ ├── [id] │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── api │ │ └── v1 │ │ │ ├── [slug] │ │ │ ├── atom │ │ │ │ └── route.ts │ │ │ ├── changelogs │ │ │ │ ├── route.ts │ │ │ │ └── subscribers │ │ │ │ │ └── route.ts │ │ │ ├── sso │ │ │ │ └── route.ts │ │ │ └── views │ │ │ │ └── route.ts │ │ │ ├── profile │ │ │ ├── notifications │ │ │ │ ├── [notificationId] │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ │ ├── projects │ │ │ ├── [slug] │ │ │ │ ├── analytics │ │ │ │ │ └── route.ts │ │ │ │ ├── api-keys │ │ │ │ │ ├── [id] │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ ├── changelogs │ │ │ │ │ ├── [id] │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── route.ts │ │ │ │ │ └── subscribers │ │ │ │ │ │ └── route.ts │ │ │ │ ├── config │ │ │ │ │ ├── domain │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── integrations │ │ │ │ │ │ ├── discord │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ ├── slack │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── sso │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ ├── feedback │ │ │ │ │ ├── [feedbackId] │ │ │ │ │ │ ├── comments │ │ │ │ │ │ │ ├── [commentId] │ │ │ │ │ │ │ │ ├── route.ts │ │ │ │ │ │ │ │ └── upvote │ │ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ ├── route.ts │ │ │ │ │ │ └── upvotes │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── export │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── route.ts │ │ │ │ │ └── tags │ │ │ │ │ │ ├── [name] │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── route.ts │ │ │ │ ├── invites │ │ │ │ │ ├── [inviteId] │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ ├── members │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ └── waitlist │ │ │ └── route.ts │ ├── auth │ │ └── callback │ │ │ └── route.ts │ ├── dash │ │ ├── (auth) │ │ │ ├── login │ │ │ │ └── page.tsx │ │ │ └── signup │ │ │ │ └── page.tsx │ │ ├── [slug] │ │ │ ├── analytics │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── changelog │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── feedback │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── settings │ │ │ │ ├── general │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── hub │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── integrations │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── team │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ ├── invite │ │ │ └── [inviteId] │ │ │ │ └── page.tsx │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── home │ │ ├── (links) │ │ │ ├── demo │ │ │ │ └── page.tsx │ │ │ ├── deploy-vercel │ │ │ │ └── page.tsx │ │ │ ├── discord │ │ │ │ └── page.tsx │ │ │ ├── docs │ │ │ │ └── page.tsx │ │ │ ├── github │ │ │ │ └── page.tsx │ │ │ └── twitter │ │ │ │ └── page.tsx │ │ └── (pages) │ │ │ ├── deploy │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ ├── layout.tsx │ └── robots.ts │ ├── components │ ├── dashboard │ │ ├── analytics │ │ │ ├── bar-list.tsx │ │ │ ├── chart-cards.tsx │ │ │ └── line-chart.tsx │ │ ├── changelogs │ │ │ ├── api-sheet.tsx │ │ │ ├── changelog-list.tsx │ │ │ ├── content-editor.tsx │ │ │ ├── date-picker.tsx │ │ │ └── image-upload.tsx │ │ ├── feedback │ │ │ ├── feedback-table.tsx │ │ │ ├── header-buttons.tsx │ │ │ ├── status-combobox.tsx │ │ │ └── tag-combobox.tsx │ │ ├── modals │ │ │ ├── add-api-key-modal.tsx │ │ │ ├── add-custom-theme-modal.tsx │ │ │ ├── add-edit-changelog-modal.tsx │ │ │ ├── add-member-modal.tsx │ │ │ ├── add-project-modal.tsx │ │ │ ├── add-sso-modal.tsx │ │ │ ├── add-tag-modal.tsx │ │ │ ├── add-waitlist-modal.tsx │ │ │ ├── connect-discord-modal.tsx │ │ │ ├── connect-slack-modal.tsx │ │ │ ├── edit-profile-modal.tsx │ │ │ ├── send-feedback-modal.tsx │ │ │ └── view-feedback-modal.tsx │ │ └── settings │ │ │ ├── category-tabs.tsx │ │ │ ├── general-cards.tsx │ │ │ ├── hub-cards.tsx │ │ │ ├── integration-cards.tsx │ │ │ └── team-table.tsx │ ├── home │ │ ├── changelog-section.tsx │ │ ├── dashboard-section.tsx │ │ ├── feedback-section.tsx │ │ ├── footer.tsx │ │ ├── header.tsx │ │ ├── home-content.tsx │ │ └── spotlight-card.tsx │ ├── hub │ │ ├── analytics-wrapper.tsx │ │ ├── feedback │ │ │ ├── button-header.tsx │ │ │ ├── comments │ │ │ │ ├── comment.tsx │ │ │ │ ├── comments-list.tsx │ │ │ │ └── sort-combobox.tsx │ │ │ └── feedback-list.tsx │ │ ├── modals │ │ │ ├── create-post-modal.tsx │ │ │ ├── login-signup-modal.tsx │ │ │ └── subscribe-email-modal.tsx │ │ ├── nav-bar.tsx │ │ └── theme-wrapper.tsx │ ├── layout │ │ ├── accept-invite-form.tsx │ │ ├── inbox-popover.tsx │ │ ├── nav-bar-mobile.tsx │ │ ├── nav-tabs.tsx │ │ ├── onboarding.tsx │ │ ├── project-dropdown.tsx │ │ ├── sidebar.tsx │ │ ├── theme-button.tsx │ │ ├── title-provider.tsx │ │ └── unsubscribe-card.tsx │ ├── logo-provider.tsx │ ├── shared │ │ ├── file-drop.tsx │ │ ├── icons │ │ │ ├── analytics-dark.json │ │ │ ├── analytics-light.json │ │ │ ├── calendar-dark.json │ │ │ ├── calendar-light.json │ │ │ ├── chat-dark.json │ │ │ ├── chat-light.json │ │ │ ├── code-dark.json │ │ │ ├── code-light.json │ │ │ ├── feedback-dark.json │ │ │ ├── feedback-light.json │ │ │ ├── icons-animated.ts │ │ │ ├── icons-static.tsx │ │ │ ├── logout-dark.json │ │ │ ├── logout-light.json │ │ │ ├── profile-dark.json │ │ │ ├── profile-light.json │ │ │ ├── search-dark.json │ │ │ ├── search-light.json │ │ │ ├── settings-dark.json │ │ │ ├── settings-light.json │ │ │ ├── tag-label-dark.json │ │ │ └── tag-label-light.json │ │ ├── lottie-player.tsx │ │ ├── placeholder.css │ │ ├── tiptap-editor.tsx │ │ ├── tooltip-label.tsx │ │ ├── tooltip.tsx │ │ └── user-dropdown.tsx │ ├── theme-provider.tsx │ └── user-auth-form.tsx │ ├── emails │ ├── changelog-email.tsx │ ├── index.ts │ └── project-invite.tsx │ ├── lib │ ├── api │ │ ├── changelogs.ts │ │ ├── comments.ts │ │ ├── feedback.ts │ │ ├── integrations.ts │ │ ├── invites.ts │ │ ├── projects.ts │ │ ├── public.ts │ │ └── user.ts │ ├── auth.ts │ ├── constants.ts │ ├── hooks │ │ ├── use-create-query.ts │ │ └── use-scroll.ts │ ├── supabase.ts │ ├── tinybird.ts │ ├── types.ts │ └── utils.ts │ ├── middleware.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── icon-192x192.png │ ├── icon-256x256.png │ ├── icon-384x384.png │ ├── icon-512x512.png │ ├── manifest.json │ └── og-image.png │ ├── tailwind.config.js │ └── tsconfig.json ├── package.json ├── packages ├── client │ ├── .gitignore │ ├── README.md │ ├── lib │ │ ├── fetch.ts │ │ └── types.ts │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── eslint-config-custom │ ├── library.js │ ├── next.js │ ├── package.json │ ├── react-internal.js │ └── rules.js ├── tailwind-config │ ├── package.json │ └── tailwind.config.js ├── tinybird │ ├── datasources │ │ └── click_events.datasource │ └── endpoints │ │ ├── clicks.pipe │ │ ├── timeseries.pipe │ │ ├── top_changelogs.pipe │ │ ├── top_feedback.pipe │ │ └── visitors.pipe ├── tsconfig │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ui │ ├── .eslintrc.js │ ├── components.json │ ├── components │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── avatar.tsx │ │ ├── background │ │ └── background.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── radio-group.tsx │ │ ├── responsive-dialog.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── skeleton.tsx │ │ ├── switch.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx │ ├── declarations.d.ts │ ├── lib │ ├── hooks │ │ └── use-media-query.ts │ └── utils.ts │ ├── package.json │ ├── styles │ ├── Satoshi-Variable.woff2 │ ├── fonts.ts │ └── globals.css │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── supabase ├── .gitignore ├── config.toml └── migrations │ ├── 20231126133700_db_auth_schema.sql │ ├── 20231126133809_storage_schema.sql │ ├── 20231130182016_api_key_system.sql │ ├── 20240113153829_custom_theming.sql │ ├── 20240118215535_notification_system.sql │ └── 20240130194400_changelog_subscribers.sql └── turbo.json /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | #! Supabase Project URL & Anon Key 2 | # https://app.supabase.com/project/_/settings/api 3 | NEXT_PUBLIC_SUPABASE_URL=your-project-url 4 | NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key 5 | SUPABASE_SERVICE_ROLE_KEY=your-service-key # Only required for the single sign-on integration 6 | 7 | #! Root domain of your site 8 | # Example: feedbase.app ( without https:// and trailing slash ) 9 | NEXT_PUBLIC_ROOT_DOMAIN=your-root-domain 10 | 11 | #? Subdomain hosting 12 | SUBDOMAIN_HOSTING=false # Set to true if you are hosting on a subdomain of your root domain (e.g. feedbase.domain.com) 13 | DASHBOARD_SUBDOMAIN=feedbase # The subdomain on which you are hosting (e.g. feedbase.domain.com -> feedbase) 14 | CUSTOM_DOMAIN_WHITELIST=feedback.domain.com,feedback2.domain.com # A comma separated list of custom domains using the same root domain you are hosting on 15 | 16 | #? Resend Api Key 17 | # Only required for Email Notifications & Team Invites, can be left blank if not using these features 18 | # https://resend.com/docs/dashboard/api-keys/introduction 19 | RESEND_API_KEY=your-resend-api-key 20 | 21 | #? Github OAuth (Local development only) 22 | # https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app 23 | GITHUB_CLIENT_ID=your-client-id 24 | GITHUB_SECRET=your-github-secret 25 | 26 | #? Vercel 27 | # Only required for custom domain support, can be left blank if not using this feature 28 | # https://vercel.com/docs/custom-domains 29 | VERCEL_PROJECT_ID=your-project-id 30 | VERCEL_TEAM_ID=your-team-id # Only required if you're project is part of a team and not a personal account 31 | VERCEL_AUTH_TOKEN=your-auth-token 32 | 33 | #? Tinybird 34 | # Only required if you want to use Tinybird for hub data analytics 35 | TINYBIRD_API_URL=https://api.eu-central-1.aws.tinybird.co # Make sure to use the correct region 36 | TINYBIRD_API_KEY=your-api-key 37 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | // This tells ESLint to load the config from the package `eslint-config-custom` 4 | extends: ['custom/library'], 5 | settings: { 6 | next: { 7 | rootDir: ['apps/*/', 'packages/*/'], 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | branches: [ "main" ] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Check out the repo 16 | uses: actions/checkout@v3 17 | 18 | - name: Log in to Docker Hub 19 | uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a 20 | with: 21 | username: ${{ secrets.DOCKER_USERNAME }} 22 | password: ${{ secrets.DOCKER_PASSWORD }} 23 | 24 | - name: Extract metadata (tags, labels) for Docker 25 | id: meta 26 | uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 27 | with: 28 | images: chroxify/feedbase 29 | 30 | - name: Build and push Docker image 31 | uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 32 | with: 33 | context: . 34 | file: ./apps/web/Dockerfile 35 | push: true 36 | tags: ${{ steps.meta.outputs.tags }} 37 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.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 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # turbo 39 | .turbo 40 | 41 | # Auto Generated PWA files 42 | **/public/sw.js 43 | **/public/workbox-*.js 44 | **/public/worker-*.js 45 | **/public/sw.js.map 46 | **/public/workbox-*.js.map 47 | **/public/worker-*.js.map.tinyb 48 | 49 | # tinybird 50 | .venv 51 | .tinyb -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm run lint --fix && pnpm run ts && pnpm lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .gitignore 4 | .DS_Store -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: true, 3 | bracketSameLine: true, 4 | singleQuote: true, 5 | jsxSingleQuote: true, 6 | trailingComma: 'es5', 7 | semi: true, 8 | printWidth: 110, 9 | arrowParens: 'always', 10 | importOrder: [ 11 | '^(react/(.*)$)|^(react$)', 12 | '^(next/(.*)$)|^(next$)', 13 | '', 14 | '^types$', 15 | '^@/types/(.*)$', 16 | '^@/config/(.*)$', 17 | '^@/lib/(.*)$', 18 | '^@/components/(.*)$', 19 | '^@/styles/(.*)$', 20 | '^@/app/(.*)$', 21 | '^[./]', 22 | ], 23 | plugins: [ 24 | /** 25 | * **NOTE** tailwind plugin must come last! 26 | * @see https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins 27 | */ 28 | '@ianvs/prettier-plugin-sort-imports', 29 | 'prettier-plugin-tailwindcss', 30 | ], 31 | }; 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

The Open-Source Product Management Tool 3 |

4 | 5 |

6 | Learn more » 7 |
8 |
9 | Introduction 10 | · 11 | Tech Stack 12 | · 13 | Deploy Your Own 14 | · 15 | Roadmap 16 | · 17 | Contributing 18 |

19 |

20 | 21 | ## Introduction 22 | 23 | Feedbase is an open-source product management tool that helps products to enhance customer relationships through its robust features. It enables efficient capturing, prioritization, and resolution of customer issues, while also providing valuable insights into your product's status. 24 | 25 | ## Tech Stack 26 | 27 | - [Next.js](https://nextjs.org/) – Framework 28 | - [Typescript](https://www.typescriptlang.org/) – Language 29 | - [Tailwind](https://tailwindcss.com/) – CSS 30 | - [Shadcn/ui](https://ui.shadcn.com/) – Component Library 31 | - [Supabase](https://supabase.com/) – Database & Auth 32 | - [Vercel](https://vercel.com/) – Hosting 33 | 34 | ## Deploy Your Own 35 | 36 | If you're interested in self-hosting your own Feedbase instance, check out the [documentation](https://docs.feedbase.app/self-hosting). 37 | 38 | ## Roadmap 39 | 40 | Feedbase is currently in very early stages of development. Here's what we have planned for the future: 41 | 42 | - [ ] Further customization of the public hub 43 | - [ ] Implementation of Roadmaps & Feature Requests 44 | - [ ] Integration with other tools like GitHub, Linear, etc. 45 | - [ ] Analytics & Insights options for the public hub 46 | - [ ] Blog & Users management 47 | 48 | and much much more, so stay tuned! 49 | 50 | ## Contributing 51 | 52 | We would love to have your help in making Feedbase better! 53 | 54 | Here's how you can contribute: 55 | - [Report a bug](https://github.com/chroxify/feedbase/issues/new?labels=bug) you found while using Feedbase 56 | - [Request a feature](https://github.com/chroxify/feedbase/issues/new?labels=enhancement) that you think will be useful 57 | - [Submit a pull request](https://github.com/chroxify/feedbase/pulls) if you want to contribute with new features or bug fixes 58 | 59 | ## License 60 | Feedbase is licensed under the [GNU Affero General Public License Version 3 (AGPLv3)](https://github.com/chroxify/feedbase/blob/main/LICENSE) . 61 | -------------------------------------------------------------------------------- /apps/docs/README.md: -------------------------------------------------------------------------------- 1 | # Mintlify Starter Kit 2 | 3 | Click on `Use this template` to copy the Mintlify starter kit. The starter kit contains examples including 4 | 5 | - Guide pages 6 | - Navigation 7 | - Customizations 8 | - API Reference pages 9 | - Use of popular components 10 | 11 | ### Development 12 | 13 | Install the [Mintlify CLI](https://www.npmjs.com/package/mintlify) to preview the documentation changes locally. To install, use the following command 14 | 15 | ``` 16 | npm i -g mintlify 17 | ``` 18 | 19 | Run the following command at the root of your documentation (where mint.json is) 20 | 21 | ``` 22 | mintlify dev 23 | ``` 24 | 25 | ### Publishing Changes 26 | 27 | Install our Github App to autopropagate changes from youre repo to your deployment. Changes will be deployed to production automatically after pushing to the default branch. Find the link to install on your dashboard. 28 | 29 | #### Troubleshooting 30 | 31 | - Mintlify dev isn't running - Run `mintlify install` it'll re-install dependencies. 32 | - Page loads as a 404 - Make sure you are running in a folder with `mint.json` 33 | -------------------------------------------------------------------------------- /apps/docs/_snippets/snippet-example.mdx: -------------------------------------------------------------------------------- 1 | ## My Snippet 2 | 3 | This is an example of a reusable snippet 4 | -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/changelog/delete-projects-changelogs.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Delete a Changelog" 3 | openapi: delete /projects/{projectSlug}/changelogs/{changelogId} 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/changelog/get-projects-changelogs.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: List Changelogs 3 | openapi: get /projects/{projectSlug}/changelogs 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/changelog/patch-projects-changelogs.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Update a Changelog" 3 | openapi: put /projects/{projectSlug}/changelogs/{changelogId} 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/changelog/post-projects-changelogs.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Create a Changelog" 3 | openapi: post /projects/{projectSlug}/changelogs 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/feedback-comments/delete-projects-feedback-comments.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Delete a Comment 3 | openapi: delete /projects/{projectSlug}/feedback/{feedbackId}/comments/{commentId} 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/feedback-comments/get-projects-feedback-comments.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: List Feedback Comments 3 | openapi: get /projects/{projectSlug}/feedback/{feedbackId}/comments 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/feedback-tags/delete-projects-feedback-tags.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Delete a Feedback Tag 3 | openapi: delete /projects/{projectSlug}/feedback/tags/{tagName} 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/feedback-tags/get-projects-feedback-tags.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: List Feedback Tags 3 | openapi: get /projects/{projectSlug}/feedback/tags 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/feedback-tags/post-projects-feedback-tags.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Create a Feedback Tag 3 | openapi: post /projects/{projectSlug}/feedback/tags 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/feedback/delete-projects-feedback.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Delete a Feedback 3 | openapi: delete /projects/{projectSlug}/feedback/{feedbackId} 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/feedback/get-projects-feedback-all.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: List Project Feedback 3 | openapi: get /projects/{projectSlug}/feedback 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/feedback/get-projects-feedback-upvotes.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: List Feedback Upvoters 3 | openapi: get /projects/{projectSlug}/feedback/{feedbackId}/upvotes 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/feedback/get-projects-feedback.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Retrieve a Feedback 3 | openapi: get /projects/{projectSlug}/feedback/{feedbackId} 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/feedback/patch-projects-feedback.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Update a Feedback 3 | openapi: patch /projects/{projectSlug}/feedback/{feedbackId} 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/feedback/post-projects-feedback.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Create a Feedback 3 | openapi: post /projects/{projectSlug}/feedback 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/project-config/get-projects-config.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Retrieve a Project Config 3 | openapi: get /projects/{projectSlug}/config 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/project-config/patch-projects-config.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Update a Project Config 3 | openapi: patch /projects/{projectSlug}/config 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/project-invites/delete-projects-invites.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Delete a Project Invite 3 | openapi: delete /projects/{projectSlug}/invites/{inviteId} 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/project-invites/get-projects-invites.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "List Project Invites" 3 | openapi: get /projects/{projectSlug}/invites 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/project-invites/post-projects-invites.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Create a Project Invite" 3 | openapi: post /projects/{projectSlug}/invites 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/project-members/get-projects-members.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: List Project Members 3 | openapi: get /projects/{projectSlug}/members 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/project/delete-projects.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Delete a Project 3 | openapi: delete /projects/{projectSlug} 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/project/get-projects.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Retrieve a Project 3 | openapi: get /projects/{projectSlug} 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/project/patch-projects.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Update a Project 3 | openapi: patch /projects/{projectSlug} 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/public/get--atom.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Retrieve Project Atom Feed 3 | openapi: get /{projectSlug}/atom 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/endpoint/public/get--changelogs.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: List Project Changelogs 3 | openapi: get /{projectSlug}/changelogs 4 | --- -------------------------------------------------------------------------------- /apps/docs/api-reference/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Introduction' 3 | description: 'Welcome to the API documentation for Feedbase' 4 | --- 5 | 6 | ## Welcome 7 | 8 | Welcome to the API documentation for Feedbase. There are two different API's available: 9 | 10 | 11 | The public API is used to access public project data. This mainly includes data that is available on your public hub. 12 | 13 | 14 | 15 | The project API is used to access private project data. There are two different scopes for the project API: 16 | - Public scope should be used for our [Typescript SDK]('/sdk-reference') 17 | - Full Access scope is used to access project data and manage project settings 18 | 19 | 20 | 21 | 22 | ## Create a new API key 23 | 24 | To access the [Project API](/api-reference/endpoint/project/), you need to provide an API key. You can create a new API key with the necessary scopes by clicking below. 25 | 26 | 31 | Create a new API key to access the API. 32 | 33 | 34 | ## Authentication 35 | 36 | All project endpoints are protected and require a valid API key. You can provide the API key in the `Authorization` header as a Bearer token. 37 | 38 | ```typescript 39 | const response = await fetch('https://api.feedbase.app/v1', { 40 | method: 'GET', 41 | headers: { 42 | 'Authorization': 'Bearer fb_xxxxxxxxxxxxxxxxxxxx', 43 | }, 44 | }) 45 | ``` -------------------------------------------------------------------------------- /apps/docs/development/configuration.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Configuration' 3 | description: 'Setting up all necessary environment variables to run the project.' 4 | --- 5 | 6 | ## Environment Variables 7 | 8 | That that we have our project cloned, and database set up, we need to set up our environment variables. To do this, we will need to create a `.env` file in the root of our project. 9 | 10 | ### Supabase 11 | 12 | To retrieve the Supabase variables, we can run the following command in our terminal: 13 | 14 | ```bash 15 | supabase status 16 | ``` 17 | 18 | Supabase must be running in order for this command to work. 19 | 20 | This will return a list of urls and keys about our project. We will need to add the following variables to our `.env` file: 21 | 22 | ```bash 23 | NEXT_PUBLIC_SUPABASE_URL= # API URL 24 | NEXT_PUBLIC_SUPABASE_ANON_KEY= # anon key 25 | ``` 26 | 27 | ### Github OAuth 28 | 29 | For local authentication we will be using GitHub. To set this up, we will need to create a GitHub OAuth app. Go to your GitHub settings and click on "Developer Settings" in the sidebar. Then click on "OAuth Apps" and "New OAuth App". Fill out the form with the following information: 30 | 31 | - Application Name: `Development` 32 | - Homepage URL: `http://localhost:3000` 33 | - Authorization callback URL: `http://127.0.0.1:54321/auth/v1/callback` 34 | 35 | Once you have created the app, you will be given a Client ID and Client Secret. Add these to your `.env` file: 36 | 37 | ```bash 38 | GITHUB_CLIENT_ID= # client id 39 | GITHUB_CLIENT_SECRET= # client secret 40 | ``` 41 | 42 | ### Resend 43 | 44 | This is optional. If you do not need to send emails, you can skip this step. 45 | 46 | To send emails, we will be using [Resend](https://resend.com/). Create an account and add your API key to your `.env` file: 47 | 48 | ```bash 49 | RESEND_API_KEY= # api key 50 | ``` 51 | 52 | ### Other 53 | 54 | We will also need to add one other variables to our `.env` file: 55 | 56 | ```bash 57 | NEXT_PUBLIC_ROOT_DOMAIN= # root domain (e.g. localhost:3000) 58 | ``` 59 | 60 | ## Next Steps 61 | 62 | Now that we have everything set up, we are ready to start the development server and begin building. 63 | 64 | ```bash 65 | pnpm dev 66 | ``` -------------------------------------------------------------------------------- /apps/docs/development/installation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | description: 'Learn how to install Feedbase on your local machine' 4 | --- 5 | 6 | ## System Requirements 7 | 8 | - [Node.js 18.0.0](https://nodejs.org/en/download/) or higher 9 | - [pnpm 8.8.0](https://pnpm.io/installation) or higher 10 | 11 | ## Clone the repository 12 | 13 | 14 | ```bash 15 | git clone https://github.com/chroxify/feedbase.git && cd feedbase 16 | ``` 17 | 18 | 19 | ```bash 20 | pnpm install 21 | ``` 22 | 23 | 24 | 25 | ## Database Setup 26 | Feedbase is using [Supabase](https://supabase.io/) as its database. For local development, you can use the Supabase CLI to start a local Postgres database. 27 | 28 | 29 | 30 | Follow the instructions [here](https://github.com/supabase/cli?tab=readme-ov-file#install-the-cli) to install the Supabase CLI for your operating system. 31 | 32 | 33 | ```bash 34 | supabase start 35 | ``` 36 | 37 | 38 | Now you can visit the local Supabase dashboard at [http://localhost:54323](http://localhost:54323) to make sure all migrations have been applied correctly. 39 | 40 | -------------------------------------------------------------------------------- /apps/docs/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Welcome & Overview 3 | description: 'Welcome to Feedbase! This page will give you a quick overview of what Feedbase is and how to get started.' 4 | --- 5 | 6 | Hero Dark 10 | 11 | ## What is Feedbase? 12 | 13 | Feedbase is an open-source feedback collection and product update solution that allows you to collect feedback from your users and keep them updated on your product's progress. 14 | 15 | ## Quickstart 16 | 17 | 18 | 23 | Use our cloud hosted solution to get up and running in minutes 24 | 25 | 30 | Host Feedbase on your own servers or cloud provider for full control 31 | 32 | 37 | Develop and contribute to Feedbase locally with hot-reloading 38 | 39 | 44 | Learn more about Feedbase's API and how to use it 45 | 46 | 47 | -------------------------------------------------------------------------------- /apps/docs/logo/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['custom/next'], 4 | globals: { 5 | Messages: 'readonly', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /apps/web/Dockerfile: -------------------------------------------------------------------------------- 1 | # src Dockerfile: https://github.com/vercel/turbo/blob/main/examples/with-docker/apps/web/Dockerfile 2 | FROM node:18-alpine AS alpine 3 | 4 | # setup pnpm on the alpine base 5 | FROM alpine as base 6 | ENV PNPM_HOME="/pnpm" 7 | ENV PATH="$PNPM_HOME:$PATH" 8 | RUN corepack enable 9 | RUN pnpm install turbo --global 10 | RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/* 11 | 12 | FROM base AS builder 13 | RUN apk add --no-cache libc6-compat 14 | RUN apk update 15 | # Set working directory 16 | WORKDIR /app 17 | COPY . . 18 | RUN turbo prune --scope=web --docker 19 | 20 | # Add lockfile and package.json's of isolated subworkspace 21 | FROM base AS installer 22 | RUN apk add --no-cache libc6-compat 23 | RUN apk update 24 | WORKDIR /app 25 | 26 | # First install the dependencies (as they change less often) 27 | COPY .gitignore .gitignore 28 | COPY --from=builder /app/out/json/ . 29 | COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml 30 | COPY --from=builder /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml 31 | RUN pnpm install 32 | 33 | # Build the project 34 | COPY --from=builder /app/out/full/ . 35 | COPY turbo.json turbo.json 36 | RUN turbo run build --filter=web 37 | 38 | # use alpine as the thinest image 39 | FROM alpine AS runner 40 | WORKDIR /app 41 | 42 | # Don't run production as root 43 | RUN addgroup --system --gid 1001 nodejs 44 | RUN adduser --system --uid 1001 nextjs 45 | USER nextjs 46 | 47 | COPY --from=installer /app/apps/web/next.config.js . 48 | COPY --from=installer /app/apps/web/package.json . 49 | 50 | # Automatically leverage output traces to reduce image size 51 | # https://nextjs.org/docs/advanced-features/output-file-tracing 52 | COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/standalone ./ 53 | COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static 54 | COPY --from=installer --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public 55 | 56 | CMD node apps/web/server.js -------------------------------------------------------------------------------- /apps/web/app/[project]/changelog/[id]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Separator } from '@ui/components/ui/separator'; 2 | import { Skeleton } from '@ui/components/ui/skeleton'; 3 | 4 | export default function ChangelogPageLoading() { 5 | return ( 6 |
9 | {/* Back Button */} 10 |
11 |
12 | 13 |
14 |
15 | 16 | {/* Content */} 17 |
18 | {/* Title */} 19 |

20 | 21 |

22 | 23 | {/* Image */} 24 | 25 | 26 | {/* Author & Share */} 27 |
28 |
29 | {/* Author Avatar */} 30 | 31 | 32 | {/* Name & Date */} 33 |
34 |

35 | 36 |

37 |

38 | 39 |

40 |
41 |
42 | 43 | {/* Share */} 44 |
45 | 46 |
47 |
48 | 49 | {/* Content as html */} 50 |
51 | {[1, 2, 3, 4, 5, 6].map((index) => ( 52 | 53 | ))} 54 |
55 | 56 | {/* Separator */} 57 | 58 | 59 | {/* Next & Previous */} 60 |
61 | {/* Previous */} 62 |
63 | 64 |
65 | 66 | {/* Next */} 67 |
68 | 69 |
70 |
71 |
72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /apps/web/app/[project]/changelog/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Separator } from '@ui/components/ui/separator'; 2 | import { Skeleton } from '@ui/components/ui/skeleton'; 3 | 4 | export default function ChangelogLoading() { 5 | return ( 6 |
7 |
8 |
9 |

10 | 11 |

12 |

13 | 14 |

15 |
16 | 17 | 18 |
19 |
20 |
21 | 22 | 23 |
24 | {[1, 2, 3].map((index) => ( 25 |
28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 |
41 | 42 |
43 | 44 |
45 |
46 | ))} 47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /apps/web/app/[project]/changelog/unsubscribe/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound, redirect } from 'next/navigation'; 2 | import { getProjectBySlug } from '@/lib/api/projects'; 3 | import UnsubscribeChangelogCard from '@/components/layout/unsubscribe-card'; 4 | 5 | export default async function ChangelogUnsubscribe({ 6 | params, 7 | searchParams, 8 | }: { 9 | params: { project: string }; 10 | searchParams: { subId: string }; 11 | }) { 12 | if (!searchParams.subId) { 13 | redirect('/'); 14 | } 15 | 16 | // Check if subId is in uuid format 17 | const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i; 18 | 19 | if (!uuidRegex.test(searchParams.subId)) { 20 | redirect('/'); 21 | } 22 | 23 | // Get project 24 | const { data: project, error } = await getProjectBySlug(params.project, 'server', true, false); 25 | 26 | // If project is undefined redirects to 404 27 | if (error?.status === 404 || !project) { 28 | notFound(); 29 | } 30 | 31 | return ( 32 |
33 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /apps/web/app/[project]/feedback/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Separator } from '@ui/components/ui/separator'; 2 | import { Skeleton } from '@ui/components/ui/skeleton'; 3 | import FeedbackHeader from '@/components/hub/feedback/button-header'; 4 | 5 | export default function FeedbackLoading() { 6 | return ( 7 |
8 |
9 |
10 |

Feedback

11 |

12 | Have a suggestion or found a bug? Let us know! 13 |

14 |
15 |
16 | 17 | {/* Separator */} 18 | 19 | 20 | {/* Content */} 21 |
22 | {' '} 23 | {/* Provide placeholder values */} 24 | {/* Main */} 25 |
26 | {[1, 2, 3, 4, 5].map((index) => ( 27 | 28 | ))} 29 |
30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /apps/web/app/[project]/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | export default async function Hub() { 4 | // Redirect to changelog 5 | // This page doesn't really get called as this is catched in layout.tsx but still needed to cause 404 6 | redirect(`/feedback`); 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/[slug]/atom/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { getProjectBySlug } from '@/lib/api/projects'; 3 | import { getPublicProjectChangelogs } from '@/lib/api/public'; 4 | 5 | /* 6 | Generate atom feed for project changelog 7 | */ 8 | export async function GET(req: Request, context: { params: { slug: string } }) { 9 | // Get project data 10 | const { data: project, error } = await getProjectBySlug(context.params.slug, 'route', true, false); 11 | 12 | // If any errors thrown, return error 13 | if (error) { 14 | return NextResponse.json({ error: error.message }, { status: error.status }); 15 | } 16 | 17 | const { data: changelogs, error: changelogError } = await getPublicProjectChangelogs( 18 | context.params.slug, 19 | 'route', 20 | true, 21 | false 22 | ); 23 | 24 | // If any errors thrown, return error 25 | if (changelogError) { 26 | return NextResponse.json({ error: changelogError.message }, { status: changelogError.status }); 27 | } 28 | 29 | // Return atom formatted changelogs 30 | return new Response( 31 | ` 32 | 33 | ${project.name} Changelog 34 | ${project.name}'s Changelog 35 | 36 | 37 | ${changelogs[0].publish_date} 38 | ${project.id}${changelogs 39 | .map((post) => { 40 | return ` 41 | 42 | ${post.id} 43 | ${post.title} 44 | 45 | ${post.publish_date} 46 | ${post.author.full_name} 47 | `; 48 | }) 49 | .join('')} 50 | `, 51 | { status: 200, headers: { 'Content-Type': 'application/atom+xml; charset=utf-8' } } 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/[slug]/changelogs/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { getPublicProjectChangelogs } from '@/lib/api/public'; 3 | 4 | /* 5 | Get project changelogs 6 | GET /api/v1/projects/[slug]/changelogs 7 | */ 8 | export async function GET(req: Request, context: { params: { slug: string } }) { 9 | const { data: changelogs, error } = await getPublicProjectChangelogs( 10 | context.params.slug, 11 | 'route', 12 | true, 13 | false 14 | ); 15 | 16 | // If any errors thrown, return error 17 | if (error) { 18 | return NextResponse.json({ error: error.message }, { status: error.status }); 19 | } 20 | 21 | // Return changelogs 22 | return NextResponse.json(changelogs, { status: 200 }); 23 | } 24 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/[slug]/changelogs/subscribers/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { subscribeToProjectChangelogs, unsubscribeFromProjectChangelogs } from '@/lib/api/public'; 3 | 4 | /* 5 | Subscribe to project changelogs 6 | POST /api/v1/:slug/changelogs/subscribers 7 | { 8 | email: string, 9 | } 10 | */ 11 | export async function POST(req: Request, context: { params: { slug: string } }) { 12 | const { email } = await req.json(); 13 | 14 | // Check if email is provided 15 | if (!email) { 16 | return NextResponse.json({ error: 'email is required.' }, { status: 400 }); 17 | } 18 | 19 | // Subscribe to project changelogs 20 | const { data: subscriber, error } = await subscribeToProjectChangelogs(context.params.slug, email); 21 | 22 | // If any errors thrown, return error 23 | if (error) { 24 | return NextResponse.json({ error: error.message }, { status: error.status }); 25 | } 26 | 27 | // Return subscriber 28 | return NextResponse.json(subscriber, { status: 200 }); 29 | } 30 | 31 | /* 32 | Unsubscribe from project changelogs 33 | DELETE /api/v1/:slug/changelogs/subscribers 34 | { 35 | subId: string, 36 | } 37 | */ 38 | export async function DELETE(req: Request, context: { params: { slug: string } }) { 39 | const { subId } = await req.json(); 40 | 41 | // Check if subId is provided 42 | if (!subId) { 43 | return NextResponse.json({ error: 'subId is required.' }, { status: 400 }); 44 | } 45 | 46 | // Unsubscribe from project changelogs 47 | const { error } = await unsubscribeFromProjectChangelogs(context.params.slug, subId); 48 | 49 | // If any errors thrown, return error 50 | if (error) { 51 | return NextResponse.json({ error: error.message }, { status: error.status }); 52 | } 53 | 54 | // Return success 55 | return NextResponse.json({ success: true }, { status: 200 }); 56 | } 57 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/[slug]/views/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { recordClick } from '@/lib/tinybird'; 3 | 4 | /* 5 | Record page view 6 | POST /api/v1/[project]/views 7 | { 8 | "feedbackId": "string", 9 | "changelogId": "string", 10 | } 11 | */ 12 | export async function POST(req: NextRequest, context: { params: { slug: string } }) { 13 | const { feedbackId, changelogId } = await req.json(); 14 | 15 | // Check for Tinybird env vars 16 | if (!process.env.TINYBIRD_API_URL || !process.env.TINYBIRD_API_KEY) { 17 | return NextResponse.json({ error: 'Tinybird environment variables not set' }, { status: 500 }); 18 | } 19 | 20 | const data = await recordClick({ 21 | req, 22 | projectId: context.params.slug, 23 | feedbackId, 24 | changelogId, 25 | }); 26 | 27 | return NextResponse.json({ data }, { status: 200 }); 28 | } 29 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/profile/notifications/[notificationId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { archiveUserNotification } from '@/lib/api/user'; 3 | 4 | /* 5 | Archive a notification 6 | PATCH /api/v1/profile/notifications/:notificationId 7 | { 8 | "archived": true 9 | } 10 | */ 11 | export async function PATCH(req: Request, context: { params: { notificationId: string } }) { 12 | // Get notification id 13 | const { archived } = await req.json(); 14 | 15 | // Archive notification 16 | const { data: notification, error } = await archiveUserNotification( 17 | 'route', 18 | context.params.notificationId, 19 | archived 20 | ); 21 | 22 | // Check for errors 23 | if (error) { 24 | return NextResponse.json({ error: error.message }, { status: error.status }); 25 | } 26 | 27 | return NextResponse.json(notification, { status: 200 }); 28 | } 29 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/profile/notifications/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { getUserNotifications } from '@/lib/api/user'; 3 | 4 | /* 5 | Get user's notifications 6 | GET /api/v1/profile/notifications 7 | */ 8 | export async function GET(req: Request) { 9 | // Get user's notifications 10 | const { data: notifications, error } = await getUserNotifications('route'); 11 | 12 | // Check for errors 13 | if (error) { 14 | return NextResponse.json({ error: error.message }, { status: error.status }); 15 | } 16 | 17 | return NextResponse.json(notifications, { status: 200 }); 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/profile/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { updateUserProfile } from '@/lib/api/user'; 3 | 4 | /* 5 | Update Profile 6 | PATCH /api/v1/profile 7 | { 8 | "full_name": string, 9 | "avatar_url": string, 10 | } 11 | */ 12 | export async function PATCH(req: Request) { 13 | const { full_name: fullName, avatar_url: avatarUrl } = await req.json(); 14 | 15 | // Validate Request Body 16 | if (!fullName && !avatarUrl) { 17 | return NextResponse.json({ error: 'full_name or avatar_url is required.' }, { status: 400 }); 18 | } 19 | 20 | // Update Profile 21 | const { data: profile, error } = await updateUserProfile('route', { 22 | full_name: fullName, 23 | avatar_url: avatarUrl, 24 | }); 25 | 26 | // Check for errors 27 | if (error) { 28 | return NextResponse.json({ error: error.message }, { status: error.status }); 29 | } 30 | 31 | return NextResponse.json(profile, { status: 200 }); 32 | } 33 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/analytics/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getProjectAnalytics } from '@/lib/api/projects'; 3 | 4 | /* 5 | Get Analytics 6 | GET /api/v1/projects/:slug/analytics 7 | */ 8 | export async function GET(req: NextRequest, context: { params: { slug: string } }) { 9 | // Check if tinybird variables are set 10 | if (!process.env.TINYBIRD_API_URL || !process.env.TINYBIRD_API_KEY) { 11 | return NextResponse.json({ error: 'Tinybird variables not set.' }, { status: 500 }); 12 | } 13 | 14 | // Get query params 15 | const start = req.nextUrl.searchParams.get('start'); 16 | const end = req.nextUrl.searchParams.get('end'); 17 | 18 | // Check if start and end are valid dates 19 | if ((start && !Date.parse(start)) || (end && !Date.parse(end))) { 20 | return NextResponse.json({ error: 'Invalid start or end date.' }, { status: 400 }); 21 | } 22 | 23 | const { data: analyticsData, error } = await getProjectAnalytics(context.params.slug, 'route'); 24 | 25 | // If any errors thrown, return error 26 | if (error) { 27 | return NextResponse.json({ error: error.message }, { status: error.status }); 28 | } 29 | 30 | // Return response 31 | return NextResponse.json(analyticsData, { status: 200 }); 32 | } 33 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/api-keys/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { deleteProjectApiKey } from '@/lib/api/projects'; 3 | 4 | /* 5 | Delete api key for a project 6 | DELETE /api/v1/projects/:slug/config/api/:token 7 | */ 8 | export async function DELETE(req: Request, context: { params: { slug: string; id: string } }) { 9 | const { error } = await deleteProjectApiKey(context.params.slug, context.params.id, 'route'); 10 | 11 | if (error) { 12 | return NextResponse.json({ error }, { status: error.status }); 13 | } 14 | 15 | return NextResponse.json({ success: true }, { status: 200 }); 16 | } 17 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/api-keys/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { createProjectApiKey, getProjectApiKeys } from '@/lib/api/projects'; 3 | 4 | /* 5 | Get all API keys for a project 6 | GET /api/v1/projects/:slug/config/api 7 | */ 8 | export async function GET(req: Request, context: { params: { slug: string } }) { 9 | // Get all api keys 10 | const { data: apiKeys, error } = await getProjectApiKeys(context.params.slug, 'route'); 11 | 12 | if (error) { 13 | return NextResponse.json(error, { status: error.status }); 14 | } 15 | 16 | return NextResponse.json(apiKeys, { status: 200 }); 17 | } 18 | 19 | /* 20 | Create a new API key for a project 21 | POST /api/v1/projects/:slug/config/api 22 | { 23 | "name": "string", 24 | "permissions": "string" 25 | } 26 | */ 27 | export async function POST(req: Request, context: { params: { slug: string } }) { 28 | const { name, permission } = (await req.json()) as { name: string; permission: string }; 29 | 30 | // Validate input 31 | if (!name || !permission) { 32 | return NextResponse.json({ error: 'name and permission are required' }, { status: 400 }); 33 | } 34 | 35 | // Create api key 36 | const { data: apiKey, error } = await createProjectApiKey( 37 | context.params.slug, 38 | { name, permission }, 39 | 'route' 40 | ); 41 | 42 | if (error) { 43 | return NextResponse.json({ error: error.message }, { status: error.status }); 44 | } 45 | 46 | return NextResponse.json(apiKey, { status: 200 }); 47 | } 48 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/changelogs/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { deleteChangelog, updateChangelog } from '@/lib/api/changelogs'; 3 | 4 | export const runtime = 'edge'; 5 | 6 | /* 7 | Update project changelog 8 | PUT /api/v1/projects/[slug]/changelogs/[id] 9 | { 10 | title: string; 11 | summary: string; 12 | content: string; 13 | image?: string; 14 | publish_date?: Date; 15 | published: boolean; 16 | } 17 | */ 18 | export async function PUT(req: Request, context: { params: { slug: string; id: string } }) { 19 | const { title, summary, content, image, publishDate, published } = await req.json(); 20 | 21 | const { data: changelog, error } = await updateChangelog( 22 | context.params.id, 23 | context.params.slug, 24 | { title, summary, content, image, publish_date: publishDate, published }, 25 | 'route' 26 | ); 27 | 28 | // If any errors thrown, return error 29 | if (error) { 30 | return NextResponse.json({ error: error.message }, { status: error.status }); 31 | } 32 | 33 | // Return changelog 34 | return NextResponse.json(changelog, { status: 200 }); 35 | } 36 | 37 | /* 38 | Delete project changelog 39 | DELETE /api/v1/projects/[slug]/changelogs/[id] 40 | */ 41 | export async function DELETE(req: Request, context: { params: { slug: string; id: string } }) { 42 | const { data, error } = await deleteChangelog(context.params.id, context.params.slug, 'route'); 43 | 44 | // If any errors thrown, return error 45 | if (error) { 46 | return NextResponse.json({ error: error.message }, { status: error.status }); 47 | } 48 | 49 | // Return success 50 | return NextResponse.json(data, { status: 200 }); 51 | } 52 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/changelogs/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { createChangelog, getAllProjectChangelogs } from '@/lib/api/changelogs'; 3 | import { ChangelogProps } from '@/lib/types'; 4 | 5 | export const runtime = 'edge'; 6 | 7 | /* 8 | Create Changelog 9 | POST /api/v1/projects/[slug]/changelogs 10 | { 11 | title: string; 12 | summary: string; 13 | content: string; 14 | image?: string; 15 | publish_date?: Date; 16 | published: boolean; 17 | } 18 | */ 19 | export async function POST(req: Request, context: { params: { slug: string } }) { 20 | const { 21 | title, 22 | summary, 23 | content, 24 | image, 25 | publish_date: publishDate, 26 | published, 27 | } = (await req.json()) as ChangelogProps['Insert']; 28 | 29 | // Validate Request Body 30 | if (published) { 31 | if (!title || !summary || !content) { 32 | return NextResponse.json( 33 | { error: 'title, summary, and content are required when publishing a changelog.' }, 34 | { status: 400 } 35 | ); 36 | } 37 | } 38 | 39 | const { data: changelog, error } = await createChangelog( 40 | context.params.slug, 41 | { 42 | title: title || '', 43 | summary: summary || '', 44 | content: content || '', 45 | image: image || '', 46 | publish_date: publishDate || null, 47 | published: published || false, 48 | project_id: 'dummy-id', 49 | slug: 'dummy-slug', 50 | author_id: 'dummy-author', 51 | }, 52 | 'route' 53 | ); 54 | 55 | // If any errors thrown, return error 56 | if (error) { 57 | return NextResponse.json({ error: error.message }, { status: error.status }); 58 | } 59 | 60 | // Return changelog 61 | return NextResponse.json(changelog, { status: 200 }); 62 | } 63 | 64 | /* 65 | Get project changelogs 66 | GET /api/v1/projects/[slug]/changelogs 67 | */ 68 | export async function GET(req: Request, context: { params: { slug: string } }) { 69 | const { data: changelogs, error } = await getAllProjectChangelogs(context.params.slug, 'route', true); 70 | 71 | // If any errors thrown, return error 72 | if (error) { 73 | return NextResponse.json({ error: error.message }, { status: error.status }); 74 | } 75 | 76 | // Return changelogs 77 | return NextResponse.json(changelogs, { status: 200 }); 78 | } 79 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/changelogs/subscribers/route.ts: -------------------------------------------------------------------------------- 1 | import { getChangelogSubscribers } from '@/lib/api/changelogs'; 2 | 3 | /* 4 | Download changelog subscribers 5 | GET /api/v1/projects/:slug/changelogs/subscribers/download 6 | */ 7 | export async function GET(req: Request, context: { params: { slug: string } }) { 8 | const { data: subscribers, error } = await getChangelogSubscribers(context.params.slug, 'route'); 9 | 10 | // If any errors thrown, return error 11 | if (error) { 12 | return new Response(error.message, { status: error.status }); 13 | } 14 | 15 | // Create CSV 16 | const csv = `Email\n${subscribers 17 | .map((subscriber) => { 18 | return `${subscriber.email}\n`; 19 | }) 20 | .join('')}`; 21 | 22 | // Return CSV 23 | return new Response(csv, { 24 | headers: { 25 | 'Content-Type': 'text/csv', 26 | 'Content-Disposition': 'attachment; filename=subscribers.csv', 27 | }, 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/config/integrations/discord/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { updateProjectConfigBySlug } from '@/lib/api/projects'; 3 | 4 | /* 5 | Update Discord integration 6 | PATCH /api/v1/projects/:slug/config/integrations/discord 7 | { 8 | status: boolean, 9 | webhook: string, 10 | roleId: string, 11 | } 12 | */ 13 | export async function PATCH(req: Request, context: { params: { slug: string } }) { 14 | const { status, webhook, roleId } = (await req.json()) as { 15 | status: boolean; 16 | webhook: string; 17 | roleId: string; 18 | }; 19 | 20 | // If status is true, make sure webhook and roleId are not empty 21 | if (status && !webhook) { 22 | return NextResponse.json( 23 | { error: 'webhook is required when enabling Discord integration.' }, 24 | { status: 400 } 25 | ); 26 | } 27 | 28 | // Update project config 29 | const { data: updatedProjectConfig, error } = await updateProjectConfigBySlug( 30 | context.params.slug, 31 | { 32 | integration_discord_status: status, 33 | integration_discord_webhook: status ? webhook : null, 34 | integration_discord_role_id: status ? roleId : null, 35 | }, 36 | 'route' 37 | ); 38 | 39 | // If any errors thrown, return error 40 | if (error) { 41 | return NextResponse.json({ error: error.message }, { status: error.status }); 42 | } 43 | 44 | // Return updated project config 45 | return NextResponse.json(updatedProjectConfig, { status: 200 }); 46 | } 47 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/config/integrations/slack/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { updateProjectConfigBySlug } from '@/lib/api/projects'; 3 | 4 | /* 5 | Update Slack integration 6 | PATCH /api/v1/projects/:slug/config/integrations/slack 7 | { 8 | status: boolean, 9 | webhook: string, 10 | } 11 | */ 12 | export async function PATCH(req: Request, context: { params: { slug: string } }) { 13 | const { status, webhook } = (await req.json()) as { 14 | status: boolean; 15 | webhook: string; 16 | }; 17 | 18 | // If status is true, make sure webhook and roleId are not empty 19 | if (status && !webhook) { 20 | return NextResponse.json( 21 | { error: 'webhook is required when enabling Slack integration.' }, 22 | { status: 400 } 23 | ); 24 | } 25 | 26 | // Update project config 27 | const { data: updatedProjectConfig, error } = await updateProjectConfigBySlug( 28 | context.params.slug, 29 | { 30 | integration_slack_status: status, 31 | integration_slack_webhook: status ? webhook : null, 32 | }, 33 | 'route' 34 | ); 35 | 36 | // If any errors thrown, return error 37 | if (error) { 38 | return NextResponse.json({ error: error.message }, { status: error.status }); 39 | } 40 | 41 | // Return updated project config 42 | return NextResponse.json(updatedProjectConfig, { status: 200 }); 43 | } 44 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/config/integrations/sso/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { updateProjectConfigBySlug } from '@/lib/api/projects'; 3 | 4 | /* 5 | Update SSO configuration 6 | PATCH /api/v1/projects/:slug/config/integrations/sso 7 | { 8 | status: boolean, 9 | url: string, 10 | secret: string, 11 | } 12 | */ 13 | export async function PATCH(req: Request, context: { params: { slug: string } }) { 14 | const { status, url, secret } = await req.json(); 15 | 16 | if (status && (!url || !secret)) { 17 | return NextResponse.json( 18 | { error: 'url and secret are required when enabling SSO integration.' }, 19 | { status: 400 } 20 | ); 21 | } 22 | 23 | // Update project config 24 | const { data: updatedProjectConfig, error } = await updateProjectConfigBySlug( 25 | context.params.slug, 26 | { 27 | integration_sso_status: status, 28 | integration_sso_url: status ? url : null, 29 | integration_sso_secret: status ? secret : null, 30 | }, 31 | 'route' 32 | ); 33 | 34 | // If any errors thrown, return error 35 | if (error) { 36 | return NextResponse.json({ error: error.message }, { status: error.status }); 37 | } 38 | 39 | // Return updated project config 40 | return NextResponse.json(updatedProjectConfig, { status: 200 }); 41 | } 42 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/comments/[commentId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { deleteCommentForFeedbackById } from '@/lib/api/comments'; 3 | 4 | /* 5 | Delete comment for feedback by id 6 | DELETE /api/v1/projects/[slug]/feedback/[id]/comments/[id] 7 | */ 8 | export async function DELETE( 9 | req: Request, 10 | context: { params: { slug: string; feedbackId: string; commentId: string } } 11 | ) { 12 | const { data: comment, error } = await deleteCommentForFeedbackById( 13 | context.params.commentId, 14 | context.params.feedbackId, 15 | context.params.slug, 16 | 'route' 17 | ); 18 | 19 | // If any errors thrown, return error 20 | if (error) { 21 | return NextResponse.json({ error: error.message }, { status: error.status }); 22 | } 23 | 24 | // Return comment 25 | return NextResponse.json(comment, { status: 200 }); 26 | } 27 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/comments/[commentId]/upvote/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { upvoteCommentForFeedbackById } from '@/lib/api/comments'; 3 | 4 | /* 5 | Upvote comment for feedback by id 6 | POST /api/v1/projects/[slug]/feedback/[id]/comments/[id]/upvote 7 | */ 8 | export async function POST( 9 | req: Request, 10 | context: { params: { slug: string; feedbackId: string; commentId: string } } 11 | ) { 12 | const { data: comment, error } = await upvoteCommentForFeedbackById( 13 | context.params.commentId, 14 | context.params.feedbackId, 15 | context.params.slug, 16 | 'route' 17 | ); 18 | 19 | // If any errors thrown, return error 20 | if (error) { 21 | return NextResponse.json({ error: error.message }, { status: error.status }); 22 | } 23 | 24 | // Return comment 25 | return NextResponse.json(comment, { status: 200 }); 26 | } 27 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/comments/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { createCommentForFeedbackById, getCommentsForFeedbackById } from '@/lib/api/comments'; 3 | import { FeedbackCommentProps } from '@/lib/types'; 4 | 5 | /* 6 | Create feedback comment 7 | POST /api/v1/projects/[slug]/feedback/[id]/comments 8 | { 9 | content: string 10 | } 11 | */ 12 | export async function POST(req: Request, context: { params: { slug: string; feedbackId: string } }) { 13 | const { content, reply_to_id: replyToId } = (await req.json()) as FeedbackCommentProps['Insert']; 14 | 15 | if (!content) { 16 | return NextResponse.json({ error: 'Content cannot be empty' }, { status: 400 }); 17 | } 18 | 19 | const { data: comment, error } = await createCommentForFeedbackById( 20 | { 21 | feedback_id: context.params.feedbackId, 22 | content: content || '', 23 | user_id: 'dummy-id', 24 | reply_to_id: replyToId || null, 25 | }, 26 | context.params.slug, 27 | 'route' 28 | ); 29 | 30 | // If any errors thrown, return error 31 | if (error) { 32 | return NextResponse.json({ error: error.message }, { status: error.status }); 33 | } 34 | 35 | // Return comment 36 | return NextResponse.json(comment, { status: 200 }); 37 | } 38 | 39 | /* 40 | Get feedback comments 41 | GET /api/v1/projects/[slug]/feedback/[id]/comments 42 | */ 43 | export async function GET(req: Request, context: { params: { slug: string; feedbackId: string } }) { 44 | const { data: comments, error } = await getCommentsForFeedbackById( 45 | context.params.feedbackId, 46 | context.params.slug, 47 | 'route' 48 | ); 49 | 50 | // If any errors thrown, return error 51 | if (error) { 52 | return NextResponse.json({ error: error.message }, { status: error.status }); 53 | } 54 | 55 | // Return comments 56 | return NextResponse.json(comments, { status: 200 }); 57 | } 58 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { deleteFeedbackByID, getFeedbackByID, updateFeedbackByID } from '@/lib/api/feedback'; 3 | import { FeedbackWithUserInputProps } from '@/lib/types'; 4 | 5 | /* 6 | Get Project Feedback by ID 7 | GET /api/v1/projects/[slug]/feedback/[id] 8 | */ 9 | export async function GET(req: Request, context: { params: { slug: string; feedbackId: string } }) { 10 | const { data: feedback, error } = await getFeedbackByID( 11 | context.params.feedbackId, 12 | context.params.slug, 13 | 'route' 14 | ); 15 | 16 | // If any errors thrown, return error 17 | if (error) { 18 | return NextResponse.json({ error: error.message }, { status: error.status }); 19 | } 20 | 21 | // Return feedback 22 | return NextResponse.json(feedback, { status: 200 }); 23 | } 24 | 25 | /* 26 | Update Feedback by ID 27 | PATCH /api/v1/projects/[slug]/feedback/[id] 28 | { 29 | title: string; 30 | description: string; 31 | status: string; 32 | tags: string[]; 33 | } 34 | */ 35 | export async function PATCH(req: Request, context: { params: { slug: string; feedbackId: string } }) { 36 | const { title, description, status, tags } = (await req.json()) as FeedbackWithUserInputProps; 37 | 38 | const { data: feedback, error } = await updateFeedbackByID( 39 | context.params.feedbackId, 40 | context.params.slug, 41 | { 42 | title: title || '', 43 | description: description || '', 44 | status, 45 | project_id: 'dummy-id', 46 | user_id: 'dummy-id', 47 | tags: tags || undefined, 48 | }, 49 | 'route' 50 | ); 51 | 52 | // If any errors thrown, return error 53 | if (error) { 54 | return NextResponse.json({ error: error.message }, { status: error.status }); 55 | } 56 | 57 | // Return feedback 58 | return NextResponse.json(feedback, { status: 200 }); 59 | } 60 | 61 | /* 62 | Delete Feedback by ID 63 | DELETE /api/v1/projects/[slug]/feedback/[id] 64 | */ 65 | export async function DELETE(req: Request, context: { params: { slug: string; feedbackId: string } }) { 66 | const { data: feedback, error } = await deleteFeedbackByID( 67 | context.params.feedbackId, 68 | context.params.slug, 69 | 'route' 70 | ); 71 | 72 | // If any errors thrown, return error 73 | if (error) { 74 | return NextResponse.json({ error: error.message }, { status: error.status }); 75 | } 76 | 77 | // Return feedback 78 | return NextResponse.json(feedback, { status: 200 }); 79 | } 80 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/upvotes/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getFeedbackByID, getFeedbackUpvotersById, upvoteFeedbackByID } from '@/lib/api/feedback'; 3 | import { getCurrentUser } from '@/lib/api/user'; 4 | 5 | /* 6 | Get feedback upvotes 7 | GET /api/v1/projects/[slug]/feedback/[id]/upvote 8 | */ 9 | export async function GET(req: Request, context: { params: { slug: string; feedbackId: string } }) { 10 | const { data: feedback, error } = await getFeedbackByID( 11 | context.params.feedbackId, 12 | context.params.slug, 13 | 'route' 14 | ); 15 | 16 | // If any errors thrown, return error 17 | if (error) { 18 | return NextResponse.json({ error: error.message }, { status: error.status }); 19 | } 20 | 21 | // Get feedback upvoters 22 | const { data: upvoters, error: upvotersError } = await getFeedbackUpvotersById( 23 | context.params.feedbackId, 24 | context.params.slug, 25 | 'route' 26 | ); 27 | 28 | // If any errors thrown, return error 29 | if (upvotersError) { 30 | return NextResponse.json({ error: upvotersError.message }, { status: upvotersError.status }); 31 | } 32 | 33 | // Return feedback 34 | return NextResponse.json( 35 | { 36 | upvotes: feedback.upvotes, 37 | upvoters, 38 | }, 39 | { status: 200 } 40 | ); 41 | } 42 | 43 | /* 44 | Upvote a feedback 45 | POST /api/v1/projects/[slug]/feedback/[id]/upvote 46 | */ 47 | export async function POST(req: NextRequest, context: { params: { slug: string; feedbackId: string } }) { 48 | const hasUpvoted = req.nextUrl.searchParams.get('has_upvoted'); 49 | 50 | // Get current user 51 | const { data: isLoggedIn } = await getCurrentUser('route'); 52 | 53 | // Upvote feedback 54 | const { data: feedback, error } = await upvoteFeedbackByID( 55 | context.params.feedbackId, 56 | context.params.slug, 57 | 'route', 58 | hasUpvoted ? hasUpvoted === 'true' : undefined, 59 | !isLoggedIn 60 | ); 61 | 62 | // If any errors thrown, return error 63 | if (error) { 64 | return NextResponse.json({ error: error.message }, { status: error.status }); 65 | } 66 | 67 | // Return feedback 68 | return NextResponse.json(feedback, { status: 200 }); 69 | } 70 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/feedback/export/route.ts: -------------------------------------------------------------------------------- 1 | import { getAllProjectFeedback } from '@/lib/api/feedback'; 2 | import { formatRootUrl } from '@/lib/utils'; 3 | 4 | /* 5 | Export all feedback for a project 6 | GET /api/v1/projects/:slug/feedback/export 7 | */ 8 | export async function GET(req: Request, context: { params: { slug: string } }) { 9 | const { data: feedback, error } = await getAllProjectFeedback(context.params.slug, 'route'); 10 | 11 | // If any errors thrown, return error 12 | if (error) { 13 | return new Response(error.message, { status: error.status }); 14 | } 15 | 16 | // Create CSV 17 | const csv = `ID,Link,Title,Content,Status,Upvotes,Comment Count,User ID,User Name,User Email,User Avatar,Tags,Created At\n${feedback 18 | .map((feedback) => { 19 | return `${feedback.id},${formatRootUrl( 20 | context.params.slug, 21 | `/feedback/${feedback.id}` 22 | )},"${feedback.title.replace(/"/g, '""')}","${feedback.description.replace(/"/g, '""')}",${ 23 | feedback.status 24 | },${feedback.upvotes},${feedback.comment_count},${feedback.user_id},"${feedback.user.full_name.replace( 25 | /"/g, 26 | '""' 27 | )}","${feedback.user.email.replace(/"/g, '""')}",${ 28 | feedback.user.avatar_url ? `"${feedback.user.avatar_url.replace(/"/g, '""')}"` : '' 29 | },${feedback.tags ? feedback.tags.map((tag) => tag.name.replace(/"/g, '""')).join(',') : ''},${ 30 | feedback.created_at 31 | }\n`; 32 | }) 33 | .join('')}`; 34 | 35 | // Return CSV 36 | const timestamp = new Date().getTime(); 37 | return new Response(csv, { 38 | headers: { 39 | 'Content-Type': 'text/csv', 40 | 'Content-Disposition': `attachment; filename=${context.params.slug}-${timestamp}.csv`, 41 | }, 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/feedback/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { createFeedback, getAllProjectFeedback } from '@/lib/api/feedback'; 3 | import { FeedbackWithUserInputProps } from '@/lib/types'; 4 | 5 | export const runtime = 'edge'; 6 | 7 | /* 8 | Create Feedback 9 | POST /api/v1/projects/[slug]/feedback 10 | { 11 | title: string; 12 | description: string; 13 | status: string; 14 | tags: [id, id, id] 15 | } 16 | */ 17 | export async function POST(req: Request, context: { params: { slug: string } }) { 18 | const { title, description, status, tags, user } = (await req.json()) as FeedbackWithUserInputProps; 19 | 20 | // Validate Request Body 21 | if (!title) { 22 | return NextResponse.json({ error: 'title is required when creating feedback.' }, { status: 400 }); 23 | } 24 | 25 | const { data: feedback, error } = await createFeedback( 26 | context.params.slug, 27 | { 28 | title: title || '', 29 | description: description || '', 30 | status: status || '', 31 | project_id: 'dummy-id', 32 | user_id: 'dummy-id', 33 | tags: tags || [], 34 | user: user !== null ? user : undefined, 35 | }, 36 | 'route' 37 | ); 38 | 39 | // If any errors thrown, return error 40 | if (error) { 41 | return NextResponse.json({ error: error.message }, { status: error.status }); 42 | } 43 | 44 | // Return feedback 45 | return NextResponse.json(feedback, { status: 200 }); 46 | } 47 | 48 | /* 49 | Get Project Feedback 50 | GET /api/v1/projects/[slug]/feedback 51 | */ 52 | export async function GET(req: Request, context: { params: { slug: string } }) { 53 | const { data: feedback, error } = await getAllProjectFeedback(context.params.slug, 'route', false); 54 | 55 | // If any errors thrown, return error 56 | if (error) { 57 | return NextResponse.json({ error: error.message }, { status: error.status }); 58 | } 59 | 60 | // Return feedback 61 | return NextResponse.json(feedback, { status: 200 }); 62 | } 63 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/feedback/tags/[name]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { deleteFeedbackTagByName } from '@/lib/api/feedback'; 3 | 4 | /* 5 | Delete tag by name 6 | DELETE /api/v1/projects/:slug/feedback/tags/:name 7 | */ 8 | export async function DELETE(req: Request, context: { params: { slug: string; name: string } }) { 9 | const { data: tag, error } = await deleteFeedbackTagByName( 10 | context.params.slug, 11 | context.params.name, 12 | 'route' 13 | ); 14 | 15 | // If any errors thrown, return error 16 | if (error) { 17 | return NextResponse.json({ error: error.message }, { status: error.status }); 18 | } 19 | 20 | // Return tag 21 | return NextResponse.json(tag, { status: 200 }); 22 | } 23 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/feedback/tags/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { createFeedbackTag, getAllFeedbackTags } from '@/lib/api/feedback'; 3 | import { FeedbackTagProps } from '@/lib/types'; 4 | 5 | /* 6 | Create new tag 7 | POST /api/v1/projects/:slug/feedback/tags 8 | { 9 | name: string, 10 | color: string 11 | } 12 | */ 13 | export async function POST(req: Request, context: { params: { slug: string } }) { 14 | const { name, color } = (await req.json()) as FeedbackTagProps['Insert']; 15 | 16 | const { data: tag, error } = await createFeedbackTag( 17 | context.params.slug, 18 | { 19 | name: name || '', 20 | color: color || '', 21 | }, 22 | 'route' 23 | ); 24 | 25 | // If any errors thrown, return error 26 | if (error) { 27 | return NextResponse.json({ error: error.message }, { status: error.status }); 28 | } 29 | 30 | // Return tag 31 | return NextResponse.json(tag, { status: 200 }); 32 | } 33 | 34 | /* 35 | Get all feedback tags 36 | GET /api/v1/projects/:slug/feedback/tags 37 | */ 38 | export async function GET(req: Request, context: { params: { slug: string } }) { 39 | const { data: tags, error } = await getAllFeedbackTags(context.params.slug, 'route'); 40 | 41 | // If any errors thrown, return error 42 | if (error) { 43 | return NextResponse.json({ error: error.message }, { status: error.status }); 44 | } 45 | 46 | // Return tags 47 | return NextResponse.json(tags, { status: 200 }); 48 | } 49 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/invites/[inviteId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { acceptProjectInvite, deleteProjectInvite } from '@/lib/api/invites'; 3 | 4 | /* 5 | Accept project invite 6 | POST /api/v1/projects/[slug]/invites/[inviteId] 7 | */ 8 | export async function POST(req: Request, context: { params: { slug: string; inviteId: string } }) { 9 | const { data: invite, error } = await acceptProjectInvite(context.params.inviteId, 'route'); 10 | 11 | // If any errors thrown, return error 12 | if (error) { 13 | return NextResponse.json({ error: error.message }, { status: error.status }); 14 | } 15 | 16 | // Return success 17 | return NextResponse.json(invite, { status: 200 }); 18 | } 19 | 20 | /* 21 | Delete project invite 22 | DELETE /api/v1/projects/[slug]/invites/[inviteId] 23 | */ 24 | export async function DELETE(req: Request, context: { params: { slug: string; inviteId: string } }) { 25 | const { data: invite, error } = await deleteProjectInvite(context.params.inviteId, 'route'); 26 | 27 | // If any errors thrown, return error 28 | if (error) { 29 | return NextResponse.json({ error: error.message }, { status: error.status }); 30 | } 31 | 32 | // Return success 33 | return NextResponse.json(invite, { status: 200 }); 34 | } 35 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/invites/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { createProjectInvite, getProjectInvites } from '@/lib/api/invites'; 3 | 4 | /* 5 | Get all project invites 6 | GET /api/v1/projects/[slug]/invites 7 | */ 8 | export async function GET(req: Request, context: { params: { slug: string } }) { 9 | const { data: invites, error } = await getProjectInvites(context.params.slug, 'route'); 10 | 11 | // If any errors thrown, return error 12 | if (error) { 13 | return NextResponse.json({ error: error.message }, { status: error.status }); 14 | } 15 | 16 | // Return invites 17 | return NextResponse.json(invites, { status: 200 }); 18 | } 19 | 20 | /* 21 | Invite a new member to a project 22 | POST /api/v1/projects/[slug]/members 23 | { 24 | email: string 25 | } 26 | */ 27 | export async function POST(req: Request, context: { params: { slug: string } }) { 28 | const { email } = await req.json(); 29 | 30 | // Check if email is valid 31 | const emailRegex = new RegExp(/^[^\s@]+@[^\s@]+\.[^\s@]+$/); 32 | if (!emailRegex.test(email)) { 33 | return NextResponse.json({ error: 'Invalid email' }, { status: 400 }); 34 | } 35 | 36 | // Create invite 37 | const { data: invite, error } = await createProjectInvite(context.params.slug, 'route', email); 38 | 39 | // If any errors thrown, return error 40 | if (error) { 41 | return NextResponse.json({ error: error.message }, { status: error.status }); 42 | } 43 | 44 | // Return invite 45 | return NextResponse.json(invite, { status: 200 }); 46 | } 47 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/members/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { getProjectMembers } from '@/lib/api/projects'; 3 | 4 | /* 5 | Get all members of a project 6 | GET /api/v1/projects/[slug]/members 7 | */ 8 | export async function GET(req: Request, context: { params: { slug: string } }) { 9 | const { data: members, error } = await getProjectMembers(context.params.slug, 'route'); 10 | 11 | // If any errors thrown, return error 12 | if (error) { 13 | return NextResponse.json({ error: error.message }, { status: error.status }); 14 | } 15 | 16 | // Return members 17 | return NextResponse.json(members, { status: 200 }); 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/[slug]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { deleteProjectBySlug, getProjectBySlug, updateProjectBySlug } from '@/lib/api/projects'; 3 | import { ProjectProps } from '@/lib/types'; 4 | 5 | /* 6 | Get project by slug 7 | GET /api/v1/projects/[slug] 8 | */ 9 | export async function GET(req: Request, context: { params: { slug: string } }) { 10 | const { data: project, error } = await getProjectBySlug(context.params.slug, 'route'); 11 | // If any errors thrown, return error 12 | if (error) { 13 | return NextResponse.json({ error: error.message }, { status: error.status }); 14 | } 15 | 16 | // Return project 17 | return NextResponse.json({ project }, { status: 200 }); 18 | } 19 | 20 | /* 21 | Update project by slug 22 | PATCH /api/v1/projects/[slug] 23 | { 24 | name: string, 25 | slug: string, 26 | icon: string, 27 | icon_radius: string, 28 | og_image: string 29 | } 30 | */ 31 | export async function PATCH(req: Request, context: { params: { slug: string } }) { 32 | const { 33 | name, 34 | slug, 35 | icon, 36 | icon_radius: iconRadius, 37 | og_image: OGImage, 38 | } = (await req.json()) as ProjectProps['Update']; 39 | 40 | const { data: updatedProject, error } = await updateProjectBySlug( 41 | context.params.slug, 42 | { name, slug, icon, icon_radius: iconRadius, og_image: OGImage }, 43 | 'route' 44 | ); 45 | 46 | // If any errors thrown, return error 47 | if (error) { 48 | return NextResponse.json({ error: error.message }, { status: error.status }); 49 | } 50 | 51 | // Return updated project 52 | return NextResponse.json(updatedProject, { status: 200 }); 53 | } 54 | 55 | /* 56 | Delete project by slug 57 | DELETE /api/v1/projects/[slug] 58 | */ 59 | export async function DELETE(req: Request, context: { params: { slug: string } }) { 60 | const { data, error } = await deleteProjectBySlug(context.params.slug, 'route'); 61 | 62 | // If any errors thrown, return error 63 | if (error) { 64 | return NextResponse.json({ error: error.message }, { status: error.status }); 65 | } 66 | 67 | // Return success 68 | return NextResponse.json({ data }, { status: 200 }); 69 | } 70 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/projects/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { createProject } from '@/lib/api/projects'; 3 | import { getUserProjects } from '@/lib/api/user'; 4 | 5 | /* 6 | Create Project 7 | POST /api/v1/projects 8 | { 9 | "name": "Project Name", 10 | "slug": "project-slug", 11 | } 12 | */ 13 | export async function POST(req: Request) { 14 | // Get Request Body 15 | const { name, slug } = await req.json(); 16 | 17 | // Validate Request Body 18 | if (!name || !slug) { 19 | return NextResponse.json({ error: 'name and slug are required.' }, { status: 400 }); 20 | } 21 | 22 | // Create Project 23 | const { data: project, error } = await createProject({ name, slug }, 'route'); 24 | 25 | // Check for errors 26 | if (error) { 27 | return NextResponse.json({ error: error.message }, { status: error.status }); 28 | } 29 | 30 | return NextResponse.json(project, { status: 201 }); 31 | } 32 | 33 | /* 34 | Get User Projects 35 | GET /api/v1/projects 36 | */ 37 | export async function GET(req: Request) { 38 | // Get User Projects 39 | const { data: projects, error } = await getUserProjects('route'); 40 | 41 | // Check for errors 42 | if (error) { 43 | return NextResponse.json({ error: error.message }, { status: error.status }); 44 | } 45 | 46 | // Return projects 47 | return NextResponse.json(projects, { status: 200 }); 48 | } 49 | -------------------------------------------------------------------------------- /apps/web/app/api/v1/waitlist/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | /* 4 | Enroll in Waitlist 5 | POST /api/v1/waitlist 6 | { 7 | "name": "User Name", 8 | "email": "email@email.com" 9 | } 10 | */ 11 | export async function POST(req: Request) { 12 | // Return data 13 | return NextResponse.json({ error: 'Waitlist is closed.' }, { status: 400 }); 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/app/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { cookies } from 'next/headers'; 2 | import { NextResponse } from 'next/server'; 3 | import { createServerClient, type CookieOptions } from '@supabase/ssr'; 4 | 5 | export const dynamic = 'force-dynamic'; 6 | 7 | export async function GET(request: Request) { 8 | // The `/auth/callback` route is required for the server-side auth flow implemented 9 | // by the Auth Helpers package. It exchanges an auth code for the user's session. 10 | // https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange 11 | const cookieStore = cookies(); 12 | const requestUrl = new URL(request.url); 13 | const code = requestUrl.searchParams.get('code'); 14 | const redirect = requestUrl.searchParams.get('successRedirect'); 15 | 16 | if (code) { 17 | const supabase = createServerClient( 18 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 19 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 20 | { 21 | cookies: { 22 | get(name: string) { 23 | return cookieStore.get(name)?.value; 24 | }, 25 | set(name: string, value: string, options: CookieOptions) { 26 | cookieStore.set({ name, value, ...options }); 27 | }, 28 | remove(name: string, options: CookieOptions) { 29 | cookieStore.set({ name, value: '', ...options }); 30 | }, 31 | }, 32 | } 33 | ); 34 | 35 | await supabase.auth.exchangeCodeForSession(code); 36 | } 37 | 38 | // URL to redirect to after sign in process completes 39 | return NextResponse.redirect(new URL(redirect || '/', requestUrl.origin)); 40 | } 41 | -------------------------------------------------------------------------------- /apps/web/app/dash/(auth)/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import { cookies } from 'next/headers'; 3 | import Link from 'next/link'; 4 | import { redirect } from 'next/navigation'; 5 | import { createServerClient } from '@supabase/ssr'; 6 | import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui/components/ui/card'; 7 | import { UserAuthForm } from '@/components/user-auth-form'; 8 | 9 | export const metadata: Metadata = { 10 | title: 'Sign in to Feedbase', 11 | description: 'Sign in to your Feedbase account.', 12 | }; 13 | 14 | export default async function SignIn() { 15 | // Create a Supabase client configured to use cookies 16 | const cookieStore = cookies(); 17 | const supabase = createServerClient( 18 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 19 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 20 | { 21 | cookies: { 22 | get(name: string) { 23 | return cookieStore.get(name)?.value; 24 | }, 25 | }, 26 | } 27 | ); 28 | 29 | // Retrieve possible session 30 | const { 31 | data: { user }, 32 | } = await supabase.auth.getUser(); 33 | 34 | // If there is a session, redirect to projects 35 | if (user) { 36 | redirect('/'); 37 | } 38 | 39 | return ( 40 |
41 | 42 | 43 | Sign In 44 | 45 | Sign in with your email address to continue. 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | Don't have an account?{' '} 54 | 55 | Sign Up 56 | 57 | 58 | 59 | 60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /apps/web/app/dash/(auth)/signup/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import { cookies } from 'next/headers'; 3 | import Link from 'next/link'; 4 | import { redirect } from 'next/navigation'; 5 | import { createServerClient } from '@supabase/ssr'; 6 | import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui/components/ui/card'; 7 | import { UserAuthForm } from '@/components/user-auth-form'; 8 | 9 | export const metadata: Metadata = { 10 | title: 'Sign up to Feedbase', 11 | description: 'Sign up for a new Feedbase account.', 12 | }; 13 | 14 | export default async function SignUp() { 15 | // Create a Supabase client configured to use cookies 16 | const cookieStore = cookies(); 17 | const supabase = createServerClient( 18 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 19 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 20 | { 21 | cookies: { 22 | get(name: string) { 23 | return cookieStore.get(name)?.value; 24 | }, 25 | }, 26 | } 27 | ); 28 | 29 | // Retrieve possible session 30 | const { 31 | data: { user }, 32 | } = await supabase.auth.getUser(); 33 | 34 | // If there is a session, redirect to projects 35 | if (user) { 36 | redirect('/'); 37 | } 38 | 39 | return ( 40 |
41 | 42 | 43 | Sign up 44 | 45 | Sign up with your email address to continue. 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | Already have an account?{' '} 54 | 55 | Sign In 56 | 57 | 58 | 59 | 60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /apps/web/app/dash/[slug]/analytics/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@ui/components/ui/skeleton'; 2 | 3 | export default function FeedbackLoading() { 4 | return ( 5 |
6 |
7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /apps/web/app/dash/[slug]/analytics/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { getProjectAnalytics } from '@/lib/api/projects'; 3 | import AnalyticsCards from '@/components/dashboard/analytics/chart-cards'; 4 | 5 | export default async function AnalyticsPage({ params }: { params: { slug: string } }) { 6 | const { data, error } = await getProjectAnalytics(params.slug, 'server'); 7 | 8 | if (!data) { 9 | return
Error: {error?.message}
; 10 | } 11 | 12 | return ( 13 |
14 | 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/app/dash/[slug]/changelog/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@ui/components/ui/skeleton'; 2 | 3 | export default function FeedbackLoading() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/dash/[slug]/feedback/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@ui/components/ui/skeleton'; 2 | 3 | export default function FeedbackLoading() { 4 | return ( 5 | <> 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/app/dash/[slug]/feedback/page.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardDescription, CardHeader, CardTitle } from 'ui/components/ui/card'; 2 | import { getAllFeedbackTags, getAllProjectFeedback } from '@/lib/api/feedback'; 3 | import FeedbackTable from '@/components/dashboard/feedback/feedback-table'; 4 | import FeedbackHeader from '@/components/dashboard/feedback/header-buttons'; 5 | 6 | export default async function Feedback({ 7 | params, 8 | searchParams, 9 | }: { 10 | params: { slug: string }; 11 | searchParams: { tag: string }; 12 | }) { 13 | const { data: feedback, error } = await getAllProjectFeedback(params.slug, 'server'); 14 | if (error) { 15 | return
{error.message}
; 16 | } 17 | 18 | const { data: tags, error: tagsError } = await getAllFeedbackTags(params.slug, 'server'); 19 | if (tagsError) { 20 | return
{tagsError.message}
; 21 | } 22 | 23 | return ( 24 |
25 | {/* Header Row */} 26 | 27 | 28 | {/* Feedback Posts */} 29 | {/* If there is no feedback, show empty text in the center */} 30 | {feedback.length === 0 && ( 31 | 32 | 33 | No feedback yet 34 | 35 | Once somone submits feedback, it will show up here. Make sure to share it so others can vote on 36 | it! 37 | 38 | 39 | 40 | )} 41 | 42 | {/* If there is feedback, show feedback list */} 43 | {feedback.length > 0 && } 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /apps/web/app/dash/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | export default async function ProjectPage({ params }: { params: { slug: string } }) { 4 | // Redirect to changelog 5 | redirect(`/${params.slug}/changelog`); 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/app/dash/[slug]/settings/general/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from 'ui/components/ui/skeleton'; 2 | 3 | export default function GeneralLoading() { 4 | return ( 5 | // 3 cards with loading state 6 |
7 | 8 | 9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/app/dash/[slug]/settings/general/page.tsx: -------------------------------------------------------------------------------- 1 | import { getProjectBySlug, getProjectConfigBySlug } from '@/lib/api/projects'; 2 | import GeneralConfigCards from '@/components/dashboard/settings/general-cards'; 3 | 4 | export default async function GeneralSettings({ params }: { params: { slug: string } }) { 5 | // Fetch project data 6 | const { data: project, error } = await getProjectBySlug(params.slug, 'server'); 7 | 8 | if (error) { 9 | return
{error.message}
; 10 | } 11 | 12 | // Fetch project config 13 | const { data: projectConfig, error: projectConfigError } = await getProjectConfigBySlug( 14 | params.slug, 15 | 'server' 16 | ); 17 | 18 | if (projectConfigError) { 19 | return
{projectConfigError.message}
; 20 | } 21 | 22 | return ( 23 |
24 | {/* General Card */} 25 | 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /apps/web/app/dash/[slug]/settings/hub/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from 'ui/components/ui/skeleton'; 2 | 3 | export default function HubLoading() { 4 | return ( 5 | // 3 cards with loading state 6 |
7 | 8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/app/dash/[slug]/settings/hub/page.tsx: -------------------------------------------------------------------------------- 1 | import { getProjectBySlug, getProjectConfigBySlug } from '@/lib/api/projects'; 2 | import HubConfigCards from '@/components/dashboard/settings/hub-cards'; 3 | 4 | export default async function HubSettings({ params }: { params: { slug: string } }) { 5 | // Fetch project data 6 | const { data: project, error } = await getProjectBySlug(params.slug, 'server'); 7 | 8 | if (error) { 9 | return
{error.message}
; 10 | } 11 | 12 | // Fetch project config 13 | const { data: projectConfig, error: configError } = await getProjectConfigBySlug(params.slug, 'server'); 14 | 15 | if (configError) { 16 | return
{configError.message}
; 17 | } 18 | 19 | return ( 20 |
21 | {/* Hub Card */} 22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /apps/web/app/dash/[slug]/settings/integrations/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from 'ui/components/ui/skeleton'; 2 | 3 | export default function IntegrationsLoading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/app/dash/[slug]/settings/integrations/page.tsx: -------------------------------------------------------------------------------- 1 | import { getProjectConfigBySlug } from '@/lib/api/projects'; 2 | import IntegrationCards from '@/components/dashboard/settings/integration-cards'; 3 | 4 | export default async function IntegrationsSettings({ params }: { params: { slug: string } }) { 5 | const { data, error } = await getProjectConfigBySlug(params.slug, 'server'); 6 | 7 | if (error) { 8 | return
Error
; 9 | } 10 | 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/app/dash/[slug]/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | import { headers } from 'next/headers'; 2 | import CategoryTabs from '@/components/dashboard/settings/category-tabs'; 3 | 4 | const tabs = [ 5 | { 6 | name: 'General', 7 | slug: 'general', 8 | }, 9 | { 10 | name: 'Hub', 11 | slug: 'hub', 12 | }, 13 | { 14 | name: 'Team', 15 | slug: 'team', 16 | }, 17 | { 18 | name: 'Integrations', 19 | slug: 'integrations', 20 | }, 21 | ]; 22 | 23 | export default function SettingsLayout({ 24 | children, 25 | params, 26 | }: { 27 | children: React.ReactNode; 28 | params: { slug: string }; 29 | }) { 30 | // Headers 31 | const headerList = headers(); 32 | const pathname = headerList.get('x-pathname'); 33 | 34 | // Retrieve the currently active tab 35 | const activeTabIndex = tabs.findIndex((tab) => pathname?.split('/')[3] === tab.slug); 36 | 37 | return ( 38 |
39 | {/* Navigation tabs */} 40 | 41 | 42 | {/* Content */} 43 |
{children}
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /apps/web/app/dash/[slug]/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | export default async function Dashboard({ params }: { params: { slug: string } }) { 4 | // Redirect to settings/general 5 | redirect(`/${params.slug}/settings/general`); 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/app/dash/[slug]/settings/team/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from 'ui/components/ui/skeleton'; 2 | 3 | export default function TeamLoading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/app/dash/[slug]/settings/team/page.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 'ui/components/ui/card'; 2 | import { getProjectInvites } from '@/lib/api/invites'; 3 | import { getProjectMembers } from '@/lib/api/projects'; 4 | import { TeamTable } from '@/components/dashboard/settings/team-table'; 5 | 6 | export default async function TeamSettings({ params }: { params: { slug: string } }) { 7 | const { data: members, error } = await getProjectMembers(params.slug, 'server'); 8 | 9 | if (error) { 10 | return
{error.message}
; 11 | } 12 | 13 | const { data: invites, error: invitesError } = await getProjectInvites(params.slug, 'server'); 14 | 15 | if (invitesError) { 16 | return
{invitesError.message}
; 17 | } 18 | 19 | return ( 20 |
21 | 22 | 23 | Team 24 | Add or remove users that have access to this project. 25 | 26 | 27 | 28 | 29 | 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /apps/web/app/dash/invite/[inviteId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation'; 2 | import { getProjectInvite } from '@/lib/api/invites'; 3 | import { getCurrentUser } from '@/lib/api/user'; 4 | import ProjectInviteForm from '@/components/layout/accept-invite-form'; 5 | 6 | export default async function ProjectInvite({ params }: { params: { inviteId: string } }) { 7 | const { data: invite, error: inviteError } = await getProjectInvite(params.inviteId, 'server'); 8 | 9 | if (inviteError) { 10 | return notFound(); 11 | } 12 | 13 | // Get current user 14 | const { data: user } = await getCurrentUser('server'); 15 | 16 | return ( 17 |
18 | 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/app/dash/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | import { getUserProjects } from '@/lib/api/user'; 3 | import Onboarding from '@/components/layout/onboarding'; 4 | 5 | export default async function Projects() { 6 | const { data: projects, error } = await getUserProjects('server'); 7 | 8 | if (error) { 9 | // Redirect to login if the user is not authenticated 10 | if (error.status === 401) { 11 | return redirect('/login'); 12 | } 13 | 14 | return
{error.message}
; 15 | } 16 | 17 | // Redirect to the first project 18 | if (projects.length > 0) { 19 | return redirect(`/${projects[0].slug}`); 20 | } 21 | 22 | // TODO: Improve this and make this redirect to an onboarding page if the user has no projects 23 | return ( 24 |
25 | 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /apps/web/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chroxify/feedbase/18dca32a923e12b3af54f1f761c063c7925dfeef/apps/web/app/favicon.ico -------------------------------------------------------------------------------- /apps/web/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | /* App root background */ 8 | --root-background: 230 35% 3%; /* #000000 */ 9 | 10 | /* Utility background (Buttons, inputs, etc) */ 11 | --background: 230 11% 12%; /* #0A0C10 | #060606*/ 12 | --foreground: 210 40% 98%; /* #D6D6D6 */ 13 | 14 | /* Muted background (Skeletons, Avatar, tabs, etc) */ 15 | --muted: 230 11% 8%; /* #20242c */ 16 | --muted-foreground: 215 6% 62%; 17 | 18 | --popover: 230 35% 3%; /* #0A0C10 */ 19 | --popover-foreground: 210 40% 98%; 20 | 21 | --card: 230 35% 3%; /* #0A0C10 */ 22 | --card-foreground: 210 40% 98%; 23 | 24 | --border: 222 9% 22%; /* #20242c */ 25 | --input: 222 9% 22%; /* #20242c */ 26 | 27 | --primary: 210 40% 98%; 28 | --primary-foreground: 230 35% 3%; 29 | 30 | /* Hover & active backgrounds (Nav buttons, etc) */ 31 | --secondary: 227 11% 12%; /* #20242c */ 32 | --secondary-foreground: 210 40% 98%; 33 | 34 | /* Button background hovers */ 35 | --accent: 227 11% 15%; /* #20242c */ 36 | --accent-foreground: 210 40% 98%; 37 | 38 | --destructive: 0 84.2% 60.2%; 39 | --destructive-foreground: 210 40% 98%; 40 | 41 | /* Selection & accent color */ 42 | --highlight: 231 100% 78%; 43 | 44 | --ring: 230 9% 13%; /* #20242c */ 45 | 46 | --radius: 0.5rem; 47 | } 48 | 49 | .light { 50 | /* App root background */ 51 | --root-background: 0 0% 100%; /* #000000 */ 52 | --background: 0 0% 100%; 53 | --foreground: 240 10% 3.9%; 54 | --card: 0 0% 100%; 55 | --card-foreground: 240 10% 3.9%; 56 | --popover: 0 0% 100%; 57 | --popover-foreground: 240 10% 3.9%; 58 | --primary: 240 5.9% 10%; 59 | --primary-foreground: 0 0% 98%; 60 | --secondary: 240 4.8% 95.9%; 61 | --secondary-foreground: 240 5.9% 10%; 62 | --muted: 240 4.8% 95.9%; 63 | --muted-foreground: 240 3.8% 46.1%; 64 | --accent: 240 4.8% 95.9%; 65 | --accent-foreground: 240 5.9% 10%; 66 | --destructive: 0 84.2% 60.2%; 67 | --destructive-foreground: 0 0% 98%; 68 | --border: 240 5.9% 90%; 69 | --input: 240 5.9% 90%; 70 | --highlight: 231 100% 78%; 71 | --ring: 240 5.9% 10%; 72 | } 73 | } 74 | 75 | @layer base { 76 | * { 77 | @apply border-border; 78 | } 79 | body { 80 | @apply bg-root text-foreground selection:bg-highlight/20 selection:text-highlight; 81 | } 82 | } -------------------------------------------------------------------------------- /apps/web/app/home/(links)/demo/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | import { formatRootUrl } from '@/lib/utils'; 3 | 4 | export default function Demo() { 5 | redirect(formatRootUrl('hub')); 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/app/home/(links)/deploy-vercel/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | export default function VercelDeploy() { 4 | redirect( 5 | 'https://vercel.com/new/clone?repository-url=https://github.com/chroxify/feedbase&env=NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_ANON_KEY,NEXT_PUBLIC_ROOT_DOMAIN&project-name=Feedbase&repo-name=feedbase' 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/app/home/(links)/discord/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | export default function Docs() { 4 | redirect('https://discord.gg/UmWgwcxbpy'); 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/home/(links)/docs/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | import { formatRootUrl } from '@/lib/utils'; 3 | 4 | export default function Docs() { 5 | redirect(formatRootUrl('docs')); 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/app/home/(links)/github/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | export default function Github() { 4 | redirect('https://github.com/chroxify/feedbase'); 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/home/(links)/twitter/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | export default function Github() { 4 | redirect('https://x.com/0xChroxify'); 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/home/(pages)/layout.tsx: -------------------------------------------------------------------------------- 1 | import HomeNav from '@/components/home/header'; 2 | 3 | export default function HomeLayout({ children }: { children: React.ReactNode }) { 4 | return ( 5 |
6 | 7 | 8 | {children} 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css'; 2 | import { Metadata, Viewport } from 'next'; 3 | import Script from 'next/script'; 4 | import { cn } from '@ui/lib/utils'; 5 | import { SpeedInsights } from '@vercel/speed-insights/next'; 6 | import { GeistSans } from 'geist/font'; 7 | import { Toaster } from 'sonner'; 8 | import { formatRootUrl } from '@/lib/utils'; 9 | 10 | export const metadata: Metadata = { 11 | title: 'Feedbase', 12 | description: 'Collect feedback & communicate product updates with ease.', 13 | metadataBase: new URL(formatRootUrl()), 14 | openGraph: { 15 | images: [ 16 | { 17 | url: '/og-image.png', 18 | }, 19 | ], 20 | }, 21 | // PWA 22 | manifest: '/manifest.json', 23 | icons: [ 24 | { 25 | rel: 'apple-touch-icon', 26 | url: '/favicon.ico', 27 | }, 28 | ], 29 | }; 30 | 31 | export const viewport: Viewport = { 32 | // Prevents auto-zoom on mobile for input fields 33 | width: 'device-width', 34 | initialScale: 1, 35 | maximumScale: 1, 36 | themeColor: '#06060A', 37 | }; 38 | 39 | export default function RootLayout({ children }: { children: React.ReactNode }) { 40 | return ( 41 | 42 | 43 | {process.env.NODE_ENV === 'production' && ( 44 |