diff --git a/.env.example b/.env.example index eb34d65..b2b28d0 100644 --- a/.env.example +++ b/.env.example @@ -34,3 +34,10 @@ VERCEL_AUTH_TOKEN=your-auth-token # Only required if you want to use Tinybird for hub data analytics TINYBIRD_API_URL=https://api.eu-central-1.aws.tinybird.co # Make sure to use the correct region TINYBIRD_API_KEY=your-api-key + +#? Trigger.dev +# Only required if you want to use Trigger.dev +TRIGGER_PROJECT_ID=your-project-id +TRIGGER_API_KEY=your-trigger-api-key +TRIGGER_API_URL=https://api.trigger.dev # This differes based if you're self-hosting or using the cloud version +NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY=your-public-api-key \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 1bd948e..1bb7f9e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,10 +1,10 @@ +// This configuration only applies to the package manager root. +/** @type {import("eslint").Linter.Config} */ module.exports = { - root: true, - // This tells ESLint to load the config from the package `eslint-config-custom` - extends: ['custom/library'], - settings: { - next: { - rootDir: ['apps/*/', 'packages/*/'], - }, + extends: ['@feedbase/eslint-config/library.js'], + ignorePatterns: ['apps/**', 'packages/**'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: true, }, }; diff --git a/.husky/pre-commit b/.husky/pre-commit index fdf7287..4e1995a 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,3 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" - -pnpm run lint --fix && pnpm run ts && pnpm lint-staged +pnpm lint-staged diff --git a/apps/docs/api-reference/endpoint/changelog/delete-projects-changelogs.mdx b/apps/docs/api-reference/endpoint/changelog/delete-projects-changelogs.mdx index bc524cd..63189a6 100644 --- a/apps/docs/api-reference/endpoint/changelog/delete-projects-changelogs.mdx +++ b/apps/docs/api-reference/endpoint/changelog/delete-projects-changelogs.mdx @@ -1,4 +1,4 @@ --- -title: "Delete a Changelog" -openapi: delete /projects/{projectSlug}/changelogs/{changelogId} ---- \ No newline at end of file +title: 'Delete a Changelog' +openapi: delete /projects/{workspaceSlug}/changelogs/{changelogId} +--- diff --git a/apps/docs/api-reference/endpoint/changelog/get-projects-changelogs.mdx b/apps/docs/api-reference/endpoint/changelog/get-projects-changelogs.mdx index 59f620c..cbc8106 100644 --- a/apps/docs/api-reference/endpoint/changelog/get-projects-changelogs.mdx +++ b/apps/docs/api-reference/endpoint/changelog/get-projects-changelogs.mdx @@ -1,4 +1,4 @@ --- title: List Changelogs -openapi: get /projects/{projectSlug}/changelogs ---- \ No newline at end of file +openapi: get /projects/{workspaceSlug}/changelogs +--- diff --git a/apps/docs/api-reference/endpoint/changelog/patch-projects-changelogs.mdx b/apps/docs/api-reference/endpoint/changelog/patch-projects-changelogs.mdx index f9bd53c..bd31595 100644 --- a/apps/docs/api-reference/endpoint/changelog/patch-projects-changelogs.mdx +++ b/apps/docs/api-reference/endpoint/changelog/patch-projects-changelogs.mdx @@ -1,4 +1,4 @@ --- -title: "Update a Changelog" -openapi: put /projects/{projectSlug}/changelogs/{changelogId} ---- \ No newline at end of file +title: 'Update a Changelog' +openapi: put /projects/{workspaceSlug}/changelogs/{changelogId} +--- diff --git a/apps/docs/api-reference/endpoint/changelog/post-projects-changelogs.mdx b/apps/docs/api-reference/endpoint/changelog/post-projects-changelogs.mdx index d02d2f2..d9bb486 100644 --- a/apps/docs/api-reference/endpoint/changelog/post-projects-changelogs.mdx +++ b/apps/docs/api-reference/endpoint/changelog/post-projects-changelogs.mdx @@ -1,4 +1,4 @@ --- -title: "Create a Changelog" -openapi: post /projects/{projectSlug}/changelogs ---- \ No newline at end of file +title: 'Create a Changelog' +openapi: post /projects/{workspaceSlug}/changelogs +--- diff --git a/apps/docs/api-reference/endpoint/feedback-comments/delete-projects-feedback-comments.mdx b/apps/docs/api-reference/endpoint/feedback-comments/delete-projects-feedback-comments.mdx index 9c97ed6..faab519 100644 --- a/apps/docs/api-reference/endpoint/feedback-comments/delete-projects-feedback-comments.mdx +++ b/apps/docs/api-reference/endpoint/feedback-comments/delete-projects-feedback-comments.mdx @@ -1,4 +1,4 @@ --- title: Delete a Comment -openapi: delete /projects/{projectSlug}/feedback/{feedbackId}/comments/{commentId} ---- \ No newline at end of file +openapi: delete /projects/{workspaceSlug}/feedback/{feedbackId}/comments/{commentId} +--- diff --git a/apps/docs/api-reference/endpoint/feedback-comments/get-projects-feedback-comments.mdx b/apps/docs/api-reference/endpoint/feedback-comments/get-projects-feedback-comments.mdx index eb6e8c9..1d646a0 100644 --- a/apps/docs/api-reference/endpoint/feedback-comments/get-projects-feedback-comments.mdx +++ b/apps/docs/api-reference/endpoint/feedback-comments/get-projects-feedback-comments.mdx @@ -1,4 +1,4 @@ --- title: List Feedback Comments -openapi: get /projects/{projectSlug}/feedback/{feedbackId}/comments ---- \ No newline at end of file +openapi: get /projects/{workspaceSlug}/feedback/{feedbackId}/comments +--- diff --git a/apps/docs/api-reference/endpoint/feedback-tags/delete-projects-feedback-tags.mdx b/apps/docs/api-reference/endpoint/feedback-tags/delete-projects-feedback-tags.mdx index b6f6214..1fa9f1e 100644 --- a/apps/docs/api-reference/endpoint/feedback-tags/delete-projects-feedback-tags.mdx +++ b/apps/docs/api-reference/endpoint/feedback-tags/delete-projects-feedback-tags.mdx @@ -1,4 +1,4 @@ --- title: Delete a Feedback Tag -openapi: delete /projects/{projectSlug}/feedback/tags/{tagName} ---- \ No newline at end of file +openapi: delete /projects/{workspaceSlug}/feedback/tags/{tagName} +--- diff --git a/apps/docs/api-reference/endpoint/feedback-tags/get-projects-feedback-tags.mdx b/apps/docs/api-reference/endpoint/feedback-tags/get-projects-feedback-tags.mdx index 96b1172..410adc9 100644 --- a/apps/docs/api-reference/endpoint/feedback-tags/get-projects-feedback-tags.mdx +++ b/apps/docs/api-reference/endpoint/feedback-tags/get-projects-feedback-tags.mdx @@ -1,4 +1,4 @@ --- title: List Feedback Tags -openapi: get /projects/{projectSlug}/feedback/tags ---- \ No newline at end of file +openapi: get /projects/{workspaceSlug}/feedback/tags +--- diff --git a/apps/docs/api-reference/endpoint/feedback-tags/post-projects-feedback-tags.mdx b/apps/docs/api-reference/endpoint/feedback-tags/post-projects-feedback-tags.mdx index f9184bd..73d7d71 100644 --- a/apps/docs/api-reference/endpoint/feedback-tags/post-projects-feedback-tags.mdx +++ b/apps/docs/api-reference/endpoint/feedback-tags/post-projects-feedback-tags.mdx @@ -1,4 +1,4 @@ --- title: Create a Feedback Tag -openapi: post /projects/{projectSlug}/feedback/tags ---- \ No newline at end of file +openapi: post /projects/{workspaceSlug}/feedback/tags +--- diff --git a/apps/docs/api-reference/endpoint/feedback/delete-projects-feedback.mdx b/apps/docs/api-reference/endpoint/feedback/delete-projects-feedback.mdx index 753d98c..c9a4f4d 100644 --- a/apps/docs/api-reference/endpoint/feedback/delete-projects-feedback.mdx +++ b/apps/docs/api-reference/endpoint/feedback/delete-projects-feedback.mdx @@ -1,4 +1,4 @@ --- title: Delete a Feedback -openapi: delete /projects/{projectSlug}/feedback/{feedbackId} ---- \ No newline at end of file +openapi: delete /projects/{workspaceSlug}/feedback/{feedbackId} +--- diff --git a/apps/docs/api-reference/endpoint/feedback/get-projects-feedback-all.mdx b/apps/docs/api-reference/endpoint/feedback/get-projects-feedback-all.mdx index 49287c5..2c889a7 100644 --- a/apps/docs/api-reference/endpoint/feedback/get-projects-feedback-all.mdx +++ b/apps/docs/api-reference/endpoint/feedback/get-projects-feedback-all.mdx @@ -1,4 +1,4 @@ --- title: List Project Feedback -openapi: get /projects/{projectSlug}/feedback ---- \ No newline at end of file +openapi: get /projects/{workspaceSlug}/feedback +--- diff --git a/apps/docs/api-reference/endpoint/feedback/get-projects-feedback-upvotes.mdx b/apps/docs/api-reference/endpoint/feedback/get-projects-feedback-upvotes.mdx index 26ce89b..04b1580 100644 --- a/apps/docs/api-reference/endpoint/feedback/get-projects-feedback-upvotes.mdx +++ b/apps/docs/api-reference/endpoint/feedback/get-projects-feedback-upvotes.mdx @@ -1,4 +1,4 @@ --- title: List Feedback Upvoters -openapi: get /projects/{projectSlug}/feedback/{feedbackId}/upvotes ---- \ No newline at end of file +openapi: get /projects/{workspaceSlug}/feedback/{feedbackId}/upvotes +--- diff --git a/apps/docs/api-reference/endpoint/feedback/get-projects-feedback.mdx b/apps/docs/api-reference/endpoint/feedback/get-projects-feedback.mdx index 94811e7..81bc379 100644 --- a/apps/docs/api-reference/endpoint/feedback/get-projects-feedback.mdx +++ b/apps/docs/api-reference/endpoint/feedback/get-projects-feedback.mdx @@ -1,4 +1,4 @@ --- title: Retrieve a Feedback -openapi: get /projects/{projectSlug}/feedback/{feedbackId} ---- \ No newline at end of file +openapi: get /projects/{workspaceSlug}/feedback/{feedbackId} +--- diff --git a/apps/docs/api-reference/endpoint/feedback/patch-projects-feedback.mdx b/apps/docs/api-reference/endpoint/feedback/patch-projects-feedback.mdx index 2022075..3063e98 100644 --- a/apps/docs/api-reference/endpoint/feedback/patch-projects-feedback.mdx +++ b/apps/docs/api-reference/endpoint/feedback/patch-projects-feedback.mdx @@ -1,4 +1,4 @@ --- title: Update a Feedback -openapi: patch /projects/{projectSlug}/feedback/{feedbackId} ---- \ No newline at end of file +openapi: patch /projects/{workspaceSlug}/feedback/{feedbackId} +--- diff --git a/apps/docs/api-reference/endpoint/feedback/post-projects-feedback.mdx b/apps/docs/api-reference/endpoint/feedback/post-projects-feedback.mdx index d63bb57..87db47f 100644 --- a/apps/docs/api-reference/endpoint/feedback/post-projects-feedback.mdx +++ b/apps/docs/api-reference/endpoint/feedback/post-projects-feedback.mdx @@ -1,4 +1,4 @@ --- title: Create a Feedback -openapi: post /projects/{projectSlug}/feedback ---- \ No newline at end of file +openapi: post /projects/{workspaceSlug}/feedback +--- diff --git a/apps/docs/api-reference/endpoint/project-config/get-projects-config.mdx b/apps/docs/api-reference/endpoint/project-config/get-projects-config.mdx index f73c62a..4f5873a 100644 --- a/apps/docs/api-reference/endpoint/project-config/get-projects-config.mdx +++ b/apps/docs/api-reference/endpoint/project-config/get-projects-config.mdx @@ -1,4 +1,4 @@ --- title: Retrieve a Project Config -openapi: get /projects/{projectSlug}/config ---- \ No newline at end of file +openapi: get /projects/{workspaceSlug}/config +--- diff --git a/apps/docs/api-reference/endpoint/project-config/patch-projects-config.mdx b/apps/docs/api-reference/endpoint/project-config/patch-projects-config.mdx index b640c28..f2a52a8 100644 --- a/apps/docs/api-reference/endpoint/project-config/patch-projects-config.mdx +++ b/apps/docs/api-reference/endpoint/project-config/patch-projects-config.mdx @@ -1,4 +1,4 @@ --- title: Update a Project Config -openapi: patch /projects/{projectSlug}/config ---- \ No newline at end of file +openapi: patch /projects/{workspaceSlug}/config +--- diff --git a/apps/docs/api-reference/endpoint/project-invites/delete-projects-invites.mdx b/apps/docs/api-reference/endpoint/project-invites/delete-projects-invites.mdx index 1ad3972..1bffc1a 100644 --- a/apps/docs/api-reference/endpoint/project-invites/delete-projects-invites.mdx +++ b/apps/docs/api-reference/endpoint/project-invites/delete-projects-invites.mdx @@ -1,4 +1,4 @@ --- title: Delete a Project Invite -openapi: delete /projects/{projectSlug}/invites/{inviteId} ---- \ No newline at end of file +openapi: delete /projects/{workspaceSlug}/invites/{inviteId} +--- diff --git a/apps/docs/api-reference/endpoint/project-invites/get-projects-invites.mdx b/apps/docs/api-reference/endpoint/project-invites/get-projects-invites.mdx index ba0d58e..0b3a4a9 100644 --- a/apps/docs/api-reference/endpoint/project-invites/get-projects-invites.mdx +++ b/apps/docs/api-reference/endpoint/project-invites/get-projects-invites.mdx @@ -1,4 +1,4 @@ --- -title: "List Project Invites" -openapi: get /projects/{projectSlug}/invites ---- \ No newline at end of file +title: 'List Project Invites' +openapi: get /projects/{workspaceSlug}/invites +--- diff --git a/apps/docs/api-reference/endpoint/project-invites/post-projects-invites.mdx b/apps/docs/api-reference/endpoint/project-invites/post-projects-invites.mdx index 3bb3965..b418336 100644 --- a/apps/docs/api-reference/endpoint/project-invites/post-projects-invites.mdx +++ b/apps/docs/api-reference/endpoint/project-invites/post-projects-invites.mdx @@ -1,4 +1,4 @@ --- -title: "Create a Project Invite" -openapi: post /projects/{projectSlug}/invites ---- \ No newline at end of file +title: 'Create a Project Invite' +openapi: post /projects/{workspaceSlug}/invites +--- diff --git a/apps/docs/api-reference/endpoint/project-members/get-projects-members.mdx b/apps/docs/api-reference/endpoint/project-members/get-projects-members.mdx index da481bb..3216a8d 100644 --- a/apps/docs/api-reference/endpoint/project-members/get-projects-members.mdx +++ b/apps/docs/api-reference/endpoint/project-members/get-projects-members.mdx @@ -1,4 +1,4 @@ --- title: List Project Members -openapi: get /projects/{projectSlug}/members ---- \ No newline at end of file +openapi: get /projects/{workspaceSlug}/members +--- diff --git a/apps/docs/api-reference/endpoint/project/delete-projects.mdx b/apps/docs/api-reference/endpoint/project/delete-projects.mdx index 12698c7..a40ce40 100644 --- a/apps/docs/api-reference/endpoint/project/delete-projects.mdx +++ b/apps/docs/api-reference/endpoint/project/delete-projects.mdx @@ -1,4 +1,4 @@ --- title: Delete a Project -openapi: delete /projects/{projectSlug} ---- \ No newline at end of file +openapi: delete /projects/{workspaceSlug} +--- diff --git a/apps/docs/api-reference/endpoint/project/get-projects.mdx b/apps/docs/api-reference/endpoint/project/get-projects.mdx index 0890cc7..e2e4aed 100644 --- a/apps/docs/api-reference/endpoint/project/get-projects.mdx +++ b/apps/docs/api-reference/endpoint/project/get-projects.mdx @@ -1,4 +1,4 @@ --- title: Retrieve a Project -openapi: get /projects/{projectSlug} ---- \ No newline at end of file +openapi: get /projects/{workspaceSlug} +--- diff --git a/apps/docs/api-reference/endpoint/project/patch-projects.mdx b/apps/docs/api-reference/endpoint/project/patch-projects.mdx index 6e1a89e..b6aa2ed 100644 --- a/apps/docs/api-reference/endpoint/project/patch-projects.mdx +++ b/apps/docs/api-reference/endpoint/project/patch-projects.mdx @@ -1,4 +1,4 @@ --- title: Update a Project -openapi: patch /projects/{projectSlug} ---- \ No newline at end of file +openapi: patch /projects/{workspaceSlug} +--- diff --git a/apps/docs/api-reference/endpoint/public/get--atom.mdx b/apps/docs/api-reference/endpoint/public/get--atom.mdx index 0dff71a..c302201 100644 --- a/apps/docs/api-reference/endpoint/public/get--atom.mdx +++ b/apps/docs/api-reference/endpoint/public/get--atom.mdx @@ -1,4 +1,4 @@ --- title: Retrieve Project Atom Feed -openapi: get /{projectSlug}/atom ---- \ No newline at end of file +openapi: get /{workspaceSlug}/atom +--- diff --git a/apps/docs/api-reference/endpoint/public/get--changelogs.mdx b/apps/docs/api-reference/endpoint/public/get--changelogs.mdx index ef6d2e7..1cfde5b 100644 --- a/apps/docs/api-reference/endpoint/public/get--changelogs.mdx +++ b/apps/docs/api-reference/endpoint/public/get--changelogs.mdx @@ -1,4 +1,4 @@ --- title: List Project Changelogs -openapi: get /{projectSlug}/changelogs ---- \ No newline at end of file +openapi: get /{workspaceSlug}/changelogs +--- diff --git a/apps/docs/package.json b/apps/docs/package.json new file mode 100644 index 0000000..d5fce8d --- /dev/null +++ b/apps/docs/package.json @@ -0,0 +1,7 @@ +{ + "name": "docs", + "version": "0.1.0", + "scripts": { + "dev": "mintlify dev --port 3001" + } +} diff --git a/apps/web/.eslintrc.js b/apps/web/.eslintrc.js index df11621..473f095 100644 --- a/apps/web/.eslintrc.js +++ b/apps/web/.eslintrc.js @@ -1,7 +1,20 @@ module.exports = { root: true, - extends: ['custom/next'], + extends: ['@feedbase/eslint-config/next.js'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + }, + ignorePatterns: ['dist', '.eslintrc.js', 'postcss.config.js', 'tailwind.config.js'], + settings: { + 'import/resolver': { + typescript: { + project: ['packages/*/tsconfig.json', 'apps/*/tsconfig.json', 'tsconfig.json'], + }, + }, + }, globals: { Messages: 'readonly', + NodeJS: true, }, }; diff --git a/apps/web/app/[project]/feedback/[id]/loading.tsx b/apps/web/app/[project]/feedback/[id]/loading.tsx deleted file mode 100644 index 33a46c8..0000000 --- a/apps/web/app/[project]/feedback/[id]/loading.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { Separator } from '@ui/components/ui/separator'; -import { Skeleton } from '@ui/components/ui/skeleton'; - -export default function FeedbackPageLoading() { - return ( -
-
-
-
- -
-
-
-
-

- -

- -
- {[1, 2, 3, 4, 5, 6].map((index) => ( - - ))} -
- - {/* Info Mobile */} -
-
-
-

Upvotes

- -
- -
-

Status

- -
- -
-

Tags

-
- - -
-
-
- - - -
-
-

Created

- -
- -
-

Author

-
- - -
-
-
-
- - {/* Comment Input */} -
- -
- - {/* Comments */} -
-
-

Comments

- -
- - -
- -
- {[1, 2, 3].map((index) => ( -
-
-
-
- - - · - -
-
-
- -
-
- -
- -
- - -
- - - -
-
-
-
- ))} -
-
-
-
- -
-
-
-

Upvotes

- -
- -
-

Status

- -
- -
-

Tags

-
- - -
-
-
- - - -
-
-

Created

- -
- -
-

Author

-
- - -
-
-
-
-
- ); -} diff --git a/apps/web/app/[project]/feedback/loading.tsx b/apps/web/app/[project]/feedback/loading.tsx deleted file mode 100644 index 1936668..0000000 --- a/apps/web/app/[project]/feedback/loading.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Separator } from '@ui/components/ui/separator'; -import { Skeleton } from '@ui/components/ui/skeleton'; -import FeedbackHeader from '@/components/hub/feedback/button-header'; - -export default function FeedbackLoading() { - return ( -
-
-
-

Feedback

-

- Have a suggestion or found a bug? Let us know! -

-
-
- - {/* Separator */} - - - {/* Content */} -
- {' '} - {/* Provide placeholder values */} - {/* Main */} -
- {[1, 2, 3, 4, 5].map((index) => ( - - ))} -
-
-
- ); -} diff --git a/apps/web/app/[project]/feedback/page.tsx b/apps/web/app/[project]/feedback/page.tsx deleted file mode 100644 index 52c022a..0000000 --- a/apps/web/app/[project]/feedback/page.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Metadata } from 'next'; -import { notFound } from 'next/navigation'; -import { Separator } from '@ui/components/ui/separator'; -import { getProjectBySlug, getProjectConfigBySlug } from '@/lib/api/projects'; -import { getPublicProjectFeedback } from '@/lib/api/public'; -import { getCurrentUser } from '@/lib/api/user'; -import AnalyticsWrapper from '@/components/hub/analytics-wrapper'; -import FeedbackHeader from '@/components/hub/feedback/button-header'; -import FeedbackList from '@/components/hub/feedback/feedback-list'; - -type Props = { - params: { project: string }; -}; - -// Metadata -export async function generateMetadata({ params }: Props): Promise { - // Get project - const { data: project, error } = await getProjectBySlug(params.project, 'server', true, false); - - // If project is undefined redirects to 404 - if (error?.status === 404 || !project) { - notFound(); - } - - return { - title: `Feedback - ${project.name}`, - description: 'Have a suggestion or found a bug? Let us know!', - }; -} - -export default async function Feedback({ params }: Props) { - // Get current user - const { data: user } = await getCurrentUser('server'); - - const { data: feedback, error } = await getPublicProjectFeedback(params.project, 'server', true, false); - - if (error) { - return
{error.message}
; - } - - // Fetch project config if user not logged in - const { data: config } = await getProjectConfigBySlug(params.project, 'server', true, false); - - return ( - - {/* Header */} -
-
-

Feedback

-

- Have a suggestion or found a bug? Let us know! -

-
-
- - {/* Seperator */} - - - {/* content */} -
- - - {/* Main */} -
- -
-
-
- ); -} diff --git a/apps/web/app/[project]/layout.tsx b/apps/web/app/[project]/layout.tsx deleted file mode 100644 index 76d5117..0000000 --- a/apps/web/app/[project]/layout.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { Metadata } from 'next'; -import { headers } from 'next/headers'; -import { notFound, redirect } from 'next/navigation'; -import { Separator } from 'ui/components/ui/separator'; -import { getProjectBySlug, getProjectConfigBySlug } from '@/lib/api/projects'; -import { getCurrentUser } from '@/lib/api/user'; -import Header from '@/components/hub/nav-bar'; -import CustomThemeWrapper from '@/components/hub/theme-wrapper'; -import { ThemeProvider as NextThemeProvider } from '@/components/theme-provider'; - -type Props = { - children: React.ReactNode; - params: { project: string }; -}; - -// Metadata -export async function generateMetadata({ params }: Props): Promise { - // Get project - const { data: project, error } = await getProjectBySlug(params.project, 'server', true, false); - - // If project is undefined redirects to 404 - if (error?.status === 404 || !project) { - notFound(); - } - - return { - title: project.name, - description: `Discover the latest updates, roadmaps, submit feedback, and explore more about ${project.name}.`, - icons: project.icon, - openGraph: { - images: [ - { - url: project.og_image || '', - width: 1200, - height: 600, - alt: project.name, - }, - ], - }, - }; -} - -const tabs = [ - { - name: 'Feedback', - link: '/feedback', - }, - { - name: 'Changelog', - link: '/changelog', - }, -]; - -export default async function HubLayout({ children, params }: Props) { - const headerList = headers(); - const pathname = headerList.get('x-pathname'); - const hostname = headerList.get('host'); - const currentTab = tabs.find((tab) => tab.link === `/${pathname!.split('/')[1]}`); - - if (!currentTab) { - redirect('/feedback'); - } - - // Get project data - const { data: project, error } = await getProjectBySlug(params.project, 'server', true, false); - - if (error?.status === 404 || !project) { - notFound(); - } - - // Get project config - const { data: config } = await getProjectConfigBySlug(params.project, 'server', true, false); - - if (!config) { - notFound(); - } - - // Check if custom domain is set and redirect to it - if (config.custom_domain && config.custom_domain_verified && hostname !== config.custom_domain) { - redirect(`https://${config.custom_domain}`); - } - - // Check if any modules are disabled and remove them from the tabs - if (!config.changelog_enabled) { - tabs.splice(1, 1); - } - - // Get current user - const { data: user } = await getCurrentUser('server'); - - return ( - - - {/* Header */} -
- {/* Header */} -
- - {/* Separator with max screen width */} - - - {/* Main content */} -
- {children} -
-
-
- - {/* Powered by */} - {/* TODO: Improve */} - {/*
- -
*/} -
- ); -} diff --git a/apps/web/app/[project]/changelog/[id]/loading.tsx b/apps/web/app/[workspace]/changelog/[id]/loading.tsx similarity index 92% rename from apps/web/app/[project]/changelog/[id]/loading.tsx rename to apps/web/app/[workspace]/changelog/[id]/loading.tsx index 5e0a6d5..b41bf1c 100644 --- a/apps/web/app/[project]/changelog/[id]/loading.tsx +++ b/apps/web/app/[workspace]/changelog/[id]/loading.tsx @@ -1,5 +1,5 @@ -import { Separator } from '@ui/components/ui/separator'; -import { Skeleton } from '@ui/components/ui/skeleton'; +import { Separator } from '@feedbase/ui/components/separator'; +import { Skeleton } from '@feedbase/ui/components/skeleton'; export default function ChangelogPageLoading() { return ( @@ -34,7 +34,7 @@ export default function ChangelogPageLoading() {

-

+

diff --git a/apps/web/app/[project]/changelog/[id]/page.tsx b/apps/web/app/[workspace]/changelog/[id]/page.tsx similarity index 80% rename from apps/web/app/[project]/changelog/[id]/page.tsx rename to apps/web/app/[workspace]/changelog/[id]/page.tsx index f8ef938..f6ed9da 100644 --- a/apps/web/app/[project]/changelog/[id]/page.tsx +++ b/apps/web/app/[workspace]/changelog/[id]/page.tsx @@ -2,23 +2,28 @@ import type { Metadata } from 'next'; import Image from 'next/image'; import Link from 'next/link'; import { notFound } from 'next/navigation'; -import { Separator } from '@ui/components/ui/separator'; -import { cn } from '@ui/lib/utils'; -import { fontMono } from '@ui/styles/fonts'; -import { Avatar, AvatarFallback, AvatarImage } from 'ui/components/ui/avatar'; -import { getPublicProjectChangelogs } from '@/lib/api/public'; +import { Avatar, AvatarFallback, AvatarImage } from '@feedbase/ui/components/avatar'; +import { Separator } from '@feedbase/ui/components/separator'; +import { cn } from '@feedbase/ui/lib/utils'; +import { fontMono } from '@feedbase/ui/styles/fonts'; +import { getPublicWorkspaceChangelogs } from '@/lib/api/public'; import { formatRootUrl } from '@/lib/utils'; -import AnalyticsWrapper from '@/components/hub/analytics-wrapper'; +import AnalyticsWrapper from '@/components/shared/analytics-wrapper'; import { Icons } from '@/components/shared/icons/icons-static'; type Props = { - params: { project: string; id: string }; + params: { workspace: string; id: string }; }; // Metadata export async function generateMetadata({ params }: Props): Promise { // Get changelogs - const { data: changelogs, error } = await getPublicProjectChangelogs(params.project, 'server', true, false); + const { data: changelogs, error } = await getPublicWorkspaceChangelogs( + params.workspace, + 'server', + true, + false + ); if (error?.status === 404 || !changelogs) { notFound(); @@ -38,7 +43,7 @@ export async function generateMetadata({ params }: Props): Promise { openGraph: { images: [ { - url: changelog.image || '', + url: changelog.thumbnail || '', width: 1200, height: 600, alt: changelog.title, @@ -50,7 +55,12 @@ export async function generateMetadata({ params }: Props): Promise { export default async function ChangelogPage({ params }: Props) { // Get changelogs - const { data: changelogs, error } = await getPublicProjectChangelogs(params.project, 'server', true, false); + const { data: changelogs, error } = await getPublicWorkspaceChangelogs( + params.workspace, + 'server', + true, + false + ); // If error.status redirects to 404 if (error?.status === 404 || !changelogs) { @@ -73,7 +83,7 @@ export default async function ChangelogPage({ params }: Props) { } return ( - + {/* // Row Splitting up date and Content */}
-

+

← Back to Changelog

@@ -91,7 +101,7 @@ export default async function ChangelogPage({ params }: Props) { {/* Content */}
-

+

{changelog.author.full_name}

-

+

LUM-32 dangerouslySetInnerHTML={{ __html: changelog.content! }} /> @@ -178,7 +188,7 @@ export default async function ChangelogPage({ params }: Props) {
+ className='text-foreground/60 hover:text-foreground w-full text-sm transition-colors'> ← {changelogs[changelogIndex - 1]?.title}
@@ -189,7 +199,7 @@ export default async function ChangelogPage({ params }: Props) {
+ className='text-foreground/60 hover:text-foreground w-full text-sm transition-colors'> {changelogs[changelogIndex + 1]?.title} →
diff --git a/apps/web/app/[project]/changelog/loading.tsx b/apps/web/app/[workspace]/changelog/loading.tsx similarity index 92% rename from apps/web/app/[project]/changelog/loading.tsx rename to apps/web/app/[workspace]/changelog/loading.tsx index e60375d..f71b560 100644 --- a/apps/web/app/[project]/changelog/loading.tsx +++ b/apps/web/app/[workspace]/changelog/loading.tsx @@ -1,5 +1,5 @@ -import { Separator } from '@ui/components/ui/separator'; -import { Skeleton } from '@ui/components/ui/skeleton'; +import { Separator } from '@feedbase/ui/components/separator'; +import { Skeleton } from '@feedbase/ui/components/skeleton'; export default function ChangelogLoading() { return ( @@ -9,7 +9,7 @@ export default function ChangelogLoading() {

-

+

diff --git a/apps/web/app/[project]/changelog/page.tsx b/apps/web/app/[workspace]/changelog/page.tsx similarity index 52% rename from apps/web/app/[project]/changelog/page.tsx rename to apps/web/app/[workspace]/changelog/page.tsx index 0e56224..cbfdb5e 100644 --- a/apps/web/app/[project]/changelog/page.tsx +++ b/apps/web/app/[workspace]/changelog/page.tsx @@ -2,36 +2,42 @@ import { Metadata } from 'next'; import Image from 'next/image'; import Link from 'next/link'; import { notFound } from 'next/navigation'; -import { fontMono } from '@ui/styles/fonts'; -import { Separator } from 'ui/components/ui/separator'; -import { getProjectBySlug, getProjectConfigBySlug } from '@/lib/api/projects'; -import { getPublicProjectChangelogs } from '@/lib/api/public'; -import AnalyticsWrapper from '@/components/hub/analytics-wrapper'; -import SubscribeToEmailUpdates from '@/components/hub/modals/subscribe-email-modal'; +import { Separator } from '@feedbase/ui/components/separator'; +import { fontMono } from '@feedbase/ui/styles/fonts'; +import { getWorkspaceModuleConfig } from '@/lib/api/module'; +import { getPublicWorkspaceChangelogs } from '@/lib/api/public'; +import { getWorkspaceBySlug } from '@/lib/api/workspace'; +import SubscribeToEmailUpdates from '@/components/modals/subscribe-email-modal'; +import AnalyticsWrapper from '@/components/shared/analytics-wrapper'; type Props = { - params: { project: string }; + params: { workspace: string }; }; // Metadata export async function generateMetadata({ params }: Props): Promise { - // Get project - const { data: project, error } = await getProjectBySlug(params.project, 'server', true, false); + // Get workspace + const { data: workspace, error } = await getWorkspaceBySlug(params.workspace, 'server', true, false); - // If project is undefined redirects to 404 - if (error?.status === 404 || !project) { + // If workspace is undefined redirects to 404 + if (error?.status === 404 || !workspace) { notFound(); } return { - title: `Changelog - ${project.name}`, - description: `All the latest updates, improvements, and fixes to ${project.name}.`, + title: `Changelog - ${workspace.name}`, + description: `All the latest updates, improvements, and fixes to ${workspace.name}.`, }; } export default async function Changelogs({ params }: Props) { // Get changelogs - const { data: changelogs, error } = await getPublicProjectChangelogs(params.project, 'server', true, false); + const { data: changelogs, error } = await getPublicWorkspaceChangelogs( + params.workspace, + 'server', + true, + false + ); // If error.status redirects to 404 if (error?.status === 404 || !changelogs) { @@ -40,82 +46,82 @@ export default async function Changelogs({ params }: Props) { // Sort changelogs by publish_date (newest first) changelogs.sort((a, b) => { - return new Date(b.publish_date!).getTime() - new Date(a.publish_date!).getTime(); + return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); }); - // Get project config - const { data: projectConfig, error: projectConfigError } = await getProjectConfigBySlug( - params.project, + // Get workspace config + const { data: workspaceConfig, error: workspaceConfigError } = await getWorkspaceModuleConfig( + params.workspace, 'server', true, false ); // If error.status redirects to 404 - if (projectConfigError?.status === 404 || !projectConfig) { + if (workspaceConfigError?.status === 404 || !workspaceConfig) { notFound(); } - // Get project - const { data: project, error: projectError } = await getProjectBySlug( - params.project, + // Get workspace + const { data: workspace, error: workspaceError } = await getWorkspaceBySlug( + params.workspace, 'server', true, false ); - // If project is undefined redirects to 404 - if (projectError?.status === 404 || !project) { + // If workspace is undefined redirects to 404 + if (workspaceError?.status === 404 || !workspace) { notFound(); } return ( - -
-
-

Changelog

-

- All the latest updates, improvements, and fixes to {project.name}. + +

+ {/* Title, Description */} +
+

Changelog

+

+ All the latest updates, improvements, and fixes to {workspace.name}.{' '}

+
- {/* Buttons */} -
- {/* Email */} - - - - - · - - {/* Twitter */} - {projectConfig.changelog_twitter_handle !== null && - projectConfig.changelog_twitter_handle !== '' && ( -
- - Follow us on Twitter - - - · -
- )} - - {/* RRS Update Feed */} - + {/* Email */} + +
+ Subscribe to Updates + + + + · + + {/* Twitter */} + {workspaceConfig?.changelog_twitter_handle ? ( +
+ + Follow us on Twitter + + + · +
+ ) : null} + + {/* RRS Update Feed */} + + Subscribe to Atom Feed +
@@ -134,9 +140,9 @@ export default async function Changelogs({ params }: Props) { {/* Date */}
-

-

+

+ {workspaceConfig.changelog_preview_style === 'summary' && ( +

{changelog.summary}

)} - {projectConfig.changelog_preview_style === 'content' && ( + {workspaceConfig.changelog_preview_style === 'content' && (
)} @@ -198,8 +204,8 @@ export default async function Changelogs({ params }: Props) { {/* Empty State */} {changelogs.length === 0 && (
-

No changelogs yet

-

+

No changelogs yet

+

The latest updates, improvements, and fixes will be posted here. Stay tuned!

diff --git a/apps/web/app/[project]/changelog/unsubscribe/page.tsx b/apps/web/app/[workspace]/changelog/unsubscribe/page.tsx similarity index 54% rename from apps/web/app/[project]/changelog/unsubscribe/page.tsx rename to apps/web/app/[workspace]/changelog/unsubscribe/page.tsx index a6af8c2..e6d305c 100644 --- a/apps/web/app/[project]/changelog/unsubscribe/page.tsx +++ b/apps/web/app/[workspace]/changelog/unsubscribe/page.tsx @@ -1,12 +1,12 @@ import { notFound, redirect } from 'next/navigation'; -import { getProjectBySlug } from '@/lib/api/projects'; -import UnsubscribeChangelogCard from '@/components/layout/unsubscribe-card'; +import { getWorkspaceBySlug } from '@/lib/api/workspace'; +import UnsubscribeChangelogCard from '@/components/changelog/unsubscribe-card'; export default async function ChangelogUnsubscribe({ params, searchParams, }: { - params: { project: string }; + params: { workspace: string }; searchParams: { subId: string }; }) { if (!searchParams.subId) { @@ -20,17 +20,17 @@ export default async function ChangelogUnsubscribe({ redirect('/'); } - // Get project - const { data: project, error } = await getProjectBySlug(params.project, 'server', true, false); + // Get workspace + const { data: workspace, error } = await getWorkspaceBySlug(params.workspace, 'server', true, false); - // If project is undefined redirects to 404 - if (error?.status === 404 || !project) { + // If workspace is undefined redirects to 404 + if (error?.status === 404 || !workspace) { notFound(); } return (
- +
); } diff --git a/apps/web/app/[project]/feedback/[id]/page.tsx b/apps/web/app/[workspace]/feedback/[id]/page.tsx similarity index 70% rename from apps/web/app/[project]/feedback/[id]/page.tsx rename to apps/web/app/[workspace]/feedback/[id]/page.tsx index 54b8bd6..0a3d013 100644 --- a/apps/web/app/[project]/feedback/[id]/page.tsx +++ b/apps/web/app/[workspace]/feedback/[id]/page.tsx @@ -1,27 +1,31 @@ import { Metadata } from 'next'; import Link from 'next/link'; import { notFound } from 'next/navigation'; -import { Avatar, AvatarFallback, AvatarImage } from '@ui/components/ui/avatar'; -import { Separator } from '@ui/components/ui/separator'; -import { cn } from '@ui/lib/utils'; -import { BadgeCheck, CheckCircle2, CircleDashed, CircleDot, CircleDotDashed, XCircle } from 'lucide-react'; -import { getCommentsForFeedbackById } from '@/lib/api/comments'; -import { getPublicProjectFeedback } from '@/lib/api/public'; +import { Avatar, AvatarFallback, AvatarImage } from '@feedbase/ui/components/avatar'; +import { Separator } from '@feedbase/ui/components/separator'; +import { cn } from '@feedbase/ui/lib/utils'; +import { BadgeCheck } from 'lucide-react'; +import { getPublicWorkspaceFeedback } from '@/lib/api/public'; import { getCurrentUser } from '@/lib/api/user'; -import { PROSE_CN } from '@/lib/constants'; -import AnalyticsWrapper from '@/components/hub/analytics-wrapper'; -import CommentsList from '@/components/hub/feedback/comments/comments-list'; +import { PROSE_CN, STATUS_OPTIONS } from '@/lib/constants'; +import CommentsList from '@/components/feedback/hub/comments-list'; +import AnalyticsWrapper from '@/components/shared/analytics-wrapper'; type Props = { - params: { project: string; id: string }; + params: { workspace: string; id: string }; }; // Metadata export async function generateMetadata({ params }: Props): Promise { // Get feedback - const { data: feedbackList, error } = await getPublicProjectFeedback(params.project, 'server', true, false); + const { data: feedbackList, error } = await getPublicWorkspaceFeedback( + params.workspace, + 'server', + true, + false + ); - // If project is undefined redirects to 404 + // If workspace is undefined redirects to 404 if (error?.status === 404 || !feedbackList) { notFound(); } @@ -36,36 +40,17 @@ export async function generateMetadata({ params }: Props): Promise { return { title: feedback.title, - description: feedback.description, + description: feedback.content, }; } -// Status options -const statusOptions = [ - { - label: 'Backlog', - icon: CircleDashed, - }, - { - label: 'Planned', - icon: CircleDotDashed, - }, - { - label: 'In Progress', - icon: CircleDot, - }, - { - label: 'Completed', - icon: CheckCircle2, - }, - { - label: 'Rejected', - icon: XCircle, - }, -]; - export default async function FeedbackDetails({ params }: Props) { - const { data: feedbackList, error } = await getPublicProjectFeedback(params.project, 'server', true, false); + const { data: feedbackList, error } = await getPublicWorkspaceFeedback( + params.workspace, + 'server', + true, + false + ); if (error || !feedbackList) { return
{error.message}
; @@ -79,31 +64,19 @@ export default async function FeedbackDetails({ params }: Props) { notFound(); } - // Get comments - const { data: comments, error: commentsError } = await getCommentsForFeedbackById( - params.id, - params.project, - 'server', - false - ); - - if (commentsError) { - return
{commentsError.message}
; - } - // Get current user const { data: user } = await getCurrentUser('server'); return ( - + {/* // Row Splitting up date and Content */}
{/* Back Button */}
- -

+ +

← Back to Posts

@@ -117,8 +90,8 @@ export default async function FeedbackDetails({ params }: Props) {

{feedback.title}

@@ -128,38 +101,36 @@ export default async function FeedbackDetails({ params }: Props) {
{/* Upvotes */}
-

Upvotes

+

Upvotes

{/* Upvotes */} - {feedback.upvotes} + {feedback.upvotes}
{/* Status */}
-

Status

+

Status

{(() => { if (feedback.status) { const currentStatus = - statusOptions.find( + STATUS_OPTIONS.find( (option) => option.label.toLowerCase() === feedback.status?.toLowerCase() - ) || statusOptions[0]; + ) || STATUS_OPTIONS[0]; return ( -
+
- - {currentStatus.label} - + {currentStatus.label}
); } - return No status; + return No status; })()}
{/* Tags */}
-

Tags

+

Tags

{/* Grid with all tags */}
@@ -171,7 +142,7 @@ export default async function FeedbackDetails({ params }: Props) { {/* Tag color */}
{/* Tag name */} -
+
{tag.name}
@@ -181,7 +152,7 @@ export default async function FeedbackDetails({ params }: Props) { {/* Empty State */} {(!feedback.tags || feedback.tags.length === 0) && ( - No tags + No tags )}
@@ -193,9 +164,9 @@ export default async function FeedbackDetails({ params }: Props) {
{/* Created */}
-

Created

+

Created

-

+

{new Date(feedback.created_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', @@ -206,17 +177,15 @@ export default async function FeedbackDetails({ params }: Props) { {/* Author */}

-

Author

+

Author

{/* Author */} -
+
{/* User */}
- - {feedback.user.full_name[0]} - + {feedback.user.full_name[0]} {/* If team member, add small verified badge to top of profile picture */} {feedback.user.isTeamMember ? (
@@ -226,19 +195,14 @@ export default async function FeedbackDetails({ params }: Props) {
{/* Name */} - {feedback.user.full_name} + {feedback.user.full_name}
{/* Comments */} - +
@@ -248,36 +212,36 @@ export default async function FeedbackDetails({ params }: Props) {
{/* Upvotes */}
-

Upvotes

+

Upvotes

{/* Upvotes */} - {feedback.upvotes} + {feedback.upvotes}
{/* Status */}
-

Status

+

Status

{(() => { if (feedback.status) { const currentStatus = - statusOptions.find( + STATUS_OPTIONS.find( (option) => option.label.toLowerCase() === feedback.status?.toLowerCase() - ) || statusOptions[0]; + ) || STATUS_OPTIONS[0]; return ( -
+
- {currentStatus.label} + {currentStatus.label}
); } - return No status; + return No status; })()}
{/* Tags */}
-

Tags

+

Tags

{/* Grid with all tags */}
@@ -289,7 +253,7 @@ export default async function FeedbackDetails({ params }: Props) { {/* Tag color */}
{/* Tag name */} -
+
{tag.name}
@@ -299,7 +263,7 @@ export default async function FeedbackDetails({ params }: Props) { {/* Empty State */} {(!feedback.tags || feedback.tags.length === 0) && ( - No tags + No tags )}
@@ -311,9 +275,9 @@ export default async function FeedbackDetails({ params }: Props) {
{/* Created */}
-

Created

+

Created

-

+

{new Date(feedback.created_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', @@ -324,17 +288,15 @@ export default async function FeedbackDetails({ params }: Props) { {/* Author */}

-

Author

+

Author

{/* Author */} -
+
{/* User */}
- - {feedback.user.full_name[0]} - + {feedback.user.full_name[0]} {/* If team member, add small verified badge to top of profile picture */} {feedback.user.isTeamMember ? (
@@ -345,7 +307,7 @@ export default async function FeedbackDetails({ params }: Props) { {/* Name */} - {feedback.user.full_name} + {feedback.user.full_name}
diff --git a/apps/web/app/[project]/page.tsx b/apps/web/app/[workspace]/feedback/page.tsx similarity index 73% rename from apps/web/app/[project]/page.tsx rename to apps/web/app/[workspace]/feedback/page.tsx index c6f6f88..2c59ed8 100644 --- a/apps/web/app/[project]/page.tsx +++ b/apps/web/app/[workspace]/feedback/page.tsx @@ -1,7 +1,7 @@ import { redirect } from 'next/navigation'; -export default async function Hub() { +export default async function Feedback() { // Redirect to changelog // This page doesn't really get called as this is catched in layout.tsx but still needed to cause 404 - redirect(`/feedback`); + redirect(`/`); } diff --git a/apps/web/app/[workspace]/layout.tsx b/apps/web/app/[workspace]/layout.tsx new file mode 100644 index 0000000..da4072e --- /dev/null +++ b/apps/web/app/[workspace]/layout.tsx @@ -0,0 +1,160 @@ +import { Metadata } from 'next'; +import { headers } from 'next/headers'; +import { notFound, redirect } from 'next/navigation'; +import { Separator } from '@feedbase/ui/components/separator'; +import { getWorkspaceBoards } from '@/lib/api/boards'; +import { getWorkspaceModuleConfig } from '@/lib/api/module'; +import { getWorkspaceTheme } from '@/lib/api/theme'; +import { getCurrentUser } from '@/lib/api/user'; +import { getWorkspaceBySlug } from '@/lib/api/workspace'; +import Header from '@/components/layout/nav-bar'; +import CustomThemeWrapper from '@/components/layout/theme-wrapper'; +import { ThemeProvider as NextThemeProvider } from '@/components/theme-provider'; + +type Props = { + children: React.ReactNode; + params: { workspace: string }; + searchParams: Record; +}; + +// Metadata +export async function generateMetadata({ params }: Props): Promise { + // Get workspace + const { data: workspace, error } = await getWorkspaceBySlug(params.workspace, 'server', true, false); + + // If workspace is undefined redirects to 404 + if (error?.status === 404 || !workspace) { + notFound(); + } + + return { + title: workspace.name, + description: `Discover the latest updates, roadmaps, submit feedback, and explore more about ${workspace.name}.`, + icons: workspace.icon, + openGraph: { + images: [ + { + url: workspace.opengraph_image || '', + width: 1200, + height: 600, + alt: workspace.name, + }, + ], + }, + }; +} + +const tabs: { name: string; link: string; items?: { name: string; link: string }[] }[] = [ + { + name: 'Boards', + link: '/', + items: [], + }, + { + name: 'Roadmap', + link: '/roadmap', + }, + { + name: 'Changelog', + link: '/changelog', + }, +]; + +export default async function HubLayout({ children, params, searchParams }: Props) { + const headerList = headers(); + const pathname = headerList.get('x-pathname'); + const hostname = headerList.get('host'); + + // Get workspace data + const { data: workspace, error } = await getWorkspaceBySlug(params.workspace, 'server', true, false); + + if (error?.status === 404 || !workspace) { + notFound(); + } + + // Get workspace boards + const { data: boards, error: boardsError } = await getWorkspaceBoards( + params.workspace, + 'server', + true, + false + ); + + if (boardsError || !boards) { + notFound(); + } + + // Set workspace boards to tabs + tabs[0].items = boards.map((board) => ({ + name: board.name, + link: `/board/${board.name.toLowerCase().replace(/\s+/g, '-')}`, + })); + + // Get workspace config + const { data: config } = await getWorkspaceModuleConfig(params.workspace, 'server', true, false); + + // Get workspace theme + const { data: workspaceTheme } = await getWorkspaceTheme(params.workspace, 'server', true, false); + + if (!config || !workspaceTheme) { + notFound(); + } + + // Get current user + const { data: user } = await getCurrentUser('server'); + + // Check if custom domain is set and redirect to it + if (workspace.custom_domain && workspace.custom_domain_verified && hostname !== workspace.custom_domain) { + // Validate redirect rules + switch (workspace.custom_domain_redirect) { + case 'root_redirect': + return redirect(`https://${workspace.custom_domain}`); + case 'direct_redirect': + return redirect(`https://${workspace.custom_domain}${pathname}`); + case 'no_redirect': + break; + } + } + + // Get current tab + const currentTab = tabs.find((tab) => { + if (tab.link === pathname) { + return true; + } + if (tab.items) { + const subItem = tab.items.find((item) => item.link === pathname); + if (subItem) { + // Return the subitem directly + return subItem; + } + } + return false; + }); + + // Extract subItem if found, otherwise use the main tab + const foundItem = currentTab?.items?.find((item) => item.link === pathname) || currentTab; + + // Check if any modules are disabled and remove them from the tabs + if (!config.changelog_enabled) { + tabs.splice(1, 1); + } + + return ( + // +
+ {/* Header */} +
+ + {/* Main content */} +
+ {children} +
+
+ ); +} diff --git a/apps/web/app/[workspace]/page.tsx b/apps/web/app/[workspace]/page.tsx new file mode 100644 index 0000000..ff32044 --- /dev/null +++ b/apps/web/app/[workspace]/page.tsx @@ -0,0 +1,98 @@ +import { Metadata } from 'next'; +import { headers } from 'next/headers'; +import { notFound } from 'next/navigation'; +import { Separator } from '@feedbase/ui/components/separator'; +import { getWorkspaceBoards } from '@/lib/api/boards'; +import { getWorkspaceModuleConfig } from '@/lib/api/module'; +import { getCurrentUser } from '@/lib/api/user'; +import { getWorkspaceBySlug } from '@/lib/api/workspace'; +import FeedbackBoardList from '@/components/feedback/hub/board-list'; +import FeedbackHeader from '@/components/feedback/hub/button-header'; +import FeedbackList from '@/components/feedback/hub/feedback-list'; +import AnalyticsWrapper from '@/components/shared/analytics-wrapper'; + +type Props = { + params: { workspace: string }; +}; + +// Metadata +export async function generateMetadata({ params }: Props): Promise { + // Get workspace + const { data: workspace, error } = await getWorkspaceBySlug(params.workspace, 'server', true, false); + + // If workspace is undefined redirects to 404 + if (error?.status === 404 || !workspace) { + notFound(); + } + + return { + title: `Feedback - ${workspace.name}`, + description: 'Have a suggestion or found a bug? Let us know!', + }; +} + +export default async function Feedback({ params }: Props) { + const headerList = headers(); + const pathname = headerList.get('x-pathname'); + + // Get current user + const { data: user } = await getCurrentUser('server'); + + // Fetch feedback boards + const { data: boards, error: boardError } = await getWorkspaceBoards( + params.workspace, + 'server', + true, + false + ); + + if (boardError) { + return
{boardError.message}
; + } + + // Get workspace module config + const { data: moduleConfig, error: moduleError } = await getWorkspaceModuleConfig( + params.workspace, + 'server', + true, + false + ); + + if (moduleError) { + return
{moduleError.message}
; + } + + // Search for the initial board mathing the pathname by /board/board-name format + const initialBoard = boards.find( + (board) => pathname?.includes(`/board/${board.name.toLowerCase().replace(/\s+/g, '-')}`) + ); + + return ( + + {/* Title, Description */} +
+
+

Feedback

+

+ Have a suggestion or found a bug? Let us know! +

+
+
+ + {/* Seperator */} + + +
+ {/* Filter Header, Feedback List */} +
+ {/* Filter Header */} + + {/* Feedback List */} + +
+ {/* Boards */} + +
+
+ ); +} diff --git a/apps/web/app/[workspace]/roadmap/page.tsx b/apps/web/app/[workspace]/roadmap/page.tsx new file mode 100644 index 0000000..3648c14 --- /dev/null +++ b/apps/web/app/[workspace]/roadmap/page.tsx @@ -0,0 +1,69 @@ +import { Metadata } from 'next'; +import { headers } from 'next/headers'; +import { notFound } from 'next/navigation'; +import { Separator } from '@feedbase/ui/components/separator'; +import { getWorkspaceModuleConfig } from '@/lib/api/module'; +import { getCurrentUser } from '@/lib/api/user'; +import { getWorkspaceBySlug } from '@/lib/api/workspace'; +import Roadmap from '@/components/roadmap/hub/roadmap'; +import AnalyticsWrapper from '@/components/shared/analytics-wrapper'; + +type Props = { + params: { workspace: string }; +}; + +// Metadata +export async function generateMetadata({ params }: Props): Promise { + // Get workspace + const { data: workspace, error } = await getWorkspaceBySlug(params.workspace, 'server', true, false); + + // If workspace is undefined redirects to 404 + if (error?.status === 404 || !workspace) { + notFound(); + } + + return { + title: `Roadmap - ${workspace.name}`, + description: 'Have a suggestion or found a bug? Let us know!', + }; +} + +export default async function Feedback({ params }: Props) { + const headerList = headers(); + const pathname = headerList.get('x-pathname'); + + // Get current user + const { data: user } = await getCurrentUser('server'); + + // Get workspace module config + const { data: moduleConfig, error: moduleError } = await getWorkspaceModuleConfig( + params.workspace, + 'server', + true, + false + ); + + if (moduleError) { + return
{moduleError.message}
; + } + + return ( + + {/* Title, Description */} +
+
+

Roadmap

+

+ View what we're working on and what's coming next. +

+
+
+ + {/* Seperator */} + + + {/* Roadmap */} + +
+ ); +} diff --git a/apps/web/app/api/trigger/route.ts b/apps/web/app/api/trigger/route.ts new file mode 100644 index 0000000..0b08117 --- /dev/null +++ b/apps/web/app/api/trigger/route.ts @@ -0,0 +1,9 @@ +import { client } from '@feedbase/triggers'; +import { createAppRoute } from '@trigger.dev/nextjs'; +import '@feedbase/triggers/jobs'; + +//this route is used to send and receive data with Trigger.dev +export const { POST, dynamic } = createAppRoute(client); + +//uncomment this to set a higher max duration (it must be inside your plan limits). Full docs: https://vercel.com/docs/functions/serverless-functions/runtimes#max-duration +export const maxDuration = 60; diff --git a/apps/web/app/api/v1/[slug]/atom/route.ts b/apps/web/app/api/v1/[slug]/atom/route.ts index 1218c5b..198eaeb 100644 --- a/apps/web/app/api/v1/[slug]/atom/route.ts +++ b/apps/web/app/api/v1/[slug]/atom/route.ts @@ -1,20 +1,20 @@ import { NextResponse } from 'next/server'; -import { getProjectBySlug } from '@/lib/api/projects'; -import { getPublicProjectChangelogs } from '@/lib/api/public'; +import { getPublicWorkspaceChangelogs } from '@/lib/api/public'; +import { getWorkspaceBySlug } from '@/lib/api/workspace'; /* - Generate atom feed for project changelog + Generate atom feed for workspace changelog */ export async function GET(req: Request, context: { params: { slug: string } }) { - // Get project data - const { data: project, error } = await getProjectBySlug(context.params.slug, 'route', true, false); + // Get workspace data + const { data: workspace, error } = await getWorkspaceBySlug(context.params.slug, 'route', true, false); // If any errors thrown, return error if (error) { return NextResponse.json({ error: error.message }, { status: error.status }); } - const { data: changelogs, error: changelogError } = await getPublicProjectChangelogs( + const { data: changelogs, error: changelogError } = await getPublicWorkspaceChangelogs( context.params.slug, 'route', true, @@ -30,12 +30,12 @@ export async function GET(req: Request, context: { params: { slug: string } }) { return new Response( ` - ${project.name} Changelog - ${project.name}'s Changelog + ${workspace.name} Changelog + ${workspace.name}'s Changelog ${changelogs[0].publish_date} - ${project.id}${changelogs + ${workspace.id}${changelogs .map((post) => { return ` diff --git a/apps/web/app/api/v1/[slug]/changelogs/route.ts b/apps/web/app/api/v1/[slug]/changelogs/route.ts index 8650092..1f46ed6 100644 --- a/apps/web/app/api/v1/[slug]/changelogs/route.ts +++ b/apps/web/app/api/v1/[slug]/changelogs/route.ts @@ -1,12 +1,12 @@ import { NextResponse } from 'next/server'; -import { getPublicProjectChangelogs } from '@/lib/api/public'; +import { getPublicWorkspaceChangelogs } from '@/lib/api/public'; /* - Get project changelogs - GET /api/v1/projects/[slug]/changelogs + Get workspace changelogs + GET /api/v1/workspaces/[slug]/changelogs */ export async function GET(req: Request, context: { params: { slug: string } }) { - const { data: changelogs, error } = await getPublicProjectChangelogs( + const { data: changelogs, error } = await getPublicWorkspaceChangelogs( context.params.slug, 'route', true, diff --git a/apps/web/app/api/v1/[slug]/changelogs/subscribers/route.ts b/apps/web/app/api/v1/[slug]/changelogs/subscribers/route.ts index f225b75..0f96984 100644 --- a/apps/web/app/api/v1/[slug]/changelogs/subscribers/route.ts +++ b/apps/web/app/api/v1/[slug]/changelogs/subscribers/route.ts @@ -1,8 +1,8 @@ import { NextResponse } from 'next/server'; -import { subscribeToProjectChangelogs, unsubscribeFromProjectChangelogs } from '@/lib/api/public'; +import { subscribeToWorkspaceChangelogs, unsubscribeFromWorkspaceChangelogs } from '@/lib/api/public'; /* - Subscribe to project changelogs + Subscribe to workspace changelogs POST /api/v1/:slug/changelogs/subscribers { email: string, @@ -16,8 +16,8 @@ export async function POST(req: Request, context: { params: { slug: string } }) return NextResponse.json({ error: 'email is required.' }, { status: 400 }); } - // Subscribe to project changelogs - const { data: subscriber, error } = await subscribeToProjectChangelogs(context.params.slug, email); + // Subscribe to workspace changelogs + const { data: subscriber, error } = await subscribeToWorkspaceChangelogs(context.params.slug, email); // If any errors thrown, return error if (error) { @@ -29,7 +29,7 @@ export async function POST(req: Request, context: { params: { slug: string } }) } /* - Unsubscribe from project changelogs + Unsubscribe from workspace changelogs DELETE /api/v1/:slug/changelogs/subscribers { subId: string, @@ -43,8 +43,8 @@ export async function DELETE(req: Request, context: { params: { slug: string } } return NextResponse.json({ error: 'subId is required.' }, { status: 400 }); } - // Unsubscribe from project changelogs - const { error } = await unsubscribeFromProjectChangelogs(context.params.slug, subId); + // Unsubscribe from workspace changelogs + const { error } = await unsubscribeFromWorkspaceChangelogs(context.params.slug, subId); // If any errors thrown, return error if (error) { diff --git a/apps/web/app/api/v1/[slug]/feedback/route.ts b/apps/web/app/api/v1/[slug]/feedback/route.ts new file mode 100644 index 0000000..51e8479 --- /dev/null +++ b/apps/web/app/api/v1/[slug]/feedback/route.ts @@ -0,0 +1,23 @@ +import { NextResponse } from 'next/server'; +import { getPublicWorkspaceFeedback } from '@/lib/api/public'; + +/* + Get public feedback + GET /api/v1/[slug]/feedback + */ +export async function GET(req: Request, context: { params: { slug: string } }) { + const { data: feedback, error } = await getPublicWorkspaceFeedback( + context.params.slug, + 'route', + true, + false + ); + + // If any errors thrown, return error + if (error) { + return NextResponse.json({ error: error.message }, { status: error.status }); + } + + // Return feedback + return NextResponse.json(feedback, { status: 200 }); +} diff --git a/apps/web/app/api/v1/[slug]/sso/route.ts b/apps/web/app/api/v1/[slug]/sso/route.ts index 889291e..92ead6c 100644 --- a/apps/web/app/api/v1/[slug]/sso/route.ts +++ b/apps/web/app/api/v1/[slug]/sso/route.ts @@ -2,7 +2,7 @@ import { cookies } from 'next/headers'; import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@supabase/supabase-js'; import { JwtPayload, verify } from 'jsonwebtoken'; -import { getProjectBySlug } from '@/lib/api/projects'; +import { getWorkspaceBySlug } from '@/lib/api/workspace'; export async function GET(req: NextRequest, context: { params: { slug: string } }) { const redirectTo = req.nextUrl.searchParams.get('redirect_to'); @@ -18,33 +18,33 @@ export async function GET(req: NextRequest, context: { params: { slug: string } const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY); - // Get project by slug - const { data: project, error: projectError } = await getProjectBySlug( + // Get workspace by slug + const { data: workspace, error: workspaceError } = await getWorkspaceBySlug( context.params.slug, 'route', true, false ); - if (projectError) { - return NextResponse.json(projectError, { status: 500 }); + if (workspaceError) { + return NextResponse.json(workspaceError, { status: 500 }); } - // Get projects jwt secret - const { data: projectConfig, error: projectConfigError } = await supabase - .from('project_configs') + // Get workspaces jwt secret + const { data: workspaceConfig, error: workspaceConfigError } = await supabase + .from('workspace_configs') .select('integration_sso_secret') - .eq('project_id', project.id) + .eq('workspace_id', workspace.id) .single(); - if (projectConfigError) { - return NextResponse.json(projectConfigError.message, { status: 500 }); + if (workspaceConfigError) { + return NextResponse.json(workspaceConfigError.message, { status: 500 }); } // Verify jwt let payload: JwtPayload; try { - const decoded = verify(jwtPayload, projectConfig?.integration_sso_secret as string); + const decoded = verify(jwtPayload, workspaceConfig?.integration_sso_secret as string); payload = decoded as JwtPayload; } catch (error) { if (error instanceof Error) { @@ -59,7 +59,7 @@ export async function GET(req: NextRequest, context: { params: { slug: string } return NextResponse.json({ error: 'Invalid payload' }, { status: 500 }); } - const email = (payload.email as string).replace('@', `+${project.id}@`); + const email = (payload.email as string).replace('@', `+${workspace.id}@`); // Create user based on jwt payload const { error } = await supabase.auth.admin.createUser({ diff --git a/apps/web/app/api/v1/[slug]/views/route.ts b/apps/web/app/api/v1/[slug]/views/route.ts index 9b038e7..ec0681e 100644 --- a/apps/web/app/api/v1/[slug]/views/route.ts +++ b/apps/web/app/api/v1/[slug]/views/route.ts @@ -3,7 +3,7 @@ import { recordClick } from '@/lib/tinybird'; /* Record page view - POST /api/v1/[project]/views + POST /api/v1/[workspace]/views { "feedbackId": "string", "changelogId": "string", @@ -19,7 +19,7 @@ export async function POST(req: NextRequest, context: { params: { slug: string } const data = await recordClick({ req, - projectId: context.params.slug, + workspaceId: context.params.slug, feedbackId, changelogId, }); diff --git a/apps/web/app/api/v1/projects/[slug]/config/integrations/sso/route.ts b/apps/web/app/api/v1/projects/[slug]/config/integrations/sso/route.ts deleted file mode 100644 index 72a5472..0000000 --- a/apps/web/app/api/v1/projects/[slug]/config/integrations/sso/route.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { NextResponse } from 'next/server'; -import { updateProjectConfigBySlug } from '@/lib/api/projects'; - -/* - Update SSO configuration - PATCH /api/v1/projects/:slug/config/integrations/sso - { - status: boolean, - url: string, - secret: string, - } -*/ -export async function PATCH(req: Request, context: { params: { slug: string } }) { - const { status, url, secret } = await req.json(); - - if (status && (!url || !secret)) { - return NextResponse.json( - { error: 'url and secret are required when enabling SSO integration.' }, - { status: 400 } - ); - } - - // Update project config - const { data: updatedProjectConfig, error } = await updateProjectConfigBySlug( - context.params.slug, - { - integration_sso_status: status, - integration_sso_url: status ? url : null, - integration_sso_secret: status ? secret : null, - }, - 'route' - ); - - // If any errors thrown, return error - if (error) { - return NextResponse.json({ error: error.message }, { status: error.status }); - } - - // Return updated project config - return NextResponse.json(updatedProjectConfig, { status: 200 }); -} diff --git a/apps/web/app/api/v1/projects/[slug]/config/route.ts b/apps/web/app/api/v1/projects/[slug]/config/route.ts deleted file mode 100644 index 938fdfb..0000000 --- a/apps/web/app/api/v1/projects/[slug]/config/route.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getProjectConfigBySlug, updateProjectConfigBySlug } from '@/lib/api/projects'; -import { ProjectConfigProps } from '@/lib/types'; - -/* - Get Project Config by slug - GET /api/v1/projects/[slug]/config -*/ -export async function GET(req: Request, context: { params: { slug: string } }) { - const { data: projectConfig, error } = await getProjectConfigBySlug(context.params.slug, 'route'); - - // If any errors thrown, return error - if (error) { - return NextResponse.json({ error: error.message }, { status: error.status }); - } - - // Return project config - return NextResponse.json(projectConfig, { status: 200 }); -} - -/* - Update Project Config by slug - PATCH /api/v1/projects/[slug]/config - { - changelog_preview_style: string; - changelog_twitter_handle: string; - } -*/ -export async function PATCH(req: Request, context: { params: { slug: string } }) { - const { - changelog_enabled: changelogEnabled, - changelog_preview_style: changelogPreviewStyle, - changelog_twitter_handle: changelogTwitterHandle, - feedback_allow_anon_upvoting: feedbackAllowAnonUpvoting, - custom_theme: customTheme, - custom_theme_root: customThemeRoot, - custom_theme_primary_foreground: customThemePrimaryForeground, - custom_theme_background: customThemeBackground, - custom_theme_secondary_background: customThemeSecondaryBackground, - custom_theme_accent: customThemeAccent, - custom_theme_border: customThemeBorder, - logo_redirect_url: logoRedirectUrl, - } = (await req.json()) as ProjectConfigProps['Update']; - - // Update project config - const { data: updatedProjectConfig, error } = await updateProjectConfigBySlug( - context.params.slug, - { - changelog_enabled: changelogEnabled, - changelog_preview_style: changelogPreviewStyle, - changelog_twitter_handle: changelogTwitterHandle, - feedback_allow_anon_upvoting: feedbackAllowAnonUpvoting, - custom_theme: customTheme, - custom_theme_root: customThemeRoot, - custom_theme_primary_foreground: customThemePrimaryForeground, - custom_theme_background: customThemeBackground, - custom_theme_secondary_background: customThemeSecondaryBackground, - custom_theme_accent: customThemeAccent, - custom_theme_border: customThemeBorder, - logo_redirect_url: logoRedirectUrl, - }, - 'route' - ); - - // If any errors thrown, return error - if (error) { - return NextResponse.json({ error: error.message }, { status: error.status }); - } - - // Return updated project config - return NextResponse.json(updatedProjectConfig, { status: 200 }); -} diff --git a/apps/web/app/api/v1/projects/[slug]/feedback/route.ts b/apps/web/app/api/v1/projects/[slug]/feedback/route.ts deleted file mode 100644 index 7c24e3d..0000000 --- a/apps/web/app/api/v1/projects/[slug]/feedback/route.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { NextResponse } from 'next/server'; -import { createFeedback, getAllProjectFeedback } from '@/lib/api/feedback'; -import { FeedbackWithUserInputProps } from '@/lib/types'; - -export const runtime = 'edge'; - -/* - Create Feedback - POST /api/v1/projects/[slug]/feedback - { - title: string; - description: string; - status: string; - tags: [id, id, id] - } -*/ -export async function POST(req: Request, context: { params: { slug: string } }) { - const { title, description, status, tags, user } = (await req.json()) as FeedbackWithUserInputProps; - - // Validate Request Body - if (!title) { - return NextResponse.json({ error: 'title is required when creating feedback.' }, { status: 400 }); - } - - const { data: feedback, error } = await createFeedback( - context.params.slug, - { - title: title || '', - description: description || '', - status: status || '', - project_id: 'dummy-id', - user_id: 'dummy-id', - tags: tags || [], - user: user !== null ? user : undefined, - }, - 'route' - ); - - // If any errors thrown, return error - if (error) { - return NextResponse.json({ error: error.message }, { status: error.status }); - } - - // Return feedback - return NextResponse.json(feedback, { status: 200 }); -} - -/* - Get Project Feedback - GET /api/v1/projects/[slug]/feedback -*/ -export async function GET(req: Request, context: { params: { slug: string } }) { - const { data: feedback, error } = await getAllProjectFeedback(context.params.slug, 'route', false); - - // If any errors thrown, return error - if (error) { - return NextResponse.json({ error: error.message }, { status: error.status }); - } - - // Return feedback - return NextResponse.json(feedback, { status: 200 }); -} diff --git a/apps/web/app/api/v1/projects/[slug]/route.ts b/apps/web/app/api/v1/projects/[slug]/route.ts deleted file mode 100644 index ddfa07b..0000000 --- a/apps/web/app/api/v1/projects/[slug]/route.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { NextResponse } from 'next/server'; -import { deleteProjectBySlug, getProjectBySlug, updateProjectBySlug } from '@/lib/api/projects'; -import { ProjectProps } from '@/lib/types'; - -/* - Get project by slug - GET /api/v1/projects/[slug] -*/ -export async function GET(req: Request, context: { params: { slug: string } }) { - const { data: project, error } = await getProjectBySlug(context.params.slug, 'route'); - // If any errors thrown, return error - if (error) { - return NextResponse.json({ error: error.message }, { status: error.status }); - } - - // Return project - return NextResponse.json({ project }, { status: 200 }); -} - -/* - Update project by slug - PATCH /api/v1/projects/[slug] - { - name: string, - slug: string, - icon: string, - icon_radius: string, - og_image: string - } -*/ -export async function PATCH(req: Request, context: { params: { slug: string } }) { - const { - name, - slug, - icon, - icon_radius: iconRadius, - og_image: OGImage, - } = (await req.json()) as ProjectProps['Update']; - - const { data: updatedProject, error } = await updateProjectBySlug( - context.params.slug, - { name, slug, icon, icon_radius: iconRadius, og_image: OGImage }, - 'route' - ); - - // If any errors thrown, return error - if (error) { - return NextResponse.json({ error: error.message }, { status: error.status }); - } - - // Return updated project - return NextResponse.json(updatedProject, { status: 200 }); -} - -/* - Delete project by slug - DELETE /api/v1/projects/[slug] -*/ -export async function DELETE(req: Request, context: { params: { slug: string } }) { - const { data, error } = await deleteProjectBySlug(context.params.slug, 'route'); - - // If any errors thrown, return error - if (error) { - return NextResponse.json({ error: error.message }, { status: error.status }); - } - - // Return success - return NextResponse.json({ data }, { status: 200 }); -} diff --git a/apps/web/app/api/v1/route.ts b/apps/web/app/api/v1/route.ts index 56224ea..eb89aaf 100644 --- a/apps/web/app/api/v1/route.ts +++ b/apps/web/app/api/v1/route.ts @@ -23,16 +23,16 @@ export function GET(): NextResponse { }, ], paths: { - '/{projectSlug}/atom': { + '/{workspaceSlug}/atom': { get: { - description: 'Generate atom feed for project changelog', - operationId: 'getProjectChangelogsAtom', + description: 'Generate atom feed for workspace changelog', + operationId: 'getWorkspaceChangelogsAtom', security: [], parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -51,7 +51,7 @@ export function GET(): NextResponse { }, }, 400: { - description: 'Invalid project slug supplied', + description: 'Invalid workspace slug supplied', content: { 'application/json': { schema: { @@ -61,7 +61,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project not found', + description: 'Workspace not found', content: { 'application/json': { schema: { @@ -73,16 +73,16 @@ export function GET(): NextResponse { }, }, }, - '/{projectSlug}/changelogs': { + '/{workspaceSlug}/changelogs': { get: { - description: 'Get public project changelogs', - operationId: 'getPublicProjectChangelogs', + description: 'Get public workspace changelogs', + operationId: 'getPublicWorkspaceChangelogs', security: [], parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -104,7 +104,7 @@ export function GET(): NextResponse { }, }, 400: { - description: 'Invalid project slug supplied', + description: 'Invalid workspace slug supplied', content: { 'application/json': { schema: { @@ -114,7 +114,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project not found', + description: 'Workspace not found', content: { 'application/json': { schema: { @@ -126,15 +126,15 @@ export function GET(): NextResponse { }, }, }, - '/projects/{projectSlug}': { + '/workspaces/{workspaceSlug}': { get: { - description: 'Get a project by slug', - operationId: 'getProjectBySlug', + description: 'Get a workspace by slug', + operationId: 'getWorkspaceBySlug', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -147,13 +147,13 @@ export function GET(): NextResponse { content: { 'application/json': { schema: { - $ref: '#/components/schemas/Project', + $ref: '#/components/schemas/Workspace', }, }, }, }, 400: { - description: 'Invalid project slug supplied', + description: 'Invalid workspace slug supplied', content: { 'application/json': { schema: { @@ -163,7 +163,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project not found', + description: 'Workspace not found', content: { 'application/json': { schema: { @@ -175,13 +175,13 @@ export function GET(): NextResponse { }, }, patch: { - description: 'Update a project by slug', - operationId: 'updateProjectBySlug', + description: 'Update a workspace by slug', + operationId: 'updateWorkspaceBySlug', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -189,12 +189,12 @@ export function GET(): NextResponse { }, ], requestBody: { - description: 'Project object that needs to be updated', + description: 'Workspace object that needs to be updated', required: true, content: { 'application/json': { schema: { - $ref: '#/components/schemas/ProjectUpdate', + $ref: '#/components/schemas/WorkspaceUpdate', }, }, }, @@ -205,13 +205,13 @@ export function GET(): NextResponse { content: { 'application/json': { schema: { - $ref: '#/components/schemas/Project', + $ref: '#/components/schemas/Workspace', }, }, }, }, 400: { - description: 'Invalid project slug supplied', + description: 'Invalid workspace slug supplied', content: { 'application/json': { schema: { @@ -221,7 +221,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project not found', + description: 'Workspace not found', content: { 'application/json': { schema: { @@ -233,13 +233,13 @@ export function GET(): NextResponse { }, }, delete: { - description: 'Delete a project by slug', - operationId: 'deleteProjectBySlug', + description: 'Delete a workspace by slug', + operationId: 'deleteWorkspaceBySlug', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -252,13 +252,13 @@ export function GET(): NextResponse { content: { 'application/json': { schema: { - $ref: '#/components/schemas/Project', + $ref: '#/components/schemas/Workspace', }, }, }, }, 400: { - description: 'Invalid project slug supplied', + description: 'Invalid workspace slug supplied', content: { 'application/json': { schema: { @@ -268,7 +268,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project not found', + description: 'Workspace not found', content: { 'application/json': { schema: { @@ -280,15 +280,15 @@ export function GET(): NextResponse { }, }, }, - '/projects/{projectSlug}/changelogs': { + '/workspaces/{workspaceSlug}/changelogs': { get: { - description: 'Get all project changelogs', - operationId: 'getProjectChangelogs', + description: 'Get all workspace changelogs', + operationId: 'getWorkspaceChangelogs', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -310,7 +310,7 @@ export function GET(): NextResponse { }, }, 400: { - description: 'Invalid project slug supplied', + description: 'Invalid workspace slug supplied', content: { 'application/json': { schema: { @@ -320,7 +320,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project not found', + description: 'Workspace not found', content: { 'application/json': { schema: { @@ -332,13 +332,13 @@ export function GET(): NextResponse { }, }, post: { - description: 'Create project changelog', - operationId: 'createProjectChangelog', + description: 'Create workspace changelog', + operationId: 'createWorkspaceChangelog', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -368,7 +368,7 @@ export function GET(): NextResponse { }, }, 400: { - description: 'Invalid project slug supplied', + description: 'Invalid workspace slug supplied', content: { 'application/json': { schema: { @@ -378,7 +378,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project not found', + description: 'Workspace not found', content: { 'application/json': { schema: { @@ -390,15 +390,15 @@ export function GET(): NextResponse { }, }, }, - '/projects/{projectSlug}/changelogs/{changelogId}': { + '/workspaces/{workspaceSlug}/changelogs/{changelogId}': { put: { - description: 'Update project changelog by id', - operationId: 'updateProjectChangelogById', + description: 'Update workspace changelog by id', + operationId: 'updateWorkspaceChangelogById', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -437,21 +437,21 @@ export function GET(): NextResponse { }, }, 400: { - description: 'Invalid project slug or changelog id supplied', + description: 'Invalid workspace slug or changelog id supplied', }, 404: { - description: 'Project or changelog not found', + description: 'Workspace or changelog not found', }, }, }, delete: { - description: 'Delete project changelog by id', - operationId: 'deleteProjectChangelogById', + description: 'Delete workspace changelog by id', + operationId: 'deleteWorkspaceChangelogById', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -479,23 +479,23 @@ export function GET(): NextResponse { }, }, 400: { - description: 'Invalid project slug or changelog id supplied', + description: 'Invalid workspace slug or changelog id supplied', }, 404: { - description: 'Project or changelog not found', + description: 'Workspace or changelog not found', }, }, }, }, - '/projects/{projectSlug}/config': { + '/workspaces/{workspaceSlug}/config': { get: { - description: 'Get project config', - operationId: 'getProjectConfig', + description: 'Get workspace config', + operationId: 'getWorkspaceConfig', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -508,13 +508,13 @@ export function GET(): NextResponse { content: { 'application/json': { schema: { - $ref: '#/components/schemas/ProjectConfig', + $ref: '#/components/schemas/WorkspaceConfig', }, }, }, }, 400: { - description: 'Invalid project slug supplied', + description: 'Invalid workspace slug supplied', content: { 'application/json': { schema: { @@ -524,7 +524,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project not found', + description: 'Workspace not found', content: { 'application/json': { schema: { @@ -536,13 +536,13 @@ export function GET(): NextResponse { }, }, patch: { - description: 'Update project config', - operationId: 'updateProjectConfig', + description: 'Update workspace config', + operationId: 'updateWorkspaceConfig', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -550,13 +550,13 @@ export function GET(): NextResponse { }, ], requestBody: { - description: 'Project config object that needs to be updated', + description: 'Workspace config object that needs to be updated', required: true, content: { 'application/json': { schema: { type: 'object', - $ref: '#/components/schemas/ProjectConfigUpdate', + $ref: '#/components/schemas/WorkspaceConfigUpdate', }, }, }, @@ -568,13 +568,13 @@ export function GET(): NextResponse { 'application/json': { schema: { type: 'object', - $ref: '#/components/schemas/ProjectConfig', + $ref: '#/components/schemas/WorkspaceConfig', }, }, }, }, 400: { - description: 'Invalid project slug supplied', + description: 'Invalid workspace slug supplied', content: { 'application/json': { schema: { @@ -584,7 +584,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project not found', + description: 'Workspace not found', content: { 'application/json': { schema: { @@ -596,15 +596,15 @@ export function GET(): NextResponse { }, }, }, - '/projects/{projectSlug}/feedback': { + '/workspaces/{workspaceSlug}/feedback': { get: { - description: 'Get all project feedback', - operationId: 'getProjectFeedback', + description: 'Get all workspace feedback', + operationId: 'getWorkspaceFeedback', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -626,7 +626,7 @@ export function GET(): NextResponse { }, }, 400: { - description: 'Invalid project slug supplied', + description: 'Invalid workspace slug supplied', content: { 'application/json': { schema: { @@ -636,7 +636,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project not found', + description: 'Workspace not found', content: { 'application/json': { schema: { @@ -648,13 +648,13 @@ export function GET(): NextResponse { }, }, post: { - description: 'Create project feedback', - operationId: 'createProjectFeedback', + description: 'Create workspace feedback', + operationId: 'createWorkspaceFeedback', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -684,7 +684,7 @@ export function GET(): NextResponse { }, }, 400: { - description: 'Invalid project slug supplied', + description: 'Invalid workspace slug supplied', content: { 'application/json': { schema: { @@ -694,7 +694,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project not found', + description: 'Workspace not found', content: { 'application/json': { schema: { @@ -706,15 +706,15 @@ export function GET(): NextResponse { }, }, }, - '/projects/{projectSlug}/feedback/{feedbackId}': { + '/workspaces/{workspaceSlug}/feedback/{feedbackId}': { get: { - description: 'Get project feedback by id', - operationId: 'getProjectFeedbackById', + description: 'Get workspace feedback by id', + operationId: 'getWorkspaceFeedbackById', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -742,7 +742,7 @@ export function GET(): NextResponse { }, }, 400: { - description: 'Invalid project slug or feedback id supplied', + description: 'Invalid workspace slug or feedback id supplied', content: { 'application/json': { schema: { @@ -764,13 +764,13 @@ export function GET(): NextResponse { }, }, patch: { - description: 'Update project feedback by id', - operationId: 'updateProjectFeedbackById', + description: 'Update workspace feedback by id', + operationId: 'updateWorkspaceFeedbackById', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -809,7 +809,7 @@ export function GET(): NextResponse { }, }, 400: { - description: 'Invalid project slug or feedback id supplied', + description: 'Invalid workspace slug or feedback id supplied', content: { 'application/json': { schema: { @@ -831,13 +831,13 @@ export function GET(): NextResponse { }, }, delete: { - description: 'Delete project feedback by id', - operationId: 'deleteProjectFeedbackById', + description: 'Delete workspace feedback by id', + operationId: 'deleteWorkspaceFeedbackById', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -865,7 +865,7 @@ export function GET(): NextResponse { }, }, 400: { - description: 'Invalid project slug or feedback id supplied', + description: 'Invalid workspace slug or feedback id supplied', content: { 'application/json': { schema: { @@ -887,15 +887,15 @@ export function GET(): NextResponse { }, }, }, - '/projects/{projectSlug}/feedback/{feedbackId}/comments': { + '/workspaces/{workspaceSlug}/feedback/{feedbackId}/comments': { get: { description: 'Get feedback comments', operationId: 'getFeedbackComments', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -926,7 +926,7 @@ export function GET(): NextResponse { }, }, 400: { - description: 'Invalid project slug or feedback id supplied', + description: 'Invalid workspace slug or feedback id supplied', content: { 'application/json': { schema: { @@ -936,7 +936,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project or feedback not found', + description: 'Workspace or feedback not found', content: { 'application/json': { schema: { @@ -948,15 +948,15 @@ export function GET(): NextResponse { }, }, }, - '/projects/{projectSlug}/feedback/{feedbackId}/comments/{commentId}': { + '/workspaces/{workspaceSlug}/feedback/{feedbackId}/comments/{commentId}': { delete: { - description: 'Delete project feedback comment by id', - operationId: 'deleteProjectFeedbackCommentById', + description: 'Delete workspace feedback comment by id', + operationId: 'deleteWorkspaceFeedbackCommentById', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -993,7 +993,7 @@ export function GET(): NextResponse { }, }, 400: { - description: 'Invalid project slug or feedback id supplied', + description: 'Invalid workspace slug or feedback id supplied', content: { 'application/json': { schema: { @@ -1003,7 +1003,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project or feedback not found', + description: 'Workspace or feedback not found', content: { 'application/json': { schema: { @@ -1015,15 +1015,15 @@ export function GET(): NextResponse { }, }, }, - '/projects/{projectSlug}/feedback/{feedbackId}/upvotes': { + '/workspaces/{workspaceSlug}/feedback/{feedbackId}/upvotes': { get: { description: 'Get feedback upvoters', operationId: 'getFeedbackUpvoters', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -1054,7 +1054,7 @@ export function GET(): NextResponse { }, }, 400: { - description: 'Invalid project slug or feedback id supplied', + description: 'Invalid workspace slug or feedback id supplied', content: { 'application/json': { schema: { @@ -1076,15 +1076,15 @@ export function GET(): NextResponse { }, }, }, - '/projects/{projectSlug}/feedback/tags': { + '/workspaces/{workspaceSlug}/feedback/tags': { get: { description: 'Get feedback tags', operationId: 'getFeedbackTags', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -1106,7 +1106,7 @@ export function GET(): NextResponse { }, }, 400: { - description: 'Invalid project slug supplied', + description: 'Invalid workspace slug supplied', content: { 'application/json': { schema: { @@ -1116,7 +1116,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project not found', + description: 'Workspace not found', content: { 'application/json': { schema: { @@ -1132,9 +1132,9 @@ export function GET(): NextResponse { operationId: 'createFeedbackTag', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -1164,7 +1164,7 @@ export function GET(): NextResponse { }, }, 400: { - description: 'Invalid project slug supplied', + description: 'Invalid workspace slug supplied', content: { 'application/json': { schema: { @@ -1174,7 +1174,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project not found', + description: 'Workspace not found', content: { 'application/json': { schema: { @@ -1186,15 +1186,15 @@ export function GET(): NextResponse { }, }, }, - '/projects/{projectSlug}/feedback/tags/{tagName}': { + '/workspaces/{workspaceSlug}/feedback/tags/{tagName}': { delete: { description: 'Delete feedback tag by name', operationId: 'deleteFeedbackTagByName', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -1222,7 +1222,7 @@ export function GET(): NextResponse { }, }, 400: { - description: 'Invalid project slug or tag name supplied', + description: 'Invalid workspace slug or tag name supplied', content: { 'application/json': { schema: { @@ -1232,7 +1232,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project or tag not found', + description: 'Workspace or tag not found', content: { 'application/json': { schema: { @@ -1244,15 +1244,15 @@ export function GET(): NextResponse { }, }, }, - '/projects/{projectSlug}/invites': { + '/workspaces/{workspaceSlug}/invites': { get: { - description: 'List project team invites', - operationId: 'getProjectInvites', + description: 'List workspace team invites', + operationId: 'getWorkspaceInvites', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -1267,14 +1267,14 @@ export function GET(): NextResponse { schema: { type: 'array', items: { - $ref: '#/components/schemas/ExtendedProjectInvite', + $ref: '#/components/schemas/ExtendedWorkspaceInvite', }, }, }, }, }, 400: { - description: 'Invalid project slug supplied', + description: 'Invalid workspace slug supplied', content: { 'application/json': { schema: { @@ -1284,7 +1284,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project not found', + description: 'Workspace not found', content: { 'application/json': { schema: { @@ -1296,13 +1296,13 @@ export function GET(): NextResponse { }, }, post: { - description: 'Create a project invite', - operationId: 'createProjectInvite', + description: 'Create a workspace invite', + operationId: 'createWorkspaceInvite', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -1310,7 +1310,7 @@ export function GET(): NextResponse { }, ], requestBody: { - description: 'Project invite object that needs to be created', + description: 'Workspace invite object that needs to be created', required: true, content: { 'application/json': { @@ -1331,13 +1331,13 @@ export function GET(): NextResponse { content: { 'application/json': { schema: { - $ref: '#/components/schemas/ProjectInvite', + $ref: '#/components/schemas/WorkspaceInvite', }, }, }, }, 400: { - description: 'Invalid project slug supplied', + description: 'Invalid workspace slug supplied', content: { 'application/json': { schema: { @@ -1347,7 +1347,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project not found', + description: 'Workspace not found', content: { 'application/json': { schema: { @@ -1359,15 +1359,15 @@ export function GET(): NextResponse { }, }, }, - '/projects/{projectSlug}/invites/{inviteId}': { + '/workspaces/{workspaceSlug}/invites/{inviteId}': { delete: { - description: 'Delete project invite by id', - operationId: 'deleteProjectInviteById', + description: 'Delete workspace invite by id', + operationId: 'deleteWorkspaceInviteById', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -1389,13 +1389,13 @@ export function GET(): NextResponse { content: { 'application/json': { schema: { - $ref: '#/components/schemas/ProjectInvite', + $ref: '#/components/schemas/WorkspaceInvite', }, }, }, }, 400: { - description: 'Invalid project slug supplied', + description: 'Invalid workspace slug supplied', content: { 'application/json': { schema: { @@ -1405,7 +1405,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project not found', + description: 'Workspace not found', content: { 'application/json': { schema: { @@ -1417,15 +1417,15 @@ export function GET(): NextResponse { }, }, }, - '/projects/{projectSlug}/members': { + '/workspaces/{workspaceSlug}/members': { get: { - description: 'Get project members', - operationId: 'getProjectMembers', + description: 'Get workspace members', + operationId: 'getWorkspaceMembers', parameters: [ { - name: 'projectSlug', + name: 'workspaceSlug', in: 'path', - description: 'Project slug', + description: 'Workspace slug', required: true, schema: { type: 'string', @@ -1441,14 +1441,14 @@ export function GET(): NextResponse { schema: { type: 'array', items: { - $ref: '#/components/schemas/ProjectMember', + $ref: '#/components/schemas/WorkspaceMember', }, }, }, }, }, 400: { - description: 'Invalid project slug supplied', + description: 'Invalid workspace slug supplied', content: { 'application/json': { schema: { @@ -1458,7 +1458,7 @@ export function GET(): NextResponse { }, }, 404: { - description: 'Project not found', + description: 'Workspace not found', content: { 'application/json': { schema: { @@ -1473,7 +1473,7 @@ export function GET(): NextResponse { }, components: { schemas: { - Project: { + Workspace: { type: 'object', properties: { id: { @@ -1501,7 +1501,7 @@ export function GET(): NextResponse { }, }, }, - ProjectUpdate: { + WorkspaceUpdate: { type: 'object', properties: { name: { @@ -1528,7 +1528,7 @@ export function GET(): NextResponse { type: 'string', format: 'uuid', }, - project_id: { + workspace_id: { type: 'string', format: 'uuid', }, @@ -1567,7 +1567,7 @@ export function GET(): NextResponse { type: 'string', format: 'uuid', }, - project_id: { + workspace_id: { type: 'string', format: 'uuid', }, @@ -1667,14 +1667,14 @@ export function GET(): NextResponse { }, }, }, - ProjectConfig: { + WorkspaceConfig: { type: 'object', properties: { id: { type: 'string', format: 'uuid', }, - project_id: { + workspace_id: { type: 'string', format: 'uuid', }, @@ -1705,7 +1705,7 @@ export function GET(): NextResponse { }, }, }, - ProjectConfigUpdate: { + WorkspaceConfigUpdate: { type: 'object', properties: { changelog_preview_style: { @@ -1723,7 +1723,7 @@ export function GET(): NextResponse { type: 'string', format: 'uuid', }, - project_id: { + workspace_id: { type: 'string', format: 'uuid', }, @@ -1759,7 +1759,7 @@ export function GET(): NextResponse { type: 'string', format: 'uuid', }, - project_id: { + workspace_id: { type: 'string', format: 'uuid', }, @@ -1998,7 +1998,7 @@ export function GET(): NextResponse { type: 'string', format: 'uuid', }, - project_id: { + workspace_id: { type: 'string', format: 'uuid', }, @@ -2025,14 +2025,14 @@ export function GET(): NextResponse { }, }, }, - ProjectInvite: { + WorkspaceInvite: { type: 'object', properties: { id: { type: 'string', format: 'uuid', }, - project_id: { + workspace_id: { type: 'string', format: 'uuid', }, @@ -2052,18 +2052,18 @@ export function GET(): NextResponse { }, }, }, - ExtendedProjectInvite: { + ExtendedWorkspaceInvite: { type: 'object', properties: { id: { type: 'string', format: 'uuid', }, - project_id: { + workspace_id: { type: 'string', format: 'uuid', }, - project: { + workspace: { type: 'object', properties: { name: { @@ -2101,7 +2101,7 @@ export function GET(): NextResponse { }, }, }, - ProjectMember: { + WorkspaceMember: { type: 'object', properties: { id: { diff --git a/apps/web/app/api/v1/projects/[slug]/config/domain/route.ts b/apps/web/app/api/v1/workspaces/[slug]/(config)/domain/route.ts similarity index 71% rename from apps/web/app/api/v1/projects/[slug]/config/domain/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/(config)/domain/route.ts index c5a2d2b..cc55d24 100644 --- a/apps/web/app/api/v1/projects/[slug]/config/domain/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/(config)/domain/route.ts @@ -1,21 +1,21 @@ import { NextResponse } from 'next/server'; -import { getProjectConfigBySlug, updateProjectConfigBySlug } from '@/lib/api/projects'; +import { getWorkspaceBySlug, updateWorkspaceBySlug } from '@/lib/api/workspace'; /* - GET /api/v1/projects/:slug/config/domain + GET /api/v1/workspaces/:slug/domain */ export async function GET(req: Request, context: { params: { slug: string } }) { - // Get project - const { data, error } = await getProjectConfigBySlug(context.params.slug, 'route'); + // Get workspace + const { data: workspace, error: workspaceError } = await getWorkspaceBySlug(context.params.slug, 'route'); // If any errors thrown, return error - if (error) { - return NextResponse.json({ error: error.message }, { status: error.status }); + if (workspaceError) { + return NextResponse.json({ error: workspaceError.message }, { status: workspaceError.status }); } const [configResponse, domainResponse] = await Promise.all([ fetch( - `https://api.vercel.com/v6/domains/${data.custom_domain}/config${ + `https://api.vercel.com/v6/domains/${workspace.custom_domain}/config${ process.env.VERCEL_TEAM_ID ? `?teamId=${process.env.VERCEL_TEAM_ID}` : '' }`, { @@ -27,9 +27,9 @@ export async function GET(req: Request, context: { params: { slug: string } }) { } ), fetch( - `https://api.vercel.com/v9/projects/${process.env.VERCEL_PROJECT_ID}/domains/${data.custom_domain}${ - process.env.VERCEL_TEAM_ID ? `?teamId=${process.env.VERCEL_TEAM_ID}` : '' - }`, + `https://api.vercel.com/v9/projects/${process.env.VERCEL_PROJECT_ID}/domains/${ + workspace.custom_domain + }${process.env.VERCEL_TEAM_ID ? `?teamId=${process.env.VERCEL_TEAM_ID}` : ''}`, { method: 'GET', headers: { @@ -58,7 +58,7 @@ export async function GET(req: Request, context: { params: { slug: string } }) { if (!domainData.verified && !configData.misconfigured) { const verifyResponse = await fetch( `https://api.vercel.com/v9/projects/${process.env.VERCEL_PROJECT_ID}/domains/${ - data.custom_domain + workspace.custom_domain }/verify${process.env.VERCEL_TEAM_ID ? `?teamId=${process.env.VERCEL_TEAM_ID}` : ''}`, { method: 'POST', @@ -102,9 +102,9 @@ export async function GET(req: Request, context: { params: { slug: string } }) { ); } - // If verification succeeded, update project config + // If verification succeeded, update workspace config if (domainData?.verified && !configData.misconfigured) { - const { error: updateError } = await updateProjectConfigBySlug( + const { error: updateError } = await updateWorkspaceBySlug( context.params.slug, { custom_domain_verified: true }, 'route' @@ -128,7 +128,7 @@ export async function GET(req: Request, context: { params: { slug: string } }) { } /* - POST /api/v1/projects/:slug/config/domain + POST /api/v1/workspaces/:slug/domain { "name": "example.com" } @@ -140,20 +140,17 @@ export async function POST(req: Request, context: { params: { slug: string } }) return NextResponse.json({ error: 'Name is required' }, { status: 400 }); } - // Get project config - const { data: projectConfig, error: projectConfigError } = await getProjectConfigBySlug( - context.params.slug, - 'route' - ); + // Get workspace config + const { data: workspace, error: workspaceError } = await getWorkspaceBySlug(context.params.slug, 'route'); // If any errors thrown, return error - if (projectConfigError) { - return NextResponse.json({ error: projectConfigError.message }, { status: projectConfigError.status }); + if (workspaceError) { + return NextResponse.json({ error: workspaceError.message }, { status: workspaceError.status }); } - // If project already has a domain, return error - if (projectConfig.custom_domain) { - return NextResponse.json({ error: 'Project already has a custom domain' }, { status: 409 }); + // If workspace already has a domain, return error + if (workspace.custom_domain) { + return NextResponse.json({ error: 'Workspace already has a custom domain' }, { status: 409 }); } const response = await fetch( @@ -188,8 +185,8 @@ export async function POST(req: Request, context: { params: { slug: string } }) ); } - // Update project config - const { error } = await updateProjectConfigBySlug( + // Update workspace config + const { error } = await updateWorkspaceBySlug( context.params.slug, { custom_domain: responseData.name, custom_domain_verified: false }, 'route' @@ -205,25 +202,25 @@ export async function POST(req: Request, context: { params: { slug: string } }) } /* - DELETE /api/v1/projects/:slug/config/domain + DELETE /api/v1/workspaces/:slug/domain */ export async function DELETE(req: Request, context: { params: { slug: string } }) { - // Get project - const { data, error } = await getProjectConfigBySlug(context.params.slug, 'route'); + // Get workspace + const { data: workspace, error: workspaceError } = await getWorkspaceBySlug(context.params.slug, 'route'); // If any errors thrown, return error - if (error) { - return NextResponse.json({ error: error.message }, { status: error.status }); + if (workspaceError) { + return NextResponse.json({ error: workspaceError.message }, { status: workspaceError.status }); } // If no domain, return error - if (!data?.custom_domain) { - return NextResponse.json({ error: 'Project does not have a custom domain' }, { status: 400 }); + if (!workspace?.custom_domain) { + return NextResponse.json({ error: 'Workspace does not have a custom domain' }, { status: 400 }); } // Delete domain from Vercel const response = await fetch( - `https://api.vercel.com/v9/projects/${process.env.VERCEL_PROJECT_ID}/domains/${data?.custom_domain}${ + `https://api.vercel.com/v9/projects/${process.env.VERCEL_PROJECT_ID}/domains/${workspace?.custom_domain}${ process.env.VERCEL_TEAM_ID ? `?teamId=${process.env.VERCEL_TEAM_ID}` : '' }`, { @@ -242,21 +239,21 @@ export async function DELETE(req: Request, context: { params: { slug: string } } return NextResponse.json({ error: responseData.error.message }, { status: 400 }); } - // Update project config - const { data: updatedProjectConfig, error: updatedProjectConfigError } = await updateProjectConfigBySlug( + // Update workspace config + const { data: updatedWorkspaceConfig, error: updatedWorkspaceConfigError } = await updateWorkspaceBySlug( context.params.slug, - { custom_domain: null, custom_domain_verified: null }, + { custom_domain: null, custom_domain_verified: false }, 'route' ); // If any errors thrown, return error - if (updatedProjectConfigError) { + if (updatedWorkspaceConfigError) { return NextResponse.json( - { error: updatedProjectConfigError.message }, - { status: updatedProjectConfigError.status } + { error: updatedWorkspaceConfigError.message }, + { status: updatedWorkspaceConfigError.status } ); } // Return response - return NextResponse.json(updatedProjectConfig, { status: 200 }); + return NextResponse.json(updatedWorkspaceConfig, { status: 200 }); } diff --git a/apps/web/app/api/v1/projects/[slug]/config/integrations/discord/route.ts b/apps/web/app/api/v1/workspaces/[slug]/(config)/integrations/discord/route.ts similarity index 50% rename from apps/web/app/api/v1/projects/[slug]/config/integrations/discord/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/(config)/integrations/discord/route.ts index 2c483cf..003e70b 100644 --- a/apps/web/app/api/v1/projects/[slug]/config/integrations/discord/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/(config)/integrations/discord/route.ts @@ -1,37 +1,37 @@ import { NextResponse } from 'next/server'; -import { updateProjectConfigBySlug } from '@/lib/api/projects'; +import { updateWorkspaceIntegrations } from '@/lib/api/integration'; /* Update Discord integration - PATCH /api/v1/projects/:slug/config/integrations/discord + PATCH /api/v1/workspaces/:slug/config/integrations/discord { - status: boolean, + enabled: boolean, webhook: string, roleId: string, } */ export async function PATCH(req: Request, context: { params: { slug: string } }) { - const { status, webhook, roleId } = (await req.json()) as { - status: boolean; + const { enabled, webhook, roleId } = (await req.json()) as { + enabled: boolean; webhook: string; - roleId: string; + roleId: number; }; // If status is true, make sure webhook and roleId are not empty - if (status && !webhook) { + if (enabled && !webhook) { return NextResponse.json( { error: 'webhook is required when enabling Discord integration.' }, { status: 400 } ); } - // Update project config - const { data: updatedProjectConfig, error } = await updateProjectConfigBySlug( + // Update workspace config + const { data: updatedWorkspaceConfig, error } = await updateWorkspaceIntegrations( context.params.slug, { - integration_discord_status: status, - integration_discord_webhook: status ? webhook : null, - integration_discord_role_id: status ? roleId : null, + discord_enabled: enabled, + discord_webhook: enabled ? webhook : null, + discord_role_id: enabled ? roleId : null, }, 'route' ); @@ -41,6 +41,6 @@ export async function PATCH(req: Request, context: { params: { slug: string } }) return NextResponse.json({ error: error.message }, { status: error.status }); } - // Return updated project config - return NextResponse.json(updatedProjectConfig, { status: 200 }); + // Return updated workspace config + return NextResponse.json(updatedWorkspaceConfig, { status: 200 }); } diff --git a/apps/web/app/api/v1/projects/[slug]/config/integrations/slack/route.ts b/apps/web/app/api/v1/workspaces/[slug]/(config)/integrations/slack/route.ts similarity index 55% rename from apps/web/app/api/v1/projects/[slug]/config/integrations/slack/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/(config)/integrations/slack/route.ts index 25496b4..2c310e4 100644 --- a/apps/web/app/api/v1/projects/[slug]/config/integrations/slack/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/(config)/integrations/slack/route.ts @@ -1,34 +1,34 @@ import { NextResponse } from 'next/server'; -import { updateProjectConfigBySlug } from '@/lib/api/projects'; +import { updateWorkspaceIntegrations } from '@/lib/api/integration'; /* Update Slack integration - PATCH /api/v1/projects/:slug/config/integrations/slack + PATCH /api/v1/workspaces/:slug/config/integrations/slack { status: boolean, webhook: string, } */ export async function PATCH(req: Request, context: { params: { slug: string } }) { - const { status, webhook } = (await req.json()) as { - status: boolean; + const { enabled, webhook } = (await req.json()) as { + enabled: boolean; webhook: string; }; // If status is true, make sure webhook and roleId are not empty - if (status && !webhook) { + if (enabled && !webhook) { return NextResponse.json( { error: 'webhook is required when enabling Slack integration.' }, { status: 400 } ); } - // Update project config - const { data: updatedProjectConfig, error } = await updateProjectConfigBySlug( + // Update workspace config + const { data: updatedWorkspaceConfig, error } = await updateWorkspaceIntegrations( context.params.slug, { - integration_slack_status: status, - integration_slack_webhook: status ? webhook : null, + slack_enabled: enabled, + slack_webhook: enabled ? webhook : null, }, 'route' ); @@ -38,6 +38,6 @@ export async function PATCH(req: Request, context: { params: { slug: string } }) return NextResponse.json({ error: error.message }, { status: error.status }); } - // Return updated project config - return NextResponse.json(updatedProjectConfig, { status: 200 }); + // Return updated workspace config + return NextResponse.json(updatedWorkspaceConfig, { status: 200 }); } diff --git a/apps/web/app/api/v1/workspaces/[slug]/(config)/modules/route.ts b/apps/web/app/api/v1/workspaces/[slug]/(config)/modules/route.ts new file mode 100644 index 0000000..700e0f0 --- /dev/null +++ b/apps/web/app/api/v1/workspaces/[slug]/(config)/modules/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from 'next/server'; +import { getWorkspaceModuleConfig } from '@/lib/api/module'; + +/* + Get workspace module config + GET /api/v1/workspaces/[slug]/modules +*/ +export async function GET(req: Request, context: { params: { slug: string } }) { + // Get workspace module config + const { data: moduleConfig, error } = await getWorkspaceModuleConfig(context.params.slug, 'route'); + + if (error) { + return NextResponse.json(error, { status: error.status }); + } + + return NextResponse.json(moduleConfig, { status: 200 }); +} diff --git a/apps/web/app/api/v1/workspaces/[slug]/(config)/sso/route.ts b/apps/web/app/api/v1/workspaces/[slug]/(config)/sso/route.ts new file mode 100644 index 0000000..36ed41c --- /dev/null +++ b/apps/web/app/api/v1/workspaces/[slug]/(config)/sso/route.ts @@ -0,0 +1,32 @@ +import { NextResponse } from 'next/server'; +import { updateWorkspaceBySlug } from '@/lib/api/workspace'; + +/* + Update SSO configuration + PATCH /api/v1/workspaces/:slug/config/integrations/sso + { + enabled: boolean, + url: string + } +*/ +export async function PATCH(req: Request, context: { params: { slug: string } }) { + const { enabled, url } = await req.json(); + + // Update workspace config + const { data: updatedWorkspaceConfig, error } = await updateWorkspaceBySlug( + context.params.slug, + { + sso_auth_enabled: enabled, + sso_auth_url: url, + }, + 'route' + ); + + // If any errors thrown, return error + if (error) { + return NextResponse.json({ error: error.message }, { status: error.status }); + } + + // Return updated workspace config + return NextResponse.json(updatedWorkspaceConfig, { status: 200 }); +} diff --git a/apps/web/app/api/v1/workspaces/[slug]/(config)/sso/secret/route.ts b/apps/web/app/api/v1/workspaces/[slug]/(config)/sso/secret/route.ts new file mode 100644 index 0000000..eeb54b4 --- /dev/null +++ b/apps/web/app/api/v1/workspaces/[slug]/(config)/sso/secret/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from 'next/server'; +import { createWorkspaceSSOSecret } from '@/lib/api/workspace'; + +/* + Generate random jwt secret + POST /api/v1/workspaces/:slug/config/integrations/sso/secret +*/ +export async function POST(req: Request, context: { params: { slug: string } }) { + const { data, error } = await createWorkspaceSSOSecret(context.params.slug, 'route'); + + // If there is an error, return it + if (error) { + return NextResponse.json(error, { status: error.status }); + } + + return NextResponse.json(data, { status: 200 }); +} diff --git a/apps/web/app/api/v1/workspaces/[slug]/(config)/theme/route.ts b/apps/web/app/api/v1/workspaces/[slug]/(config)/theme/route.ts new file mode 100644 index 0000000..0f5eb8b --- /dev/null +++ b/apps/web/app/api/v1/workspaces/[slug]/(config)/theme/route.ts @@ -0,0 +1,63 @@ +import { NextResponse } from 'next/server'; +import { getWorkspaceTheme, updateWorkspaceTheme } from '@/lib/api/theme'; + +/* + Get workspace theme + GET /api/v1/workspaces/:slug/theme +*/ +export async function GET(req: Request, context: { params: { slug: string } }) { + // Get workspace theme + const { data: theme, error } = await getWorkspaceTheme(context.params.slug, 'route'); + + if (error) { + return NextResponse.json(error, { status: error.status }); + } + + return NextResponse.json(theme, { status: 200 }); +} + +/* + Update workspace theme + PATCH /api/v1/workspaces/:slug/theme + { + "accent": "string", + "background": "string", + "border": "string", + "foreground": "string", + "root": "string", + "secondary_background": "string", + "theme": "string" + } +*/ +export async function PATCH(req: Request, context: { params: { slug: string } }) { + const { + accent, + background, + border, + foreground, + root, + secondary_background: secondaryBackground, + theme, + } = await req.json(); + + // Update workspace theme + const { data: updatedTheme, error } = await updateWorkspaceTheme( + context.params.slug, + { + accent, + background, + border, + foreground, + root, + secondary_background: secondaryBackground, + theme, + }, + 'route' + ); + + if (error) { + return NextResponse.json({ error: error.message }, { status: error.status }); + } + + return NextResponse.json(updatedTheme, { status: 200 }); +} diff --git a/apps/web/app/api/v1/projects/[slug]/analytics/route.ts b/apps/web/app/api/v1/workspaces/[slug]/analytics/route.ts similarity index 82% rename from apps/web/app/api/v1/projects/[slug]/analytics/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/analytics/route.ts index 03d0ba4..eb95e97 100644 --- a/apps/web/app/api/v1/projects/[slug]/analytics/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/analytics/route.ts @@ -1,9 +1,9 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getProjectAnalytics } from '@/lib/api/projects'; +import { getWorkspaceAnalytics } from '@/lib/api/workspace'; /* Get Analytics - GET /api/v1/projects/:slug/analytics + GET /api/v1/workspaces/:slug/analytics */ export async function GET(req: NextRequest, context: { params: { slug: string } }) { // Check if tinybird variables are set @@ -20,7 +20,7 @@ export async function GET(req: NextRequest, context: { params: { slug: string } return NextResponse.json({ error: 'Invalid start or end date.' }, { status: 400 }); } - const { data: analyticsData, error } = await getProjectAnalytics(context.params.slug, 'route'); + const { data: analyticsData, error } = await getWorkspaceAnalytics(context.params.slug, 'route'); // If any errors thrown, return error if (error) { diff --git a/apps/web/app/api/v1/projects/[slug]/api-keys/[id]/route.ts b/apps/web/app/api/v1/workspaces/[slug]/api-keys/[id]/route.ts similarity index 55% rename from apps/web/app/api/v1/projects/[slug]/api-keys/[id]/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/api-keys/[id]/route.ts index 436bbf4..bcf2640 100644 --- a/apps/web/app/api/v1/projects/[slug]/api-keys/[id]/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/api-keys/[id]/route.ts @@ -1,12 +1,12 @@ import { NextResponse } from 'next/server'; -import { deleteProjectApiKey } from '@/lib/api/projects'; +import { deleteWorkspaceApiKey } from '@/lib/api/api-key'; /* - Delete api key for a project - DELETE /api/v1/projects/:slug/config/api/:token + Delete api key for a workspace + DELETE /api/v1/workspaces/:slug/config/api/:token */ export async function DELETE(req: Request, context: { params: { slug: string; id: string } }) { - const { error } = await deleteProjectApiKey(context.params.slug, context.params.id, 'route'); + const { error } = await deleteWorkspaceApiKey(context.params.slug, context.params.id, 'route'); if (error) { return NextResponse.json({ error }, { status: error.status }); diff --git a/apps/web/app/api/v1/projects/[slug]/api-keys/route.ts b/apps/web/app/api/v1/workspaces/[slug]/api-keys/route.ts similarity index 65% rename from apps/web/app/api/v1/projects/[slug]/api-keys/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/api-keys/route.ts index b40c674..28aab04 100644 --- a/apps/web/app/api/v1/projects/[slug]/api-keys/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/api-keys/route.ts @@ -1,13 +1,14 @@ import { NextResponse } from 'next/server'; -import { createProjectApiKey, getProjectApiKeys } from '@/lib/api/projects'; +import { createWorkspaceApiKey, getWorkspaceApiKeys } from '@/lib/api/api-key'; +import { ApiKeyPermissions } from '@/lib/types'; /* - Get all API keys for a project - GET /api/v1/projects/:slug/config/api + Get all API keys for a workspace + GET /api/v1/workspaces/:slug/config/api */ export async function GET(req: Request, context: { params: { slug: string } }) { // Get all api keys - const { data: apiKeys, error } = await getProjectApiKeys(context.params.slug, 'route'); + const { data: apiKeys, error } = await getWorkspaceApiKeys(context.params.slug, 'route'); if (error) { return NextResponse.json(error, { status: error.status }); @@ -17,15 +18,15 @@ export async function GET(req: Request, context: { params: { slug: string } }) { } /* - Create a new API key for a project - POST /api/v1/projects/:slug/config/api + Create a new API key for a workspace + POST /api/v1/workspaces/:slug/config/api { "name": "string", "permissions": "string" } */ export async function POST(req: Request, context: { params: { slug: string } }) { - const { name, permission } = (await req.json()) as { name: string; permission: string }; + const { name, permission } = (await req.json()) as { name: string; permission: ApiKeyPermissions }; // Validate input if (!name || !permission) { @@ -33,7 +34,7 @@ export async function POST(req: Request, context: { params: { slug: string } }) } // Create api key - const { data: apiKey, error } = await createProjectApiKey( + const { data: apiKey, error } = await createWorkspaceApiKey( context.params.slug, { name, permission }, 'route' diff --git a/apps/web/app/api/v1/workspaces/[slug]/boards/route.ts b/apps/web/app/api/v1/workspaces/[slug]/boards/route.ts new file mode 100644 index 0000000..3af8b5f --- /dev/null +++ b/apps/web/app/api/v1/workspaces/[slug]/boards/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from 'next/server'; +import { getWorkspaceBoards } from '@/lib/api/boards'; + +/* + Get all workspace boards + GET /api/v1/workspaces/[slug]/boards +*/ +export async function GET(req: Request, context: { params: { slug: string } }) { + const { data: boards, error } = await getWorkspaceBoards(context.params.slug, 'route', true, false); + + // If any errors thrown, return error + if (error) { + return NextResponse.json({ error: error.message }, { status: error.status }); + } + + // Return boards + return NextResponse.json(boards, { status: 200 }); +} diff --git a/apps/web/app/api/v1/projects/[slug]/changelogs/[id]/route.ts b/apps/web/app/api/v1/workspaces/[slug]/changelogs/[id]/route.ts similarity index 88% rename from apps/web/app/api/v1/projects/[slug]/changelogs/[id]/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/changelogs/[id]/route.ts index c454d15..842afd2 100644 --- a/apps/web/app/api/v1/projects/[slug]/changelogs/[id]/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/changelogs/[id]/route.ts @@ -1,11 +1,11 @@ import { NextResponse } from 'next/server'; -import { deleteChangelog, updateChangelog } from '@/lib/api/changelogs'; +import { deleteChangelog, updateChangelog } from '@/lib/api/changelog'; export const runtime = 'edge'; /* - Update project changelog - PUT /api/v1/projects/[slug]/changelogs/[id] + Update workspace changelog + PUT /api/v1/workspaces/[slug]/changelogs/[id] { title: string; summary: string; @@ -35,8 +35,8 @@ export async function PUT(req: Request, context: { params: { slug: string; id: s } /* - Delete project changelog - DELETE /api/v1/projects/[slug]/changelogs/[id] + Delete workspace changelog + DELETE /api/v1/workspaces/[slug]/changelogs/[id] */ export async function DELETE(req: Request, context: { params: { slug: string; id: string } }) { const { data, error } = await deleteChangelog(context.params.id, context.params.slug, 'route'); diff --git a/apps/web/app/api/v1/projects/[slug]/changelogs/route.ts b/apps/web/app/api/v1/workspaces/[slug]/changelogs/route.ts similarity index 74% rename from apps/web/app/api/v1/projects/[slug]/changelogs/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/changelogs/route.ts index bd84f37..8689301 100644 --- a/apps/web/app/api/v1/projects/[slug]/changelogs/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/changelogs/route.ts @@ -1,12 +1,12 @@ import { NextResponse } from 'next/server'; -import { createChangelog, getAllProjectChangelogs } from '@/lib/api/changelogs'; +import { createChangelog, getAllWorkspaceChangelogs } from '@/lib/api/changelog'; import { ChangelogProps } from '@/lib/types'; export const runtime = 'edge'; /* Create Changelog - POST /api/v1/projects/[slug]/changelogs + POST /api/v1/workspaces/[slug]/changelogs { title: string; summary: string; @@ -21,10 +21,11 @@ export async function POST(req: Request, context: { params: { slug: string } }) title, summary, content, - image, + thumbnail, publish_date: publishDate, published, - } = (await req.json()) as ChangelogProps['Insert']; + notify_subscribers: notifySubscribers, + } = (await req.json()) as ChangelogProps['Insert'] & { notify_subscribers: boolean }; // Validate Request Body if (published) { @@ -42,13 +43,14 @@ export async function POST(req: Request, context: { params: { slug: string } }) title: title || '', summary: summary || '', content: content || '', - image: image || '', + thumbnail: thumbnail || null, publish_date: publishDate || null, published: published || false, - project_id: 'dummy-id', + workspace_id: 'dummy-id', slug: 'dummy-slug', author_id: 'dummy-author', }, + notifySubscribers, 'route' ); @@ -62,11 +64,11 @@ export async function POST(req: Request, context: { params: { slug: string } }) } /* - Get project changelogs - GET /api/v1/projects/[slug]/changelogs + Get workspace changelogs + GET /api/v1/workspaces/[slug]/changelogs */ export async function GET(req: Request, context: { params: { slug: string } }) { - const { data: changelogs, error } = await getAllProjectChangelogs(context.params.slug, 'route', true); + const { data: changelogs, error } = await getAllWorkspaceChangelogs(context.params.slug, 'route', true); // If any errors thrown, return error if (error) { diff --git a/apps/web/app/api/v1/workspaces/[slug]/changelogs/subscribers/count/route.ts b/apps/web/app/api/v1/workspaces/[slug]/changelogs/subscribers/count/route.ts new file mode 100644 index 0000000..de0a23a --- /dev/null +++ b/apps/web/app/api/v1/workspaces/[slug]/changelogs/subscribers/count/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from 'next/server'; +import { getChangelogSubscribers } from '@/lib/api/changelog'; + +/* + Get the subscribers count for a workspace changelog + GET /api/v1/workspaces/:slug/changelogs/subscribers/count +*/ +export async function GET(req: Request, context: { params: { slug: string } }) { + const { data: subscribers, error } = await getChangelogSubscribers(context.params.slug, 'route'); + + // If any errors thrown, return error + if (error) { + return new Response(error.message, { status: error.status }); + } + + return NextResponse.json({ count: subscribers.length }); +} diff --git a/apps/web/app/api/v1/projects/[slug]/changelogs/subscribers/route.ts b/apps/web/app/api/v1/workspaces/[slug]/changelogs/subscribers/route.ts similarity index 84% rename from apps/web/app/api/v1/projects/[slug]/changelogs/subscribers/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/changelogs/subscribers/route.ts index 914c1f1..ab2621b 100644 --- a/apps/web/app/api/v1/projects/[slug]/changelogs/subscribers/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/changelogs/subscribers/route.ts @@ -1,8 +1,8 @@ -import { getChangelogSubscribers } from '@/lib/api/changelogs'; +import { getChangelogSubscribers } from '@/lib/api/changelog'; /* Download changelog subscribers - GET /api/v1/projects/:slug/changelogs/subscribers/download + GET /api/v1/workspaces/:slug/changelogs/subscribers/download */ export async function GET(req: Request, context: { params: { slug: string } }) { const { data: subscribers, error } = await getChangelogSubscribers(context.params.slug, 'route'); diff --git a/apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/comments/[commentId]/route.ts b/apps/web/app/api/v1/workspaces/[slug]/feedback/[feedbackId]/comments/[commentId]/route.ts similarity index 82% rename from apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/comments/[commentId]/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/feedback/[feedbackId]/comments/[commentId]/route.ts index c8e2f0b..36bc132 100644 --- a/apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/comments/[commentId]/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/feedback/[feedbackId]/comments/[commentId]/route.ts @@ -1,9 +1,9 @@ import { NextResponse } from 'next/server'; -import { deleteCommentForFeedbackById } from '@/lib/api/comments'; +import { deleteCommentForFeedbackById } from '@/lib/api/comment'; /* Delete comment for feedback by id - DELETE /api/v1/projects/[slug]/feedback/[id]/comments/[id] + DELETE /api/v1/workspaces/[slug]/feedback/[id]/comments/[id] */ export async function DELETE( req: Request, diff --git a/apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/comments/[commentId]/upvote/route.ts b/apps/web/app/api/v1/workspaces/[slug]/feedback/[feedbackId]/comments/[commentId]/upvote/route.ts similarity index 81% rename from apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/comments/[commentId]/upvote/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/feedback/[feedbackId]/comments/[commentId]/upvote/route.ts index 3c2506b..0c00268 100644 --- a/apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/comments/[commentId]/upvote/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/feedback/[feedbackId]/comments/[commentId]/upvote/route.ts @@ -1,9 +1,9 @@ import { NextResponse } from 'next/server'; -import { upvoteCommentForFeedbackById } from '@/lib/api/comments'; +import { upvoteCommentForFeedbackById } from '@/lib/api/comment'; /* Upvote comment for feedback by id - POST /api/v1/projects/[slug]/feedback/[id]/comments/[id]/upvote + POST /api/v1/workspaces/[slug]/feedback/[id]/comments/[id]/upvote */ export async function POST( req: Request, diff --git a/apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/comments/route.ts b/apps/web/app/api/v1/workspaces/[slug]/feedback/[feedbackId]/comments/route.ts similarity index 85% rename from apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/comments/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/feedback/[feedbackId]/comments/route.ts index e9b38f9..5ac00b2 100644 --- a/apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/comments/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/feedback/[feedbackId]/comments/route.ts @@ -1,16 +1,17 @@ import { NextResponse } from 'next/server'; -import { createCommentForFeedbackById, getCommentsForFeedbackById } from '@/lib/api/comments'; -import { FeedbackCommentProps } from '@/lib/types'; +import { createCommentForFeedbackById, getCommentsForFeedbackById } from '@/lib/api/comment'; +import { CommentProps } from '@/lib/types'; /* Create feedback comment - POST /api/v1/projects/[slug]/feedback/[id]/comments + POST /api/v1/workspaces/[slug]/feedback/[id]/comments { content: string + reply_to_id?: string } */ export async function POST(req: Request, context: { params: { slug: string; feedbackId: string } }) { - const { content, reply_to_id: replyToId } = (await req.json()) as FeedbackCommentProps['Insert']; + const { content, reply_to_id: replyToId } = (await req.json()) as CommentProps['Insert']; if (!content) { return NextResponse.json({ error: 'Content cannot be empty' }, { status: 400 }); @@ -38,7 +39,7 @@ export async function POST(req: Request, context: { params: { slug: string; feed /* Get feedback comments - GET /api/v1/projects/[slug]/feedback/[id]/comments + GET /api/v1/workspaces/[slug]/feedback/[id]/comments */ export async function GET(req: Request, context: { params: { slug: string; feedbackId: string } }) { const { data: comments, error } = await getCommentsForFeedbackById( diff --git a/apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/route.ts b/apps/web/app/api/v1/workspaces/[slug]/feedback/[feedbackId]/route.ts similarity index 69% rename from apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/feedback/[feedbackId]/route.ts index d75433d..04c859d 100644 --- a/apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/feedback/[feedbackId]/route.ts @@ -1,13 +1,13 @@ import { NextResponse } from 'next/server'; -import { deleteFeedbackByID, getFeedbackByID, updateFeedbackByID } from '@/lib/api/feedback'; +import { deleteFeedbackById, getFeedbackById, updateFeedbackByID } from '@/lib/api/feedback'; import { FeedbackWithUserInputProps } from '@/lib/types'; /* - Get Project Feedback by ID - GET /api/v1/projects/[slug]/feedback/[id] + Get Workspace Feedback by ID + GET /api/v1/workspaces/[slug]/feedback/[id] */ export async function GET(req: Request, context: { params: { slug: string; feedbackId: string } }) { - const { data: feedback, error } = await getFeedbackByID( + const { data: feedback, error } = await getFeedbackById( context.params.feedbackId, context.params.slug, 'route' @@ -24,27 +24,35 @@ export async function GET(req: Request, context: { params: { slug: string; feedb /* Update Feedback by ID - PATCH /api/v1/projects/[slug]/feedback/[id] + PATCH /api/v1/workspaces/[slug]/feedback/[id] { title: string; - description: string; + content: string; status: string; tags: string[]; + board_id: string; } */ export async function PATCH(req: Request, context: { params: { slug: string; feedbackId: string } }) { - const { title, description, status, tags } = (await req.json()) as FeedbackWithUserInputProps; + const { + title, + content, + status, + tags, + board_id: boardId, + } = (await req.json()) as FeedbackWithUserInputProps; const { data: feedback, error } = await updateFeedbackByID( context.params.feedbackId, context.params.slug, { title: title || '', - description: description || '', - status, - project_id: 'dummy-id', + content: content || '', + status: status?.toLowerCase() as FeedbackWithUserInputProps['status'], + board_id: boardId || '', user_id: 'dummy-id', tags: tags || undefined, + workspace_id: 'dummy-id', }, 'route' ); @@ -60,10 +68,10 @@ export async function PATCH(req: Request, context: { params: { slug: string; fee /* Delete Feedback by ID - DELETE /api/v1/projects/[slug]/feedback/[id] + DELETE /api/v1/workspaces/[slug]/feedback/[id] */ export async function DELETE(req: Request, context: { params: { slug: string; feedbackId: string } }) { - const { data: feedback, error } = await deleteFeedbackByID( + const { data: feedback, error } = await deleteFeedbackById( context.params.feedbackId, context.params.slug, 'route' diff --git a/apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/upvotes/route.ts b/apps/web/app/api/v1/workspaces/[slug]/feedback/[feedbackId]/upvotes/route.ts similarity index 73% rename from apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/upvotes/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/feedback/[feedbackId]/upvotes/route.ts index 146768d..d3244af 100644 --- a/apps/web/app/api/v1/projects/[slug]/feedback/[feedbackId]/upvotes/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/feedback/[feedbackId]/upvotes/route.ts @@ -1,13 +1,12 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getFeedbackByID, getFeedbackUpvotersById, upvoteFeedbackByID } from '@/lib/api/feedback'; -import { getCurrentUser } from '@/lib/api/user'; +import { getFeedbackById, getFeedbackUpvotersById, upvoteFeedbackByID } from '@/lib/api/feedback'; /* Get feedback upvotes - GET /api/v1/projects/[slug]/feedback/[id]/upvote + GET /api/v1/workspaces/[slug]/feedback/[id]/upvotes */ export async function GET(req: Request, context: { params: { slug: string; feedbackId: string } }) { - const { data: feedback, error } = await getFeedbackByID( + const { data: feedback, error } = await getFeedbackById( context.params.feedbackId, context.params.slug, 'route' @@ -42,21 +41,14 @@ export async function GET(req: Request, context: { params: { slug: string; feedb /* Upvote a feedback - POST /api/v1/projects/[slug]/feedback/[id]/upvote + POST /api/v1/workspaces/[slug]/feedback/[id]/upvotes */ export async function POST(req: NextRequest, context: { params: { slug: string; feedbackId: string } }) { - const hasUpvoted = req.nextUrl.searchParams.get('has_upvoted'); - - // Get current user - const { data: isLoggedIn } = await getCurrentUser('route'); - // Upvote feedback const { data: feedback, error } = await upvoteFeedbackByID( context.params.feedbackId, context.params.slug, - 'route', - hasUpvoted ? hasUpvoted === 'true' : undefined, - !isLoggedIn + 'route' ); // If any errors thrown, return error diff --git a/apps/web/app/api/v1/projects/[slug]/feedback/export/route.ts b/apps/web/app/api/v1/workspaces/[slug]/feedback/export/route.ts similarity index 78% rename from apps/web/app/api/v1/projects/[slug]/feedback/export/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/feedback/export/route.ts index 1eebff8..166bf20 100644 --- a/apps/web/app/api/v1/projects/[slug]/feedback/export/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/feedback/export/route.ts @@ -1,12 +1,12 @@ -import { getAllProjectFeedback } from '@/lib/api/feedback'; +import { getAllWorkspaceFeedback } from '@/lib/api/feedback'; import { formatRootUrl } from '@/lib/utils'; /* - Export all feedback for a project - GET /api/v1/projects/:slug/feedback/export + Export all feedback for a workspace + GET /api/v1/workspaces/:slug/feedback/export */ export async function GET(req: Request, context: { params: { slug: string } }) { - const { data: feedback, error } = await getAllProjectFeedback(context.params.slug, 'route'); + const { data: feedback, error } = await getAllWorkspaceFeedback(context.params.slug, 'route'); // If any errors thrown, return error if (error) { @@ -19,7 +19,7 @@ export async function GET(req: Request, context: { params: { slug: string } }) { return `${feedback.id},${formatRootUrl( context.params.slug, `/feedback/${feedback.id}` - )},"${feedback.title.replace(/"/g, '""')}","${feedback.description.replace(/"/g, '""')}",${ + )},"${feedback.title.replace(/"/g, '""')}","${feedback.content.replace(/"/g, '""')}",${ feedback.status },${feedback.upvotes},${feedback.comment_count},${feedback.user_id},"${feedback.user.full_name.replace( /"/g, diff --git a/apps/web/app/api/v1/workspaces/[slug]/feedback/route.ts b/apps/web/app/api/v1/workspaces/[slug]/feedback/route.ts new file mode 100644 index 0000000..9f8bd23 --- /dev/null +++ b/apps/web/app/api/v1/workspaces/[slug]/feedback/route.ts @@ -0,0 +1,91 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createFeedback, getAllBoardFeedback, getAllWorkspaceFeedback } from '@/lib/api/feedback'; +import { FeedbackWithUserInputProps } from '@/lib/types'; + +export const runtime = 'edge'; + +/* + Create Feedback + POST /api/v1/workspaces/[slug]/feedback + { + title: string; + content: string; + status: string; + tags: [id, id, id], + board_id?: string; + } +*/ +export async function POST(req: Request, context: { params: { slug: string } }) { + const { + title, + content, + status, + tags, + user, + board_id: boardId, + } = (await req.json()) as FeedbackWithUserInputProps; + + // Validate Request Body + if (!title) { + return NextResponse.json({ error: 'title is required.' }, { status: 400 }); + } + + const { data: feedback, error } = await createFeedback( + boardId, + context.params.slug, + { + title: title || '', + content: content || '', + status: status?.toLowerCase() as FeedbackWithUserInputProps['status'], + board_id: boardId || '', + workspace_id: context.params.slug, + user_id: 'dummy-id', + tags: tags || [], + user: user !== null ? user : undefined, + }, + 'route' + ); + + // If any errors thrown, return error + if (error) { + return NextResponse.json({ error: error.message }, { status: error.status }); + } + + // Return feedback + return NextResponse.json(feedback, { status: 200 }); +} + +/* + Get Workspace Feedback + GET /api/v1/workspaces/[slug]/feedback + Query Parameters: + - b (board_id): string +*/ +export async function GET(req: NextRequest, context: { params: { slug: string } }) { + // Get query parameters + const boardId = req.nextUrl.searchParams.get('b'); + + // If boardId is provided, get feedback by boardId + if (boardId) { + const { data: feedback, error } = await getAllBoardFeedback(boardId, context.params.slug, 'route'); + + // If any errors thrown, return error + if (error) { + return NextResponse.json({ error: error.message }, { status: error.status }); + } + + // Return feedback + return NextResponse.json(feedback, { status: 200 }); + } + + // Get all feedback + const { data: feedback, error } = await getAllWorkspaceFeedback(context.params.slug, 'route'); + + // If any errors thrown, return error + if (error) { + return NextResponse.json({ error: error.message }, { status: error.status }); + } + + // Return feedback + return NextResponse.json(feedback, { status: 200 }); +} diff --git a/apps/web/app/api/v1/projects/[slug]/feedback/tags/[name]/route.ts b/apps/web/app/api/v1/workspaces/[slug]/feedback/tags/[name]/route.ts similarity index 91% rename from apps/web/app/api/v1/projects/[slug]/feedback/tags/[name]/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/feedback/tags/[name]/route.ts index 0713c06..0d3608a 100644 --- a/apps/web/app/api/v1/projects/[slug]/feedback/tags/[name]/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/feedback/tags/[name]/route.ts @@ -3,7 +3,7 @@ import { deleteFeedbackTagByName } from '@/lib/api/feedback'; /* Delete tag by name - DELETE /api/v1/projects/:slug/feedback/tags/:name + DELETE /api/v1/workspaces/:slug/feedback/tags/:name */ export async function DELETE(req: Request, context: { params: { slug: string; name: string } }) { const { data: tag, error } = await deleteFeedbackTagByName( diff --git a/apps/web/app/api/v1/projects/[slug]/feedback/tags/route.ts b/apps/web/app/api/v1/workspaces/[slug]/feedback/tags/route.ts similarity index 89% rename from apps/web/app/api/v1/projects/[slug]/feedback/tags/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/feedback/tags/route.ts index e6bc865..41af1a8 100644 --- a/apps/web/app/api/v1/projects/[slug]/feedback/tags/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/feedback/tags/route.ts @@ -4,7 +4,7 @@ import { FeedbackTagProps } from '@/lib/types'; /* Create new tag - POST /api/v1/projects/:slug/feedback/tags + POST /api/v1/workspaces/:slug/feedback/tags { name: string, color: string @@ -33,10 +33,10 @@ export async function POST(req: Request, context: { params: { slug: string } }) /* Get all feedback tags - GET /api/v1/projects/:slug/feedback/tags + GET /api/v1/workspaces/:slug/feedback/tags */ export async function GET(req: Request, context: { params: { slug: string } }) { - const { data: tags, error } = await getAllFeedbackTags(context.params.slug, 'route'); + const { data: tags, error } = await getAllFeedbackTags(context.params.slug, 'route', true, false); // If any errors thrown, return error if (error) { diff --git a/apps/web/app/api/v1/projects/[slug]/invites/[inviteId]/route.ts b/apps/web/app/api/v1/workspaces/[slug]/invites/[inviteId]/route.ts similarity index 61% rename from apps/web/app/api/v1/projects/[slug]/invites/[inviteId]/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/invites/[inviteId]/route.ts index 1e66f71..b40a54f 100644 --- a/apps/web/app/api/v1/projects/[slug]/invites/[inviteId]/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/invites/[inviteId]/route.ts @@ -1,12 +1,12 @@ import { NextResponse } from 'next/server'; -import { acceptProjectInvite, deleteProjectInvite } from '@/lib/api/invites'; +import { acceptWorkspaceInvite, deleteWorkspaceInvite } from '@/lib/api/invite'; /* - Accept project invite - POST /api/v1/projects/[slug]/invites/[inviteId] + Accept workspace invite + POST /api/v1/workspaces/[slug]/invites/[inviteId] */ export async function POST(req: Request, context: { params: { slug: string; inviteId: string } }) { - const { data: invite, error } = await acceptProjectInvite(context.params.inviteId, 'route'); + const { data: invite, error } = await acceptWorkspaceInvite(context.params.inviteId, 'route'); // If any errors thrown, return error if (error) { @@ -18,11 +18,11 @@ export async function POST(req: Request, context: { params: { slug: string; invi } /* - Delete project invite - DELETE /api/v1/projects/[slug]/invites/[inviteId] + Delete workspace invite + DELETE /api/v1/workspaces/[slug]/invites/[inviteId] */ export async function DELETE(req: Request, context: { params: { slug: string; inviteId: string } }) { - const { data: invite, error } = await deleteProjectInvite(context.params.inviteId, 'route'); + const { data: invite, error } = await deleteWorkspaceInvite(context.params.inviteId, 'route'); // If any errors thrown, return error if (error) { diff --git a/apps/web/app/api/v1/projects/[slug]/invites/route.ts b/apps/web/app/api/v1/workspaces/[slug]/invites/route.ts similarity index 55% rename from apps/web/app/api/v1/projects/[slug]/invites/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/invites/route.ts index 74256dc..65def20 100644 --- a/apps/web/app/api/v1/projects/[slug]/invites/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/invites/route.ts @@ -1,12 +1,12 @@ import { NextResponse } from 'next/server'; -import { createProjectInvite, getProjectInvites } from '@/lib/api/invites'; +import { createWorkspaceInvite, getWorkspaceInvites } from '@/lib/api/invite'; /* - Get all project invites - GET /api/v1/projects/[slug]/invites + Get all workspace invites + GET /api/v1/workspaces/[slug]/invites */ export async function GET(req: Request, context: { params: { slug: string } }) { - const { data: invites, error } = await getProjectInvites(context.params.slug, 'route'); + const { data: invites, error } = await getWorkspaceInvites(context.params.slug, 'route'); // If any errors thrown, return error if (error) { @@ -18,8 +18,8 @@ export async function GET(req: Request, context: { params: { slug: string } }) { } /* - Invite a new member to a project - POST /api/v1/projects/[slug]/members + Invite a new member to a workspace + POST /api/v1/workspaces/[slug]/members { email: string } @@ -27,14 +27,8 @@ export async function GET(req: Request, context: { params: { slug: string } }) { export async function POST(req: Request, context: { params: { slug: string } }) { const { email } = await req.json(); - // Check if email is valid - const emailRegex = new RegExp(/^[^\s@]+@[^\s@]+\.[^\s@]+$/); - if (!emailRegex.test(email)) { - return NextResponse.json({ error: 'Invalid email' }, { status: 400 }); - } - // Create invite - const { data: invite, error } = await createProjectInvite(context.params.slug, 'route', email); + const { data: invite, error } = await createWorkspaceInvite(context.params.slug, 'route', email); // If any errors thrown, return error if (error) { diff --git a/apps/web/app/api/v1/projects/[slug]/members/route.ts b/apps/web/app/api/v1/workspaces/[slug]/members/route.ts similarity index 60% rename from apps/web/app/api/v1/projects/[slug]/members/route.ts rename to apps/web/app/api/v1/workspaces/[slug]/members/route.ts index cfa5b2c..e73da78 100644 --- a/apps/web/app/api/v1/projects/[slug]/members/route.ts +++ b/apps/web/app/api/v1/workspaces/[slug]/members/route.ts @@ -1,12 +1,12 @@ import { NextResponse } from 'next/server'; -import { getProjectMembers } from '@/lib/api/projects'; +import { getWorkspaceMembers } from '@/lib/api/workspace'; /* - Get all members of a project - GET /api/v1/projects/[slug]/members + Get all members of a workspace + GET /api/v1/workspaces/[slug]/members */ export async function GET(req: Request, context: { params: { slug: string } }) { - const { data: members, error } = await getProjectMembers(context.params.slug, 'route'); + const { data: members, error } = await getWorkspaceMembers(context.params.slug, 'route'); // If any errors thrown, return error if (error) { diff --git a/apps/web/app/api/v1/workspaces/[slug]/route.ts b/apps/web/app/api/v1/workspaces/[slug]/route.ts new file mode 100644 index 0000000..309dac2 --- /dev/null +++ b/apps/web/app/api/v1/workspaces/[slug]/route.ts @@ -0,0 +1,81 @@ +import { NextResponse } from 'next/server'; +import { deleteWorkspaceBySlug, getWorkspaceBySlug, updateWorkspaceBySlug } from '@/lib/api/workspace'; +import { WorkspaceProps } from '@/lib/types'; + +/* + Get workspace by slug + GET /api/v1/workspaces/[slug] +*/ +export async function GET(req: Request, context: { params: { slug: string } }) { + const { data: workspace, error } = await getWorkspaceBySlug(context.params.slug, 'route'); + // If any errors thrown, return error + if (error) { + return NextResponse.json({ error: error.message }, { status: error.status }); + } + + // Return workspace + return NextResponse.json(workspace, { status: 200 }); +} + +/* + Update workspace by slug + PATCH /api/v1/workspaces/[slug] + { + name: string, + slug: string, + icon: string, + icon_radius: string, + og_image: string + icon_redirect_url: string + custom_domain_redirect: string + } +*/ +export async function PATCH(req: Request, context: { params: { slug: string } }) { + const { + name, + slug, + icon, + icon_radius: iconRadius, + opengraph_image: OGImage, + icon_redirect_url: iconRedirectURL, + custom_domain_redirect: customDomainRedirect, + } = (await req.json()) as WorkspaceProps['Update']; + + const { data: updatedWorkspace, error } = await updateWorkspaceBySlug( + context.params.slug, + { + name, + slug, + icon, + icon_radius: iconRadius, + opengraph_image: OGImage, + icon_redirect_url: iconRedirectURL, + custom_domain_redirect: customDomainRedirect, + }, + 'route' + ); + + // If any errors thrown, return error + if (error) { + return NextResponse.json({ error: error.message }, { status: error.status }); + } + + // Return updated workspace + return NextResponse.json(updatedWorkspace, { status: 200 }); +} + +/* + Delete workspace by slug + DELETE /api/v1/workspaces/[slug] +*/ +export async function DELETE(req: Request, context: { params: { slug: string } }) { + const { data, error } = await deleteWorkspaceBySlug(context.params.slug, 'route'); + + // If any errors thrown, return error + if (error) { + return NextResponse.json({ error: error.message }, { status: error.status }); + } + + // Return success + return NextResponse.json({ data }, { status: 200 }); +} diff --git a/apps/web/app/api/v1/projects/route.ts b/apps/web/app/api/v1/workspaces/route.ts similarity index 50% rename from apps/web/app/api/v1/projects/route.ts rename to apps/web/app/api/v1/workspaces/route.ts index f65280b..d10836d 100644 --- a/apps/web/app/api/v1/projects/route.ts +++ b/apps/web/app/api/v1/workspaces/route.ts @@ -1,13 +1,13 @@ import { NextResponse } from 'next/server'; -import { createProject } from '@/lib/api/projects'; -import { getUserProjects } from '@/lib/api/user'; +import { getUserWorkspaces } from '@/lib/api/user'; +import { createWorkspace } from '@/lib/api/workspace'; /* - Create Project - POST /api/v1/projects + Create Workspace + POST /api/v1/workspaces { - "name": "Project Name", - "slug": "project-slug", + "name": "Workspace Name", + "slug": "workspace-slug", } */ export async function POST(req: Request) { @@ -19,30 +19,30 @@ export async function POST(req: Request) { return NextResponse.json({ error: 'name and slug are required.' }, { status: 400 }); } - // Create Project - const { data: project, error } = await createProject({ name, slug }, 'route'); + // Create Workspace + const { data: workspace, error } = await createWorkspace({ name, slug }, 'route'); // Check for errors if (error) { return NextResponse.json({ error: error.message }, { status: error.status }); } - return NextResponse.json(project, { status: 201 }); + return NextResponse.json(workspace, { status: 201 }); } /* - Get User Projects - GET /api/v1/projects + Get User Workspaces + GET /api/v1/workspaces */ export async function GET(req: Request) { - // Get User Projects - const { data: projects, error } = await getUserProjects('route'); + // Get User Workspaces + const { data: workspaces, error } = await getUserWorkspaces('route'); // Check for errors if (error) { return NextResponse.json({ error: error.message }, { status: error.status }); } - // Return projects - return NextResponse.json(projects, { status: 200 }); + // Return workspaces + return NextResponse.json(workspaces, { status: 200 }); } diff --git a/apps/web/app/dash/(auth)/login/page.tsx b/apps/web/app/dash/(auth)/login/page.tsx index 951c959..129dc9e 100644 --- a/apps/web/app/dash/(auth)/login/page.tsx +++ b/apps/web/app/dash/(auth)/login/page.tsx @@ -2,9 +2,16 @@ import { Metadata } from 'next'; import { cookies } from 'next/headers'; import Link from 'next/link'; import { redirect } from 'next/navigation'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@feedbase/ui/components/card'; import { createServerClient } from '@supabase/ssr'; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui/components/ui/card'; -import { UserAuthForm } from '@/components/user-auth-form'; +import { UserAuthForm } from '@/components/shared/user-auth-form'; export const metadata: Metadata = { title: 'Sign in to Feedbase', @@ -31,7 +38,7 @@ export default async function SignIn() { data: { user }, } = await supabase.auth.getUser(); - // If there is a session, redirect to projects + // If there is a session, redirect to workspaces if (user) { redirect('/'); } diff --git a/apps/web/app/dash/(auth)/signup/page.tsx b/apps/web/app/dash/(auth)/signup/page.tsx index 912ed81..ffa038d 100644 --- a/apps/web/app/dash/(auth)/signup/page.tsx +++ b/apps/web/app/dash/(auth)/signup/page.tsx @@ -2,9 +2,16 @@ import { Metadata } from 'next'; import { cookies } from 'next/headers'; import Link from 'next/link'; import { redirect } from 'next/navigation'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@feedbase/ui/components/card'; import { createServerClient } from '@supabase/ssr'; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui/components/ui/card'; -import { UserAuthForm } from '@/components/user-auth-form'; +import { UserAuthForm } from '@/components/shared/user-auth-form'; export const metadata: Metadata = { title: 'Sign up to Feedbase', @@ -31,7 +38,7 @@ export default async function SignUp() { data: { user }, } = await supabase.auth.getUser(); - // If there is a session, redirect to projects + // If there is a session, redirect to workspaces if (user) { redirect('/'); } diff --git a/apps/web/app/dash/[slug]/analytics/loading.tsx b/apps/web/app/dash/[slug]/analytics/loading.tsx deleted file mode 100644 index 50dd5dd..0000000 --- a/apps/web/app/dash/[slug]/analytics/loading.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Skeleton } from '@ui/components/ui/skeleton'; - -export default function FeedbackLoading() { - return ( -
-
- - -
- -
- - -
-
- ); -} diff --git a/apps/web/app/dash/[slug]/analytics/page.tsx b/apps/web/app/dash/[slug]/analytics/page.tsx index 631e1be..35cebde 100644 --- a/apps/web/app/dash/[slug]/analytics/page.tsx +++ b/apps/web/app/dash/[slug]/analytics/page.tsx @@ -1,21 +1,55 @@ import React from 'react'; -import { getProjectAnalytics } from '@/lib/api/projects'; -import AnalyticsCards from '@/components/dashboard/analytics/chart-cards'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@feedbase/ui/components/select'; +import { getWorkspaceAnalytics } from '@/lib/api/workspace'; +import { BarChart } from '@/components/analytics/bar-chart'; +import { LineChart } from '@/components/analytics/line-chart'; export default async function AnalyticsPage({ params }: { params: { slug: string } }) { - const { data, error } = await getProjectAnalytics(params.slug, 'server'); + const { data, error } = await getWorkspaceAnalytics(params.slug, 'server'); if (!data) { return
Error: {error?.message}
; } return ( -
- +
+ {/* Header */} +
+

Analytics

+ +
+ {/* Timeframe */} + +
+
+ + {/* Chart */} +
+ + + +
); } diff --git a/apps/web/app/dash/[slug]/changelog/loading.tsx b/apps/web/app/dash/[slug]/changelog/loading.tsx deleted file mode 100644 index 7e7f0a9..0000000 --- a/apps/web/app/dash/[slug]/changelog/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Skeleton } from '@ui/components/ui/skeleton'; - -export default function FeedbackLoading() { - return ; -} diff --git a/apps/web/app/dash/[slug]/changelog/page.tsx b/apps/web/app/dash/[slug]/changelog/page.tsx index e56db5e..d715ba5 100644 --- a/apps/web/app/dash/[slug]/changelog/page.tsx +++ b/apps/web/app/dash/[slug]/changelog/page.tsx @@ -1,67 +1,36 @@ +import { Button } from '@feedbase/ui/components/button'; +import { Separator } from '@feedbase/ui/components/separator'; import { Plus } from 'lucide-react'; -import { Button } from 'ui/components/ui/button'; -import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui/components/ui/card'; -import { Separator } from 'ui/components/ui/separator'; -import { getAllProjectChangelogs } from '@/lib/api/changelogs'; -import { ApiSheet } from '@/components/dashboard/changelogs/api-sheet'; -import ChangelogList from '@/components/dashboard/changelogs/changelog-list'; -import { AddChangelogModal } from '@/components/dashboard/modals/add-edit-changelog-modal'; - -export default async function Changelog({ params }: { params: { slug: string } }) { - const { data: changelogs, error } = await getAllProjectChangelogs(params.slug, 'server'); - - if (error) { - return
{error.message}
; - } +import { ApiSheet } from '@/components/changelog/api-sheet'; +import ChangelogList from '@/components/changelog/changelog-list'; +import { AddChangelogModal } from '@/components/modals/add-edit-changelog-modal'; +export default function Changelog({ params }: { params: { slug: string } }) { return ( -
- {/* Content */} -
- {/* Header Row */} - {changelogs.length > 0 && ( -
- {/* Api Docs Button */} - +
+ {/* Header */} +
+

Changelogs

- {/* Seperator Line */} - +
+ {/* Api Docs Button */} + - {/* Create new Button */} - - - New Changelog - - } - /> -
- )} + {/* Seperator Line */} + - {/* Changelog List */} - {/* If there is no changelog, show empty text in the center */} - {changelogs.length === 0 && ( - - - No changelogs yet - - Once you create a changelog, it will show up here. - - - - Create first changelog} - /> - - - )} - - {/* If there is changelog, show changelog list */} - {changelogs.length > 0 && } + {/* Create new Button */} + + + +
+ + {/* Changelogs */} +
); } diff --git a/apps/web/app/dash/[slug]/feedback/loading.tsx b/apps/web/app/dash/[slug]/feedback/loading.tsx deleted file mode 100644 index ea9545c..0000000 --- a/apps/web/app/dash/[slug]/feedback/loading.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Skeleton } from '@ui/components/ui/skeleton'; - -export default function FeedbackLoading() { - return ( - <> - - - - ); -} diff --git a/apps/web/app/dash/[slug]/feedback/page.tsx b/apps/web/app/dash/[slug]/feedback/page.tsx index 700c37d..ea01880 100644 --- a/apps/web/app/dash/[slug]/feedback/page.tsx +++ b/apps/web/app/dash/[slug]/feedback/page.tsx @@ -1,46 +1,14 @@ -import { Card, CardDescription, CardHeader, CardTitle } from 'ui/components/ui/card'; -import { getAllFeedbackTags, getAllProjectFeedback } from '@/lib/api/feedback'; -import FeedbackTable from '@/components/dashboard/feedback/feedback-table'; -import FeedbackHeader from '@/components/dashboard/feedback/header-buttons'; - -export default async function Feedback({ - params, - searchParams, -}: { - params: { slug: string }; - searchParams: { tag: string }; -}) { - const { data: feedback, error } = await getAllProjectFeedback(params.slug, 'server'); - if (error) { - return
{error.message}
; - } - - const { data: tags, error: tagsError } = await getAllFeedbackTags(params.slug, 'server'); - if (tagsError) { - return
{tagsError.message}
; - } +import FeedbackHeader from '@/components/feedback/dashboard/feedback-header'; +import FeedbackList from '@/components/feedback/dashboard/feedback-list'; +export default async function Feedback() { return ( -
- {/* Header Row */} - - - {/* Feedback Posts */} - {/* If there is no feedback, show empty text in the center */} - {feedback.length === 0 && ( - - - No feedback yet - - Once somone submits feedback, it will show up here. Make sure to share it so others can vote on - it! - - - - )} +
+ {/* Header */} + - {/* If there is feedback, show feedback list */} - {feedback.length > 0 && } + {/* Feedback List */} +
); } diff --git a/apps/web/app/dash/[slug]/layout.tsx b/apps/web/app/dash/[slug]/layout.tsx index aa90d86..eebd0d6 100644 --- a/apps/web/app/dash/[slug]/layout.tsx +++ b/apps/web/app/dash/[slug]/layout.tsx @@ -1,11 +1,11 @@ import { headers } from 'next/headers'; import { redirect } from 'next/navigation'; -import { getCurrentUser, getUserProjects } from '@/lib/api/user'; +import { getCurrentUser, getUserWorkspaces } from '@/lib/api/user'; import { DASH_DOMAIN } from '@/lib/constants'; -import InboxPopover from '@/components/layout/inbox-popover'; +import { SidebarTabsProps } from '@/lib/types'; +import DashboardHeader from '@/components/layout/header'; import NavbarMobile from '@/components/layout/nav-bar-mobile'; import Sidebar from '@/components/layout/sidebar'; -import TitleProvider from '@/components/layout/title-provider'; import { AnalyticsIcon, CalendarIcon, @@ -13,41 +13,45 @@ import { SettingsIcon, TagLabelIcon, } from '@/components/shared/icons/icons-animated'; -import { Icons } from '@/components/shared/icons/icons-static'; -import UserDropdown from '@/components/shared/user-dropdown'; -const tabs = [ - { - name: 'Changelog', - icon: TagLabelIcon, - slug: 'changelog', - }, - { - name: 'Feedback', - icon: FeedbackIcon, - slug: 'feedback', - }, - { - name: 'Roadmap (Soon)', - icon: CalendarIcon, - slug: 'roadmap', - }, - { - name: 'Analytics', - icon: AnalyticsIcon, - slug: 'analytics', - }, - { - name: 'Settings', - icon: SettingsIcon, - slug: 'settings', - }, -]; +const tabs: SidebarTabsProps = { + Modules: [ + { + name: 'Changelog', + icon: TagLabelIcon, + slug: 'changelog', + }, + { + name: 'Feedback', + icon: FeedbackIcon, + slug: 'feedback', + }, + { + name: 'Roadmap', + icon: CalendarIcon, + slug: 'roadmap', + }, + ], + Insights: [ + { + name: 'Analytics', + icon: AnalyticsIcon, + slug: 'analytics', + }, + ], + Workspace: [ + { + name: 'Settings', + icon: SettingsIcon, + slug: 'settings/general', + }, + ], +}; export default async function DashboardLayout({ children }: { children: React.ReactNode }) { // Headers const headerList = headers(); - const projectSlug = headerList.get('x-project'); + const workspaceSlug = headerList.get('x-workspace'); const pathname = headerList.get('x-pathname'); // Fetch user @@ -57,67 +61,48 @@ export default async function DashboardLayout({ children }: { children: React.Re return redirect(`${DASH_DOMAIN}/login`); } - // Fetch the user's projects - const { data: projects } = await getUserProjects('server'); + // Fetch the user's workspaces + const { data: workspaces } = await getUserWorkspaces('server'); - // Check if the user has any projects - if (!projects || projects.length === 0) { + // Check if the user has any workspaces + if (!workspaces || workspaces.length === 0) { return redirect(`${DASH_DOMAIN}`); } - // Get the project with the current slug - const currentProject = projects.find((project) => project.slug === projectSlug); + // Get the workspace with the current slug + const currentWorkspace = workspaces.find((workspace) => workspace.slug === workspaceSlug); - // If currentProject is undefined, redirect to the first project - if (!currentProject) { - return redirect(`/${projects[0].slug}`); + // If currentWorkspace is undefined, redirect to the first workspace + if (!currentWorkspace) { + return redirect(`/${workspaces[0].slug}`); } // Retrieve the currently active tab - const activeTabIndex = tabs.findIndex((tab) => pathname?.includes(tab.slug)); + const activeTab = Object.values(tabs) + .flatMap((tabArray) => tabArray) + .find((tab) => pathname?.includes(tab.slug)); return ( -
-
- {/* Header with logo and hub button */} - {/* BUG: Find a way to solve issue of scroll bar getting removed on avatar dialog open */} - {/* https://github.com/radix-ui/primitives/discussions/1100 */} -
- {/* Logo */} - - -
- - -
-
-
- {/* Sidebar */} - +
+ {/* Header with logo and hub button */} + {/* BUG: Find a way to solve issue of scroll bar getting removed on avatar dialog open */} + {/* https://github.com/radix-ui/primitives/discussions/1100 */} + - {/* Main content */} -
- - {children} -
-
+ {/* Sidebar */} + - {/* Navbar (mobile) */} - + {/* Main content */} +
+ {children}
+ + {/* Navbar (mobile) */} +
); } diff --git a/apps/web/app/dash/[slug]/page.tsx b/apps/web/app/dash/[slug]/page.tsx index 1e344c3..4e3668f 100644 --- a/apps/web/app/dash/[slug]/page.tsx +++ b/apps/web/app/dash/[slug]/page.tsx @@ -1,6 +1,6 @@ import { redirect } from 'next/navigation'; -export default async function ProjectPage({ params }: { params: { slug: string } }) { +export default async function WorkspacePage({ params }: { params: { slug: string } }) { // Redirect to changelog redirect(`/${params.slug}/changelog`); } diff --git a/apps/web/app/dash/[slug]/roadmap/page.tsx b/apps/web/app/dash/[slug]/roadmap/page.tsx new file mode 100644 index 0000000..dc1a177 --- /dev/null +++ b/apps/web/app/dash/[slug]/roadmap/page.tsx @@ -0,0 +1,14 @@ +import RoadmapBoard from '@/components/roadmap/dashboard/roadmap-board'; +import RoadmapHeader from '@/components/roadmap/dashboard/roadmap-header'; + +export default async function Roadmap() { + return ( +
+ {/* Header */} + + + {/* Board */} + +
+ ); +} diff --git a/apps/web/app/dash/[slug]/settings/api/page.tsx b/apps/web/app/dash/[slug]/settings/api/page.tsx new file mode 100644 index 0000000..a5c81c1 --- /dev/null +++ b/apps/web/app/dash/[slug]/settings/api/page.tsx @@ -0,0 +1,9 @@ +import SettingsCard from '@/components/settings/settings-card'; + +export default function ApiSettings() { + return ( + +
s
+
+ ); +} diff --git a/apps/web/app/dash/[slug]/settings/billing/page.tsx b/apps/web/app/dash/[slug]/settings/billing/page.tsx new file mode 100644 index 0000000..c798162 --- /dev/null +++ b/apps/web/app/dash/[slug]/settings/billing/page.tsx @@ -0,0 +1,29 @@ +import Link from 'next/link'; +import { Button } from '@feedbase/ui/components/button'; +import { Label } from '@feedbase/ui/components/label'; +import { LucideExternalLink } from 'lucide-react'; +import SettingsCard from '@/components/settings/settings-card'; + +export default function BillingSettings() { + return ( + +
+
+ + + View plans + + +
+
+ + +
+
+
+ ); +} diff --git a/apps/web/app/dash/[slug]/settings/changelog/page.tsx b/apps/web/app/dash/[slug]/settings/changelog/page.tsx new file mode 100644 index 0000000..5b3429f --- /dev/null +++ b/apps/web/app/dash/[slug]/settings/changelog/page.tsx @@ -0,0 +1,15 @@ +import { Input } from '@feedbase/ui/components/input'; +import { Label } from '@feedbase/ui/components/label'; +import SettingsCard from '@/components/settings/settings-card'; + +export default function FeedbackSettings() { + return ( + +
+ + + +
+
+ ); +} diff --git a/apps/web/app/dash/[slug]/settings/domain/page.tsx b/apps/web/app/dash/[slug]/settings/domain/page.tsx new file mode 100644 index 0000000..3ea7363 --- /dev/null +++ b/apps/web/app/dash/[slug]/settings/domain/page.tsx @@ -0,0 +1,599 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import Link from 'next/link'; +import { Button } from '@feedbase/ui/components/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@feedbase/ui/components/dropdown-menu'; +import { Label } from '@feedbase/ui/components/label'; +import { Skeleton } from '@feedbase/ui/components/skeleton'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@feedbase/ui/components/tabs'; +import { cn } from '@feedbase/ui/lib/utils'; +import { fontMono } from '@feedbase/ui/styles/fonts'; +import { ExclamationCircleIcon } from '@heroicons/react/24/outline'; +import { ChevronUpDownIcon } from '@heroicons/react/24/solid'; +import { Check, CheckIcon, ClipboardList, RefreshCcw, Trash2Icon } from 'lucide-react'; +import { toast } from 'sonner'; +import useSWR from 'swr'; +import useSWRMutation from 'swr/mutation'; +import useWorkspace from '@/lib/swr/use-workspace'; +import { actionFetcher, fetcher, formatRootUrl } from '@/lib/utils'; +import SettingsCard from '@/components/settings/settings-card'; +import FetchError from '@/components/shared/fetch-error'; +import { Icons } from '@/components/shared/icons/icons-static'; +import InputGroup from '@/components/shared/input-group'; + +interface domainData { + name: string; + apexName: string; + verified: boolean; + verification: { + type: string; + domain: string; + value: string; + reason: string; + }[]; +} + +export default function DomainSettings({ params }: { params: { slug: string } }) { + const [domain, setDomain] = useState(''); + const { workspace, loading, isValidating, mutate, error } = useWorkspace(); + const [domainStatus, setDomainStatus] = useState<'unset' | 'verifying' | 'verified'>('unset'); + const [hasCopied, setHasCopied] = useState([]); + const [redirectRule, setRedirectRule] = useState<'direct_redirect' | 'root_redirect' | 'no_redirect'>(); + + const { + data: domainData, + isValidating: domainIsValidating, + mutate: domainMutate, + } = useSWR(`/api/v1/workspaces/${params.slug}/domain`, fetcher, { + errorRetryInterval: 30000, + refreshInterval: 5000, + }); + + const { trigger: submitDomainVerification, isMutating: isSubmittingDomain } = useSWRMutation( + `/api/v1/workspaces/${params.slug}/domain`, + actionFetcher, + { + onSuccess: () => { + setDomainStatus('verifying'); + }, + onError: (error) => { + toast.error(error.message); + }, + } + ); + + const { trigger: removeDomain, isMutating: isRemovingDomain } = useSWRMutation( + `/api/v1/workspaces/${params.slug}/domain`, + actionFetcher, + { + onSuccess: () => { + setDomainStatus('unset'); + setDomain(''); + }, + onError: (error) => { + toast.error(error.message); + }, + } + ); + + const { trigger: updateWorkspace } = useSWRMutation(`/api/v1/workspaces/${params.slug}`, actionFetcher, { + onSuccess: () => { + toast.success('Redirect rule updated.'); + }, + onError: (error) => { + toast.error(error.message); + }, + }); + + useEffect(() => { + if (workspace?.custom_domain) { + // Set domain if it is not set + if (!domain) { + setDomain(workspace.custom_domain); + } + + // Set domain status + if (workspace.custom_domain_verified || domainData?.verified) { + setDomainStatus('verified'); + } else { + setDomainStatus('verifying'); + } + } else { + setDomainStatus('unset'); + } + + setRedirectRule(workspace?.custom_domain_redirect); + }, [workspace, domainData]); + + function handleCopyToClipboard(value: string) { + navigator.clipboard.writeText(value); + setHasCopied((prevHasCopied) => [...prevHasCopied, value]); + + setTimeout(() => { + setHasCopied((prevHasCopied) => prevHasCopied.filter((item) => item !== value)); + }, 3000); + } + + // Loading State + if (loading) { + return ( + + + + ); + } + + // Error State + if (error) { + return ( + +
+ +
+
+ ); + } + + return ( + +
+ +
+ { + setDomain(event.target.value); + }} + /> + {domainStatus === 'unset' ? ( + + ) : ( + + )} + + +
+ +
+ + + + + + + { + updateWorkspace({ method: 'PATCH', custom_domain_redirect: 'direct_redirect' }); + setRedirectRule('direct_redirect'); + }}> +
+ Redirect direct path + + Redirects to equivalent path on your custom domain. + +
+ + +
+ { + updateWorkspace({ method: 'PATCH', custom_domain_redirect: 'root_redirect' }); + setRedirectRule('root_redirect'); + }}> +
+ Redirect to root + + Redirects to the root of your custom domain. + +
+ + +
+ { + updateWorkspace({ method: 'PATCH', custom_domain_redirect: 'no_redirect' }); + setRedirectRule('no_redirect'); + }}> +
+ Don't Redirect + + Does not redirect any requests to your custom domain. + +
+ + +
+
+
+ + +
+ +
+ {domainStatus === 'verifying' ? ( + <> +
+ + Verification Required +
+ + {/* Initial Loading state */} + {!domainData && ( +
+ + +
+ )} + + {domainData ? ( +
+ {/* Tabs, if domain is not assigned to a vercel workspace yet */} + {domainData.verification === undefined ? ( + +
+ + + A Record (Recommended) + + + CNAME Record + + + + {/* Refresh Status */} + +
+ + + + + {/* Info Table */} +
+ {/* Type */} +
+
+ + + A + +
+
+ + +
+
+ + + +
+
+ + +
+
+
+ + {/* Note */} + +
+ + + + {/* Info Table */} +
+ {/* Type */} +
+
+ + + CNAME + +
+
+ + +
+
+ + + +
+
+ + +
+
+
+ + {/* Note */} + +
+
+ ) : null} + + {/* Verification Instructions for already vercel assigned domains */} + {domainData.verification !== undefined ? ( +
+ + + {/* Info Table */} +
+ {/* Type */} +
+
+ + + TXT + +
+
+ + +
+
+ + + +
+
+
+ + {/* Note */} + +
+ ) : null} +
+ ) : null} + + ) : null} +
+
+ ); +} diff --git a/apps/web/app/dash/[slug]/settings/feedback/page.tsx b/apps/web/app/dash/[slug]/settings/feedback/page.tsx new file mode 100644 index 0000000..5b3429f --- /dev/null +++ b/apps/web/app/dash/[slug]/settings/feedback/page.tsx @@ -0,0 +1,15 @@ +import { Input } from '@feedbase/ui/components/input'; +import { Label } from '@feedbase/ui/components/label'; +import SettingsCard from '@/components/settings/settings-card'; + +export default function FeedbackSettings() { + return ( + +
+ + + +
+
+ ); +} diff --git a/apps/web/app/dash/[slug]/settings/general/loading.tsx b/apps/web/app/dash/[slug]/settings/general/loading.tsx deleted file mode 100644 index 5a31508..0000000 --- a/apps/web/app/dash/[slug]/settings/general/loading.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Skeleton } from 'ui/components/ui/skeleton'; - -export default function GeneralLoading() { - return ( - // 3 cards with loading state -
- - - -
- ); -} diff --git a/apps/web/app/dash/[slug]/settings/general/page.tsx b/apps/web/app/dash/[slug]/settings/general/page.tsx index 32c6754..cfbee47 100644 --- a/apps/web/app/dash/[slug]/settings/general/page.tsx +++ b/apps/web/app/dash/[slug]/settings/general/page.tsx @@ -1,28 +1,459 @@ -import { getProjectBySlug, getProjectConfigBySlug } from '@/lib/api/projects'; -import GeneralConfigCards from '@/components/dashboard/settings/general-cards'; +'use client'; -export default async function GeneralSettings({ params }: { params: { slug: string } }) { - // Fetch project data - const { data: project, error } = await getProjectBySlug(params.slug, 'server'); +import { useEffect, useState } from 'react'; +import { Avatar, AvatarFallback, AvatarImage } from '@feedbase/ui/components/avatar'; +import { Button } from '@feedbase/ui/components/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@feedbase/ui/components/dropdown-menu'; +import { Input } from '@feedbase/ui/components/input'; +import { Label } from '@feedbase/ui/components/label'; +import { Skeleton } from '@feedbase/ui/components/skeleton'; +import { cn } from '@feedbase/ui/lib/utils'; +import { Check, ChevronsUpDownIcon } from 'lucide-react'; +import { toast } from 'sonner'; +import useSWRMutation from 'swr/mutation'; +import useWorkspace from '@/lib/swr/use-workspace'; +import useWorkspaceTheme from '@/lib/swr/use-workspace-theme'; +import { WorkspaceProps, WorkspaceThemeProps } from '@/lib/types'; +import { actionFetcher, areObjectsEqual } from '@/lib/utils'; +import SettingsCard from '@/components/settings/settings-card'; +import FetchError from '@/components/shared/fetch-error'; +import FileDrop from '@/components/shared/file-drop'; +import InputGroup from '@/components/shared/input-group'; - if (error) { - return
{error.message}
; - } +export default function GeneralSettings({ params }: { params: { slug: string } }) { + const { + workspace: workspaceData, + loading: workspaceLoading, + error: workspaceError, + mutate: workspaceMutate, + } = useWorkspace(); + const { + workspaceTheme: workspaceThemeData, + loading: workspaceThemeLoading, + error: workspaceThemeError, + mutate: workspaceThemeMutate, + } = useWorkspaceTheme(); + + const [workspace, setWorkspace] = useState(); + const [workspaceTheme, setWorkspaceTheme] = useState(); + + const { trigger: updateWorkspace } = useSWRMutation(`/api/v1/workspaces/${params.slug}`, actionFetcher, { + onSuccess: () => { + // Check if slug has changed + if (workspace?.slug !== workspaceData?.slug) { + // Redirect to new slug + window.location.href = `/${workspace?.slug}/settings/general`; + } + + workspaceMutate(); + }, + onError: (error) => { + toast.error(error.message); + }, + }); - // Fetch project config - const { data: projectConfig, error: projectConfigError } = await getProjectConfigBySlug( - params.slug, - 'server' + const { trigger: updateWorkspaceTheme } = useSWRMutation( + `/api/v1/workspaces/${params.slug}/theme`, + actionFetcher, + { + onError: (error) => { + toast.error(error.message); + }, + } ); - if (projectConfigError) { - return
{projectConfigError.message}
; + useEffect(() => { + setWorkspace(workspaceData); + setWorkspaceTheme(workspaceThemeData); + }, [workspaceData, workspaceThemeData]); + + if (workspaceError || workspaceThemeError) { + return ( + + ); } - return ( -
- {/* General Card */} - -
- ); + if (workspaceLoading || workspaceThemeLoading) { + return ( + <> + + + + + + + + + + + + + ); + } + + if (workspace && workspaceTheme) { + return ( + <> + { + await updateWorkspace({ method: 'PATCH', ...workspace }); + }} + onCancel={() => { + setWorkspace(workspaceData); + }}> +
+ + { + setWorkspace({ + ...workspace, + name: e.target.value, + }); + }} + /> + +
+ +
+ + + { + setWorkspace({ + ...workspace, + slug: e.target.value, + }); + }} + suffix={`.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`} + placeholder='feedbase' + /> + + +
+ + {/* Workspace Logo */} +
+ + + {/* File Upload */} +
+ + + + {workspace.name[0]} + + +
+
+ + { + // Set the icon to the uploaded file + if (event.target.files?.[0]) { + // Check if the file is an image, image/png, image/jpeg, etc. + if (!['image/png', 'image/jpeg', 'image/jpg'].includes(event.target.files[0].type)) { + toast.error('Please upload a valid image.'); + return; + } + + // Read the file as a data URL + const reader = new FileReader(); + reader.onload = (e) => { + setWorkspace({ + ...workspace, + icon: e.target?.result as string, + }); + }; + reader.readAsDataURL(event.target.files[0]); + } + }} + /> + {workspace.icon ? ( + + ) : null} +
+ +
+
+
+ +
+ + + + + + + { + setWorkspace({ + ...workspace, + icon_radius: 'rounded-md', + }); + }}> + Rounded + + { + setWorkspace({ + ...workspace, + icon_radius: 'rounded-full', + }); + }}> + Circle + + { + setWorkspace({ + ...workspace, + icon_radius: 'rounded-none', + }); + }}> + Square + + + + + +
+ +
+ + { + setWorkspace({ + ...workspace, + icon_redirect_url: e.target.value, + }); + }} + placeholder='Leave blank to use default' + /> + +
+ + {/* OG Image */} +
+ OG Image} + image={workspace.opengraph_image} + setImage={(e: string | null) => { + setWorkspace({ + ...workspace, + opengraph_image: e, + }); + }} + className='h-40 w-80' + /> + + +
+
+ + + {/* Thanks to https://github.com/openstatushq for the inspiration of the theme previews */} + + + + + + + + +
+ +
+
+ + ); + } } diff --git a/apps/web/app/dash/[slug]/settings/hub/loading.tsx b/apps/web/app/dash/[slug]/settings/hub/loading.tsx index 8053295..f8a838e 100644 --- a/apps/web/app/dash/[slug]/settings/hub/loading.tsx +++ b/apps/web/app/dash/[slug]/settings/hub/loading.tsx @@ -1,4 +1,4 @@ -import { Skeleton } from 'ui/components/ui/skeleton'; +import { Skeleton } from '@feedbase/ui/components/skeleton'; export default function HubLoading() { return ( diff --git a/apps/web/app/dash/[slug]/settings/hub/page.tsx b/apps/web/app/dash/[slug]/settings/hub/page.tsx index 09506ef..0109277 100644 --- a/apps/web/app/dash/[slug]/settings/hub/page.tsx +++ b/apps/web/app/dash/[slug]/settings/hub/page.tsx @@ -1,16 +1,18 @@ -import { getProjectBySlug, getProjectConfigBySlug } from '@/lib/api/projects'; -import HubConfigCards from '@/components/dashboard/settings/hub-cards'; +import { getWorkspaceModuleConfig } from '@/lib/api/module'; +import { getWorkspaceBySlug } from '@/lib/api/workspace'; + +// import HubConfigCards from '@/components/dashboard/settings/hub-cards'; export default async function HubSettings({ params }: { params: { slug: string } }) { - // Fetch project data - const { data: project, error } = await getProjectBySlug(params.slug, 'server'); + // Fetch workspace data + const { data: workspace, error } = await getWorkspaceBySlug(params.slug, 'server'); if (error) { return
{error.message}
; } - // Fetch project config - const { data: projectConfig, error: configError } = await getProjectConfigBySlug(params.slug, 'server'); + // Fetch workspace config + const { data: workspaceConfig, error: configError } = await getWorkspaceModuleConfig(params.slug, 'server'); if (configError) { return
{configError.message}
; @@ -19,7 +21,7 @@ export default async function HubSettings({ params }: { params: { slug: string } return (
{/* Hub Card */} - + {/* */}
); } diff --git a/apps/web/app/dash/[slug]/settings/integrations/loading.tsx b/apps/web/app/dash/[slug]/settings/integrations/loading.tsx index f1cbc8e..b50f052 100644 --- a/apps/web/app/dash/[slug]/settings/integrations/loading.tsx +++ b/apps/web/app/dash/[slug]/settings/integrations/loading.tsx @@ -1,4 +1,4 @@ -import { Skeleton } from 'ui/components/ui/skeleton'; +import { Skeleton } from '@feedbase/ui/components/skeleton'; export default function IntegrationsLoading() { return ( diff --git a/apps/web/app/dash/[slug]/settings/integrations/page.tsx b/apps/web/app/dash/[slug]/settings/integrations/page.tsx index 1bd1711..2b94c5f 100644 --- a/apps/web/app/dash/[slug]/settings/integrations/page.tsx +++ b/apps/web/app/dash/[slug]/settings/integrations/page.tsx @@ -1,12 +1,81 @@ -import { getProjectConfigBySlug } from '@/lib/api/projects'; -import IntegrationCards from '@/components/dashboard/settings/integration-cards'; +import { Avatar, AvatarFallback, AvatarImage } from '@feedbase/ui/components/avatar'; +import FeedbackModal from '@/components/modals/send-feedback-modal'; +import SettingsCard from '@/components/settings/settings-card'; -export default async function IntegrationsSettings({ params }: { params: { slug: string } }) { - const { data, error } = await getProjectConfigBySlug(params.slug, 'server'); +function IntegrationCard({ + title, + description, + image, + alt, + onClick, +}: { + title: string; + description: string; + image: string; + alt: string; + onClick?: () => void; +}) { + return ( + + ); +} + +export default function IntegrationSettings({ params }: { params: { slug: string } }) { + return ( + + + + + + + + + + + More integrations coming soon! If you have a specific integration you would like to see,{' '} + + + + ! + + + ); } diff --git a/apps/web/app/dash/[slug]/settings/layout.tsx b/apps/web/app/dash/[slug]/settings/layout.tsx index 85b549f..7b7f8f9 100644 --- a/apps/web/app/dash/[slug]/settings/layout.tsx +++ b/apps/web/app/dash/[slug]/settings/layout.tsx @@ -1,24 +1,84 @@ -import { headers } from 'next/headers'; -import CategoryTabs from '@/components/dashboard/settings/category-tabs'; +import { + Blocks, + Earth, + GalleryVerticalEnd, + KanbanSquare, + RefreshCcw, + SquareAsterisk, + SwatchBook, + Terminal, + UsersRoundIcon, + Wallet2, + Webhook, +} from 'lucide-react'; +import { SidebarTabsProps } from '@/lib/types'; +import Sidebar from '@/components/layout/sidebar'; -const tabs = [ - { - name: 'General', - slug: 'general', - }, - { - name: 'Hub', - slug: 'hub', - }, - { - name: 'Team', - slug: 'team', - }, - { - name: 'Integrations', - slug: 'integrations', - }, -]; +const tabs: SidebarTabsProps = { + Workspace: [ + { + name: 'Branding', + customIcon: , + slug: 'settings/general', + }, + { + name: 'Team Members', + customIcon: , + slug: 'settings/team', + }, + { + name: 'Billing', + customIcon: , + slug: 'settings/billing', + }, + ], + Public: [ + { + name: 'Domain', + customIcon: , + slug: 'settings/domain', + }, + { + name: 'SSO', + customIcon: , + slug: 'settings/sso', + }, + ], + Modules: [ + { + name: 'Feedback', + customIcon: , + slug: 'settings/feedback', + }, + { + name: 'Roadmap', + customIcon: , + slug: 'settings/roadmap', + }, + { + name: 'Changelog', + customIcon: , + slug: 'settings/changelog', + }, + ], + Additional: [ + { + name: 'API', + customIcon: , + slug: 'settings/api', + }, + { + name: 'Webhooks', + customIcon: , + slug: 'settings/webhooks', + }, + { + name: 'Integrations', + customIcon: , + slug: 'settings/integrations', + }, + ], +}; export default function SettingsLayout({ children, @@ -27,20 +87,21 @@ export default function SettingsLayout({ children: React.ReactNode; params: { slug: string }; }) { - // Headers - const headerList = headers(); - const pathname = headerList.get('x-pathname'); - - // Retrieve the currently active tab - const activeTabIndex = tabs.findIndex((tab) => pathname?.split('/')[3] === tab.slug); - return (
- {/* Navigation tabs */} - + {/* Sidebar */} + {/* Content */} -
{children}
+
+
+ {children} +
+
); } diff --git a/apps/web/app/dash/[slug]/settings/roadmap/page.tsx b/apps/web/app/dash/[slug]/settings/roadmap/page.tsx new file mode 100644 index 0000000..a37a7c4 --- /dev/null +++ b/apps/web/app/dash/[slug]/settings/roadmap/page.tsx @@ -0,0 +1,17 @@ +import { Label } from '@feedbase/ui/components/label'; +import { Switch } from '@feedbase/ui/components/switch'; +import SettingsCard from '@/components/settings/settings-card'; + +export default function FeedbackSettings() { + return ( + +
+ + + +
+
+ ); +} diff --git a/apps/web/app/dash/[slug]/settings/sso/page.tsx b/apps/web/app/dash/[slug]/settings/sso/page.tsx new file mode 100644 index 0000000..ef4b2c9 --- /dev/null +++ b/apps/web/app/dash/[slug]/settings/sso/page.tsx @@ -0,0 +1,287 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import Link from 'next/link'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@feedbase/ui/components/alert-dialog'; +import { Button } from '@feedbase/ui/components/button'; +import { Checkbox } from '@feedbase/ui/components/checkbox'; +import { Input } from '@feedbase/ui/components/input'; +import { Label } from '@feedbase/ui/components/label'; +import { + ResponsiveDialog, + ResponsiveDialogClose, + ResponsiveDialogContent, + ResponsiveDialogDescription, + ResponsiveDialogFooter, + ResponsiveDialogHeader, + ResponsiveDialogTitle, + ResponsiveDialogTrigger, +} from '@feedbase/ui/components/responsive-dialog'; +import { Skeleton } from '@feedbase/ui/components/skeleton'; +import { toast } from 'sonner'; +import useSWRMutation from 'swr/mutation'; +import useWorkspace from '@/lib/swr/use-workspace'; +import { actionFetcher, formatRootUrl } from '@/lib/utils'; +import SettingsCard from '@/components/settings/settings-card'; +import CopyCheckIcon from '@/components/shared/copy-check-icon'; +import FetchError from '@/components/shared/fetch-error'; +import { Icons } from '@/components/shared/icons/icons-static'; +import InputGroup from '@/components/shared/input-group'; + +export default function SSOSettings({ params }: { params: { slug: string } }) { + const [ssoConfig, setSsoConfig] = useState<{ + enabled: boolean; + url: string | null | undefined; + secret: string | null | undefined; + }>({ + enabled: false, + url: null, + secret: '', + }); + const [hasCopied, setHasCopied] = useState(); + const [open, setOpen] = useState(false); + const { workspace, loading, error, mutate } = useWorkspace(); + + const { trigger: updateWorkspaceSSO, isMutating: isUpdatingWorkspaceSSO } = useSWRMutation( + `/api/v1/workspaces/${params.slug}/sso`, + actionFetcher, + { + onSuccess: () => { + mutate(); + }, + onError: (error) => { + toast.error(error.message); + }, + } + ); + + const { trigger: generateJwtSecret, isMutating: isGenerating } = useSWRMutation( + `/api/v1/workspaces/${params.slug}/sso/secret`, + actionFetcher, + { + onSuccess: (data: { secret: string }) => { + setSsoConfig({ ...ssoConfig, secret: data.secret }); + setOpen(true); + }, + onError: (error) => { + toast.error(error.message); + }, + } + ); + + useEffect(() => { + if (workspace) { + setSsoConfig({ + enabled: workspace.sso_auth_enabled, + url: workspace.sso_auth_url, + secret: workspace.sso_auth_secret_id ? 'dummy-secret' : undefined, + }); + } + }, [workspace]); + + // Loading state + if (loading && !error) { + return ( + + Configure single sign-on for your workspace.{' '} + + Learn more + + . + + }> + + + ); + } + + // Error state + if (error) { + return ( + + Configure single sign-on for your workspace.{' '} + + Learn more + + . + + }> +
+ +
+
+ ); + } + + return ( + { + setSsoConfig({ ...ssoConfig, enabled: checked }); + toast.promise(updateWorkspaceSSO({ method: 'PATCH', enabled: checked }), { + loading: ssoConfig.enabled ? `Disabling Single Sign-On` : `Enabling Single Sign-On`, + success: ssoConfig.enabled ? `Single Sign-On disabled` : `Single Sign-On enabled`, + error: () => { + setSsoConfig({ ...ssoConfig, enabled: !checked }); + return `Failed to ${ssoConfig.enabled ? 'disable' : 'enable'} Single Sign-On`; + }, + }); + }} + description={ + <> + Configure single sign-on for your workspace.{' '} + + Learn more + + . + + } + showSave={workspace?.sso_auth_url !== ssoConfig.url} + onCancel={() => { + setSsoConfig({ ...ssoConfig, url: workspace?.sso_auth_url }); + }} + onSave={async () => { + await updateWorkspaceSSO({ + method: 'PATCH', + enabled: ssoConfig.enabled, + url: ssoConfig.url, + secret: ssoConfig.secret, + }); + }}> +
+ + { + setSsoConfig({ ...ssoConfig, url: e.target.value }); + }} + /> + +
+ +
+ + + + + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will generate a new secret for your Single Sign-On + configuration and invalidate the previous one. + + + + Cancel + { + await generateJwtSecret({}); + }}> + Continue + + + + + + + + Token Created + + This token can not be shown again. Please copy it and store it in a safe place. + + + +
+ + { + setHasCopied(true); + }} + /> + } + /> +
+ + +
+ { + setHasCopied(!hasCopied); + }} + id='copied' + /> + +
+ + + +
+
+
+ + +
+
+ ); +} diff --git a/apps/web/app/dash/[slug]/settings/team/loading.tsx b/apps/web/app/dash/[slug]/settings/team/loading.tsx deleted file mode 100644 index 1753c58..0000000 --- a/apps/web/app/dash/[slug]/settings/team/loading.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Skeleton } from 'ui/components/ui/skeleton'; - -export default function TeamLoading() { - return ( -
- -
- ); -} diff --git a/apps/web/app/dash/[slug]/settings/team/page.tsx b/apps/web/app/dash/[slug]/settings/team/page.tsx index 1343f64..eb627f2 100644 --- a/apps/web/app/dash/[slug]/settings/team/page.tsx +++ b/apps/web/app/dash/[slug]/settings/team/page.tsx @@ -1,32 +1,396 @@ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 'ui/components/ui/card'; -import { getProjectInvites } from '@/lib/api/invites'; -import { getProjectMembers } from '@/lib/api/projects'; -import { TeamTable } from '@/components/dashboard/settings/team-table'; +'use client'; -export default async function TeamSettings({ params }: { params: { slug: string } }) { - const { data: members, error } = await getProjectMembers(params.slug, 'server'); +import { useState } from 'react'; +import { Avatar, AvatarFallback, AvatarImage } from '@feedbase/ui/components/avatar'; +import { Button } from '@feedbase/ui/components/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuDestructiveItem, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@feedbase/ui/components/dropdown-menu'; +import { Input } from '@feedbase/ui/components/input'; +import { Label } from '@feedbase/ui/components/label'; +import { Skeleton } from '@feedbase/ui/components/skeleton'; +import { ChevronsUpDown, MoreHorizontal } from 'lucide-react'; +import { toast } from 'sonner'; +import useSWRMutation from 'swr/mutation'; +import useTeamInvites from '@/lib/swr/use-team-invites'; +import useTeamMembers from '@/lib/swr/use-team-members'; +import { ExtendedInviteProps, TeamMemberProps } from '@/lib/types'; +import { actionFetcher, formatTimeAgo } from '@/lib/utils'; +import SettingsCard from '@/components/settings/settings-card'; +import FetchError from '@/components/shared/fetch-error'; +import { Icons } from '@/components/shared/icons/icons-static'; - if (error) { - return
{error.message}
; +export default function TeamSettings({ params }: { params: { slug: string } }) { + const { + teamMembers, + error: memberError, + mutate: mutateMember, + isValidating: isValidatingMember, + } = useTeamMembers(); + const { + teamInvites, + error: inviteError, + mutate: mutateInvite, + isValidating: inviteIsValidating, + } = useTeamInvites(); + const [search, setSearch] = useState(''); + const [filter, setFilter] = useState<'all' | 'admins' | 'members' | 'invites'>('all'); + const [inviteEmail, setInviteEmail] = useState(''); + + // Filtered + const filteredMembers = teamMembers?.filter( + (member) => + member.full_name.toLowerCase().includes(search.toLowerCase()) || + member.email.toLowerCase().includes(search.toLowerCase()) + ); + const filteredInvites = teamInvites?.filter((invite) => + invite.email.toLowerCase().includes(search.toLowerCase()) + ); + + // Send invite + const { trigger: sendInvite, isMutating: isSendingInvite } = useSWRMutation( + `/api/v1/workspaces/${params.slug}/invites`, + actionFetcher, + { + onSuccess: () => { + toast.success('Invite sent successfully.'); + // mutateMember(); + }, + onError: (error) => { + toast.error(error.message); + }, + } + ); + + // Revoking invite + function revokeInvite(inviteId: string) { + const req = fetch(`/api/v1/workspaces/${params.slug}/invites/${inviteId}`, { + method: 'DELETE', + }); + + req.then(() => { + mutateInvite(); + }); + } + + // Error state + if (memberError || inviteError) { + return memberError ? ( + + ) : ( + + ); } - const { data: invites, error: invitesError } = await getProjectInvites(params.slug, 'server'); + // Loading state + if (!filteredMembers && isValidatingMember && !filteredInvites && inviteIsValidating) { + return ( + <> + +
+ +
+ + +
+ +
+
+ +
+
+ { + setSearch(e.target.value); + }} + value={search} + /> + + + + + + { + setFilter('all'); + }}> + All + + { + setFilter('admins'); + }}> + Admins + + { + setFilter('members'); + }}> + Members + + { + setFilter('invites'); + }}> + Pending Invites + + + +
+ + + {filter === 'all' && 'members'} + {filter === 'admins' && 'admins'} + {filter === 'members' && 'members'} + {filter === 'invites' && 'pending invites'} + +
- if (invitesError) { - return
{invitesError.message}
; +
+ + Loading team members... +
+
+ + ); } return ( -
- - - Team - Add or remove users that have access to this project. - - - - - -
+ <> + +
+ +
{ + e.preventDefault(); + sendInvite({ email: inviteEmail }); + }}> + { + setInviteEmail(e.target.value); + }} + /> + +
+ +
+
+ + +
+ {/* Action Row */} +
+ { + setSearch(e.target.value); + }} + value={search} + /> + + + + + + { + setFilter('all'); + }}> + All + + { + setFilter('admins'); + }}> + Admins + + { + setFilter('members'); + }}> + Members + + { + setFilter('invites'); + }}> + Pending Invites + + + +
+ + {/* Members */} +
+ + {filter === 'all' && (filteredMembers?.length ?? 0) + (filteredInvites?.length ?? 0)} + {/* TODO: Implement different roles, currently all are handled the same. */} + {filter === 'admins' && (filteredMembers?.length ?? 0)} + {filter === 'members' && (filteredMembers?.length ?? 0)} + {filter === 'invites' && (filteredInvites?.length ?? 0)}{' '} + {filter === 'all' && + ((filteredMembers?.length ?? 0) + (filteredInvites?.length ?? 0) > 1 ? 'members' : 'member')} + {filter === 'admins' && ((filteredMembers?.length ?? 0) > 1 ? 'admins' : 'admin')} + {filter === 'members' && ((filteredMembers?.length ?? 0) > 1 ? 'members' : 'member')} + {filter === 'invites' && + ((filteredInvites?.length ?? 0) > 1 ? 'pending invites' : 'pending invite')} + + +
+ {/* Team Members */} + {(filter === 'all' || filter === 'admins' || filter === 'members') && + filteredMembers?.map((member: TeamMemberProps) => ( +
+
+ + + + {member.full_name[0]} + + +
+ {member.full_name} + {member.email} +
+
+ +
+ {/* Joined days ago */} + + Joined {formatTimeAgo(new Date(member.joined_at))} ago + + + + + + + Make Admin + Suspend + + +
+
+ ))} + + {/* Pending Invites */} + {(filter === 'all' || filter === 'invites') && + filteredInvites?.map((invite: ExtendedInviteProps) => ( +
+
+ + + {invite.email[0]} + + +
+ {invite.email} + + Invited by {invite.creator.full_name} + +
+
+ +
+ + Invited {formatTimeAgo(new Date(invite.created_at))} ago + + + + + + + + Resend + { + revokeInvite(invite.id); + }}> + Revoke + + + +
+
+ ))} + + {/* Empty States */} + {((filter === 'all' || filter === 'admins' || filter === 'members') && + filteredMembers?.length === 0 && + filteredInvites?.length === 0) || + (filter === 'invites' && filteredInvites?.length === 0) ? ( +
+ + {filter === 'all' || filter === 'admins' || filter === 'members' + ? 'No members found.' + : 'No pending invites found.'} + +
+ ) : null} +
+
+
+
+ ); } diff --git a/apps/web/app/dash/invite/[inviteId]/page.tsx b/apps/web/app/dash/invite/[inviteId]/page.tsx index 6b817f9..c8fa036 100644 --- a/apps/web/app/dash/invite/[inviteId]/page.tsx +++ b/apps/web/app/dash/invite/[inviteId]/page.tsx @@ -1,10 +1,10 @@ import { notFound } from 'next/navigation'; -import { getProjectInvite } from '@/lib/api/invites'; +import { getWorkspaceInvite } from '@/lib/api/invite'; import { getCurrentUser } from '@/lib/api/user'; -import ProjectInviteForm from '@/components/layout/accept-invite-form'; +import WorkspaceInviteForm from '@/components/workspace/accept-invite-form'; -export default async function ProjectInvite({ params }: { params: { inviteId: string } }) { - const { data: invite, error: inviteError } = await getProjectInvite(params.inviteId, 'server'); +export default async function WorkspaceInvite({ params }: { params: { inviteId: string } }) { + const { data: invite, error: inviteError } = await getWorkspaceInvite(params.inviteId, 'server'); if (inviteError) { return notFound(); @@ -15,7 +15,7 @@ export default async function ProjectInvite({ params }: { params: { inviteId: st return (
- +
); } diff --git a/apps/web/app/dash/page.tsx b/apps/web/app/dash/page.tsx index b9444a0..12760b8 100644 --- a/apps/web/app/dash/page.tsx +++ b/apps/web/app/dash/page.tsx @@ -1,9 +1,10 @@ import { redirect } from 'next/navigation'; -import { getUserProjects } from '@/lib/api/user'; -import Onboarding from '@/components/layout/onboarding'; +import { getUserWorkspaces } from '@/lib/api/user'; +import Onboarding from '@/components/workspace/onboarding'; +import WorkspaceOverview from '@/components/workspace/workspace-overview'; -export default async function Projects() { - const { data: projects, error } = await getUserProjects('server'); +export default async function Workspaces() { + const { data: workspaces, error } = await getUserWorkspaces('server'); if (error) { // Redirect to login if the user is not authenticated @@ -14,15 +15,10 @@ export default async function Projects() { return
{error.message}
; } - // Redirect to the first project - if (projects.length > 0) { - return redirect(`/${projects[0].slug}`); - } - - // TODO: Improve this and make this redirect to an onboarding page if the user has no projects + // TODO: Improve this and make this redirect to an onboarding page if the user has no workspaces return (
- + {workspaces && workspaces.length > 0 ? : }
); } diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index cfce9bc..13871c1 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -1,77 +1,127 @@ @tailwind base; @tailwind components; @tailwind utilities; - + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* + https://github.com/tailwindlabs/tailwindcss/discussions/2394 + https://github.com/tailwindlabs/tailwindcss/pull/5732 +*/ +@layer utilities { + /* Chrome, Safari and Opera */ + .no-scrollbar::-webkit-scrollbar { + display: none; + } + + .no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } + + .custom-scrollbar::-webkit-scrollbar { + width: 14px; + } + + .custom-scrollbar::-webkit-scrollbar-thumb { + border: 4px solid rgba(0, 0, 0, 0); + background-clip: padding-box; + border-radius: 50px; + background-color: hsl(var(--border) / 1); + } + + .custom-scrollbar::-webkit-scrollbar-thumb:hover { + background-color: hsl(var(--foreground) / 0.15); + } +} + @layer base { :root { /* App root background */ - --root-background: 230 35% 3%; /* #000000 */ + --root-background: 0 0% 100%; + --background: 210 20% 98%; + --foreground: 224 71% 4%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 224 71% 4%; + --primary-foreground: 0 0% 98%; + --secondary: 240 5% 95%; + --secondary-foreground: 215 14% 34%; + --muted: 240 5% 96%; + --muted-foreground: 218 11% 65%; + --accent: 240 5% 94%; + --accent-foreground: 224 71% 4%; + --destructive: 0 79% 63%; + --destructive-foreground: 0 0% 100%; + --border: 220 13% 91%; + --input: 220 13% 91%; + --highlight: 231 100% 78%; + --ring: 216 12% 84%; + + /* Charts */ + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + + --radius: 0.55rem; + } + + .dark { + /* App root background */ + --root-background: 235 7% 5%; /* Utility background (Buttons, inputs, etc) */ - --background: 230 11% 12%; /* #0A0C10 | #060606*/ - --foreground: 210 40% 98%; /* #D6D6D6 */ - + --background: 230 7% 16%; + --foreground: 220 9% 94%; + /* Muted background (Skeletons, Avatar, tabs, etc) */ - --muted: 230 11% 8%; /* #20242c */ - --muted-foreground: 215 6% 62%; - - --popover: 230 35% 3%; /* #0A0C10 */ - --popover-foreground: 210 40% 98%; - - --card: 230 35% 3%; /* #0A0C10 */ - --card-foreground: 210 40% 98%; - - --border: 222 9% 22%; /* #20242c */ - --input: 222 9% 22%; /* #20242c */ - - --primary: 210 40% 98%; - --primary-foreground: 230 35% 3%; - + --muted: 219 6% 12%; + --muted-foreground: 219 6% 65%; + + --popover: 235 7% 5%; + --popover-foreground: 220 9% 94%; + + --card: 235 7% 5%; + --card-foreground: 220 9% 94%; + + --border: 223 7% 19%; + --input: 223 6% 22%; + + --primary: 220 9% 94%; + --primary-foreground: 240 4% 10%; + /* Hover & active backgrounds (Nav buttons, etc) */ - --secondary: 227 11% 12%; /* #20242c */ - --secondary-foreground: 210 40% 98%; - + --secondary: 230 7% 16%; + --secondary-foreground: 218 7% 80%; + /* Button background hovers */ - --accent: 227 11% 15%; /* #20242c */ - --accent-foreground: 210 40% 98%; - - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - + --accent: 230 7% 18%; + --accent-foreground: 220 9% 94%; + + --destructive: 0 79% 63%; + --destructive-foreground: 220 9% 94%; + /* Selection & accent color */ --highlight: 231 100% 78%; - - --ring: 230 9% 13%; /* #20242c */ - --radius: 0.5rem; - } - - .light { - /* App root background */ - --root-background: 0 0% 100%; /* #000000 */ - --background: 0 0% 100%; - --foreground: 240 10% 3.9%; - --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; - --primary: 240 5.9% 10%; - --primary-foreground: 0 0% 98%; - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 240 5.9% 90%; - --input: 240 5.9% 90%; - --highlight: 231 100% 78%; - --ring: 240 5.9% 10%; + /* Charts */ + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + + --ring: 230 9% 13%; } } - + @layer base { * { @apply border-border; @@ -79,4 +129,4 @@ body { @apply bg-root text-foreground selection:bg-highlight/20 selection:text-highlight; } -} \ No newline at end of file +} diff --git a/apps/web/app/home/(pages)/deploy/page.tsx b/apps/web/app/home/(pages)/deploy/page.tsx index b5f2ada..c98b5ee 100644 --- a/apps/web/app/home/(pages)/deploy/page.tsx +++ b/apps/web/app/home/(pages)/deploy/page.tsx @@ -1,5 +1,5 @@ import Link from 'next/link'; -import { Button } from '@ui/components/ui/button'; +import { Button } from '@feedbase/ui/components/button'; import HomeFooter from '@/components/home/footer'; export default function Deploy() { @@ -9,7 +9,7 @@ export default function Deploy() {

Deploy Feedbase to Vercel

-

+

You can deploy your own hosted instance of Feedbase to Vercel, for free, in just a few clicks.

@@ -27,7 +27,7 @@ export default function Deploy() { Create a Supabase project -

+

Supabase is an open source Firebase alternative. It provides a database, auth, and storage.

@@ -51,7 +51,7 @@ export default function Deploy() { Fork the Supabase database -

+

Once you have your Supabase project, you can fork the Feedbase database schemas to your own project.

@@ -79,7 +79,7 @@ export default function Deploy() { Prepare your environment variables -

+

After setting up your Supabase project, check the example configuration to see where you can find the necessary environment variables.

@@ -107,7 +107,7 @@ export default function Deploy() { Deploy to Vercel -

+

Once you have your environment variables, you can deploy to Vercel.

diff --git a/apps/web/app/home/(pages)/page.tsx b/apps/web/app/home/(pages)/page.tsx index a58c6ad..15e2c4d 100644 --- a/apps/web/app/home/(pages)/page.tsx +++ b/apps/web/app/home/(pages)/page.tsx @@ -1,7 +1,7 @@ import Link from 'next/link'; -import { Button } from '@ui/components/ui/button'; +import { Background } from '@feedbase/ui/components/background/background'; +import { Button } from '@feedbase/ui/components/button'; import { ArrowRight, ChevronRight } from 'lucide-react'; -import { Background } from 'ui/components/ui/background/background'; import { formatRootUrl } from '@/lib/utils'; import ChangelogSection from '@/components/home/changelog-section'; import DashboardSection from '@/components/home/dashboard-section'; @@ -27,7 +27,7 @@ export default function Landing() { in One Central Place -

+

Feedbase simplifies feedback collection, feature prioritization, and product update sharing, allowing you to focus on building.

@@ -39,7 +39,7 @@ export default function Landing() { @@ -48,7 +48,7 @@ export default function Landing() { + className='text-foreground/60 hover:text-foreground/90 group relative mt-4 flex w-fit flex-row items-center justify-center p-1 text-center text-sm '>

See it in action

@@ -73,7 +73,7 @@ export default function Landing() { Create your feedback
community today. -

+

Capture feedback, post updates, and engage
with your users in one central place.

diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 8932134..274aa79 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,11 +1,12 @@ import './globals.css'; import { Metadata, Viewport } from 'next'; import Script from 'next/script'; -import { cn } from '@ui/lib/utils'; +import { cn } from '@feedbase/ui/lib/utils'; import { SpeedInsights } from '@vercel/speed-insights/next'; -import { GeistSans } from 'geist/font'; +import { GeistSans } from 'geist/font/sans'; import { Toaster } from 'sonner'; import { formatRootUrl } from '@/lib/utils'; +import { ThemeProvider as NextThemeProvider } from '@/components/theme-provider'; export const metadata: Metadata = { title: 'Feedbase', @@ -49,9 +50,11 @@ export default function RootLayout({ children }: { children: React.ReactNode }) )} -
- {children} -
+ +
+ {children} +
+
); diff --git a/apps/web/components/analytics/bar-chart.tsx b/apps/web/components/analytics/bar-chart.tsx new file mode 100644 index 0000000..f21cbb3 --- /dev/null +++ b/apps/web/components/analytics/bar-chart.tsx @@ -0,0 +1,135 @@ +'use client'; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@feedbase/ui/components/card'; +import { ChartConfig, ChartContainer, ChartTooltip } from '@feedbase/ui/components/chart'; +import { cn } from '@feedbase/ui/lib/utils'; +import { Bar, LabelList, BarChart as ReBarChart, XAxis, YAxis } from 'recharts'; + +const chartData = [ + { month: 'January', desktop: 186, mobile: 80 }, + { month: 'February', desktop: 305, mobile: 200 }, + { month: 'March', desktop: 237, mobile: 120 }, + { month: 'April', desktop: 73, mobile: 190 }, + { month: 'May', desktop: 209, mobile: 130 }, + { month: 'June', desktop: 214, mobile: 140 }, +]; + +const chartConfig = { + desktop: { + label: 'Desktop', + color: 'hsl(var(--accent))', + }, + mobile: { + label: 'Mobile', + color: 'hsl(var(--secondary))', + }, + label: { + color: 'hsl(var(--foreground))', + }, +} satisfies ChartConfig; + +export function BarChart() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const CustomCursor = (props: any) => { + const { x, y, width, height, points, className } = props; + + const indicatorWidth = 2; + + return ( + + + + + + + + + + + + + + + + + + + {/* 2px width indicator */} + + + {/* Main path with gradient */} + console.log('clicked')} + pointerEvents='auto' + width={width - indicatorWidth} + height={height} + points={points} + type='linear' + /> + + + ); + }; + + return ( + + + Bar Chart - Custom Label + January - June 2024 + + + + + value.slice(0, 3)} + hide + /> + + } content={
} /> + + + + + + + + + ); +} diff --git a/apps/web/components/dashboard/analytics/chart-cards.tsx b/apps/web/components/analytics/chart-cards.tsx similarity index 84% rename from apps/web/components/dashboard/analytics/chart-cards.tsx rename to apps/web/components/analytics/chart-cards.tsx index 09b08fc..638fc3b 100644 --- a/apps/web/components/dashboard/analytics/chart-cards.tsx +++ b/apps/web/components/analytics/chart-cards.tsx @@ -1,11 +1,11 @@ 'use client'; import { useState } from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from '@ui/components/ui/card'; -import { cn } from '@ui/lib/utils'; +import { Card, CardContent, CardHeader, CardTitle } from '@feedbase/ui/components/card'; +import { cn } from '@feedbase/ui/lib/utils'; import { AnalyticsProps } from '@/lib/types'; -import BarList from '@/components/dashboard/analytics/bar-list'; -import LineChart from '@/components/dashboard/analytics/line-chart'; +import BarList from '@/components/analytics/bar-list'; +import LineChart from '@/components/analytics/line-chart'; export default function AnalyticsCards({ analyticsData, @@ -35,7 +35,7 @@ export default function AnalyticsCards({ Unique Visitors - + {updatedAnalyticsData.reduce((a, b) => a + b.visitors, 0)} @@ -51,7 +51,7 @@ export default function AnalyticsCards({ Page Views - + {updatedAnalyticsData.reduce((a, b) => a + b.clicks, 0)} @@ -71,7 +71,7 @@ export default function AnalyticsCards({ Top Changelogs + ); + })} +
+ + + + + + { + const date = new Date(value); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + }); + }} + /> + { + return new Date(value).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + }} + /> + } + /> + + + + +
+ ); +} diff --git a/apps/web/components/auth-modules.tsx b/apps/web/components/auth-modules.tsx new file mode 100644 index 0000000..39ba145 --- /dev/null +++ b/apps/web/components/auth-modules.tsx @@ -0,0 +1,241 @@ +'use client'; + +import { createContext, useContext, useState } from 'react'; +import { Button } from '@feedbase/ui/components/button'; +import { Input } from '@feedbase/ui/components/input'; +import { Label } from '@feedbase/ui/components/label'; +import { createBrowserClient } from '@supabase/ssr'; +import { SupabaseClient } from '@supabase/supabase-js'; +import { toast } from 'sonner'; +import { Icons } from './shared/icons/icons-static'; + +interface AuthContextValue { + supabase: SupabaseClient; + successRedirect?: string; +} + +// Create a context to share state and dependencies +const AuthContext = createContext(null); + +// Utility component to consume the context +const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; + +const Auth = ({ successRedirect, children }: { successRedirect?: string; children: React.ReactNode }) => { + const [isLoading, setIsLoading] = useState(false); + + // TODO: Figure out issue with cookieOptions setting and set at root level instead of individually like rn + const supabase = createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ); + + const value = { isLoading, setIsLoading, supabase, successRedirect }; + + return {children}; +}; + +const EmailSignIn = ({ label = 'Continue with Email' }: { label?: string }) => { + const [loading, setLoading] = useState(false); + const { supabase, successRedirect } = useAuth(); + const [email, setEmail] = useState(''); + + const handleMailSignIn = async (event: React.FormEvent) => { + event.preventDefault(); + setLoading(true); + + if (!email) { + toast.error('Please enter your email!'); + setLoading(false); + return; + } + + // Check if user exists + const { data: user } = await supabase.from('profiles').select('*').eq('email', email).single(); + + if (!user) { + toast.error('No account found with this email address.'); + setLoading(false); + return; + } + + const { error } = await supabase.auth.signInWithOtp({ + email, + options: { + emailRedirectTo: `${location.origin}/auth/callback?successRedirect=${ + successRedirect || location.origin + }`, + }, + }); + + if (error) { + toast.error(error.message); + } else { + toast.success('Magic link has been sent to your email!'); + } + + setLoading(false); + }; + + return ( +
+
+
+ + { + setEmail(event.currentTarget.value); + }} + /> +
+ +
+
+ ); +}; + +const EmailSignUp = ({ label = 'Continue with Email' }: { label?: string }) => { + const [loading, setLoading] = useState(false); + const { supabase, successRedirect } = useAuth(); + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + + const handleMailSignUp = async (event: React.FormEvent) => { + event.preventDefault(); + setLoading(true); + + if (!name) { + toast.error('Please enter your name!'); + setLoading(false); + return; + } else if (!email) { + toast.error('Please enter your email!'); + setLoading(false); + return; + } + + // Check if user exists + const { data: user } = await supabase.from('profiles').select('*').eq('email', email).single(); + + if (user) { + toast.error('An account with this email address already exists.'); + setLoading(false); + return; + } + + const { error } = await supabase.auth.signInWithOtp({ + email, + options: { + emailRedirectTo: `${location.origin}/auth/callback?successRedirect=${ + successRedirect || location.origin + }`, + data: { + full_name: name, + }, + }, + }); + + if (error) { + toast.error(error.message); + } else { + toast.success('Magic link has been sent to your email!'); + } + + setLoading(false); + }; + + return ( +
+
+
+ + { + setName(event.currentTarget.value); + }} + /> + + { + setEmail(event.currentTarget.value); + }} + /> +
+ +
+
+ ); +}; + +const GitHub = () => { + const [loading, setLoading] = useState(false); + const { supabase, successRedirect } = useAuth(); + + const handleGitHubSignIn = async (event: React.MouseEvent) => { + event.preventDefault(); + setLoading(true); + + const { error } = await supabase.auth.signInWithOAuth({ + provider: 'github', + options: { + redirectTo: `${location.origin}/auth/callback?successRedirect=${successRedirect || location.origin}`, + }, + }); + + if (error) { + toast.error(error.message); + } + }; + + return ( + + ); +}; + +export default Auth; +export { GitHub, EmailSignUp, EmailSignIn }; diff --git a/apps/web/components/changelog/api-sheet.tsx b/apps/web/components/changelog/api-sheet.tsx new file mode 100644 index 0000000..5f2ca3f --- /dev/null +++ b/apps/web/components/changelog/api-sheet.tsx @@ -0,0 +1,201 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { Alert, AlertDescription, AlertTitle } from '@feedbase/ui/components/alert'; +import { Button } from '@feedbase/ui/components/button'; +import { Label } from '@feedbase/ui/components/label'; +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger, +} from '@feedbase/ui/components/sheet'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@feedbase/ui/components/tabs'; +import { Check, Copy, Info } from 'lucide-react'; +import { useTheme } from 'next-themes'; +import { LightAsync as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { docco, nightOwl } from 'react-syntax-highlighter/dist/esm/styles/hljs'; +import { formatRootUrl } from '@/lib/utils'; +import { CodeIcon } from '@/components/shared/icons/icons-animated'; +import LottiePlayer from '@/components/shared/lottie-player'; + +export function ApiSheet({ workspaceSlug }: { workspaceSlug: string }) { + const [isHover, setIsHover] = useState(false); + const currentTheme = useTheme(); + + const tabs = [ + { + id: 'bash', + name: 'cURL', + content: `curl --request GET \\ + --url ${formatRootUrl('', `/api/v1/${workspaceSlug}/changelogs`)} \\ + --header 'Content-Type: application/json'`, + }, + { + id: 'javascript', + name: 'JavaScript', + content: `const options = {method: 'GET'}; + +fetch('${formatRootUrl('', `/api/v1/${workspaceSlug}/changelogs`)}', options) + .then(response => response.json()) + .then(data => console.log(data));`, + }, + { + id: 'python', + name: 'Python', + content: `import requests + +url = '${formatRootUrl('', `/api/v1/${workspaceSlug}/changelogs`)}' +response = requests.get(url) +print(response.json())`, + }, + ]; + + return ( + + + + + + + Changelogs API + + Access your changelogs using our RESTful API. You can use this API to fetch your public changelogs + and display them customly on your website or application. + + +
+ {/* Endpoint */} +
+ +
+ + {formatRootUrl('', `/api/v1/${workspaceSlug}/changelogs`)} + + +
+
+ + {/* Usage */} +
+ + + +
+ {tabs.map((tab) => ( + + + + ))} +
+ +
+ {tabs.map((tab) => ( + + + {tab.content} + + + ))} +
+
+ +
+ + + Admin API + + If you want to also manage your changelogs or view private changelogs, you can use the Admin + API. + + + + + + +
+
+ +
+
+ ); +} diff --git a/apps/web/components/changelog/changelog-list.tsx b/apps/web/components/changelog/changelog-list.tsx new file mode 100644 index 0000000..8e37f2f --- /dev/null +++ b/apps/web/components/changelog/changelog-list.tsx @@ -0,0 +1,355 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import Image from 'next/image'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@feedbase/ui/components/alert-dialog'; +import { Badge } from '@feedbase/ui/components/badge'; +import { Button } from '@feedbase/ui/components/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuDestructiveItem, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@feedbase/ui/components/dropdown-menu'; +import { Separator } from '@feedbase/ui/components/separator'; +import { Skeleton } from '@feedbase/ui/components/skeleton'; +import { PhotoIcon } from '@heroicons/react/24/outline'; +import { AlertCircle, Copy, Edit, MoreVertical, Trash } from 'lucide-react'; +import { toast } from 'sonner'; +import useSWR from 'swr'; +import { ChangelogProps } from '@/lib/types'; +import { fetcher } from '@/lib/utils'; +import { AddChangelogModal } from '@/components/modals/add-edit-changelog-modal'; +import AnimatedTabs from '@/components/shared/animated-tabs'; + +export default function ChangelogList({ workspaceSlug }: { workspaceSlug: string }) { + const [tab, setTab] = useState('Drafts'); + const [changelogs, setChangelogs] = useState(); + + const { + data: changelogsData, + error, + isLoading, + mutate, + } = useSWR(`/api/v1/workspaces/${workspaceSlug}/changelogs`, fetcher); + + async function onDeleteChangelog(changelog: ChangelogProps['Row']) { + const promise = new Promise((resolve, reject) => { + fetch(`/api/v1/workspaces/${workspaceSlug}/changelogs/${changelog.id}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((res) => res.json()) + .then((data) => { + if (data.error) { + reject(data.error); + } else { + resolve(data); + } + }) + .catch((err) => { + reject(err.message); + }); + }); + + toast.promise(promise, { + loading: 'Deleting changelog...', + success: `Changelog deleted successfully!`, + error: (err) => { + return err; + }, + }); + + promise.then(() => { + mutate(); + }); + } + + async function onDuplicateChangelog(changelog: ChangelogProps['Row']) { + const promise = new Promise((resolve, reject) => { + fetch(`/api/v1/workspaces/${workspaceSlug}/changelogs`, { + method: 'POST', + body: JSON.stringify({ + title: changelog.title, + summary: changelog.summary, + content: changelog.content, + image: changelog.thumbnail, + publish_date: new Date().toISOString(), + published: false, + }), + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((res) => res.json()) + .then((data) => { + if (data.error) { + reject(data.error); + } else { + resolve(data); + } + }) + .catch((err) => { + reject(err.message); + }); + }); + + toast.promise(promise, { + loading: 'Duplicating changelog...', + success: `Changelog duplicated successfully!`, + error: (err) => { + return err; + }, + }); + + promise.then(() => { + mutate(); + }); + } + + useEffect(() => { + if (changelogsData) { + if (tab) { + const filteredChangelogs = changelogsData.filter((changelog) => { + if (tab === 'Drafts') { + return !changelog.published; + } else if (tab === 'Scheduled') { + return ( + changelog.published && + changelog.publish_date && + changelog.publish_date > new Date().toISOString() + ); + } else if (tab === 'Published') { + return ( + changelog.published && + changelog.publish_date && + changelog.publish_date <= new Date().toISOString() + ); + } + return false; + }); + + // Sort by publish date (newest first) + filteredChangelogs.sort((a, b) => { + return new Date(b.publish_date!).getTime() - new Date(a.publish_date!).getTime(); + }); + + setChangelogs(filteredChangelogs); + } else { + setChangelogs(changelogsData); + } + } + }, [changelogsData, tab]); + + return ( + <> + {/* Header tabs */} + + + + +
+ {isLoading + ? [...Array(5)].map((_, index) => ( + // eslint-disable-next-line react/no-array-index-key + + )) + : null} + + {/* Error State */} + {error && !isLoading ? ( +
+ +
+
+ Failed to load changelogs. Please try again. +
+ {/* Show error message - only hsow if error.message is of type json */} + {(() => { + try { + return ( +

{JSON.parse(error.message)?.error}

+ ); + } catch (parseError) { + return null; + } + })()} +
+ +
+ ) : null} + + {/* Empty State */} + {changelogs && changelogs.length === 0 && !error ? ( +
+
+

No changelogs found.

+

Create a new changelog to get started.

+
+ + + +
+ ) : null} + + {/* Changelogs */} + {changelogs?.map((changelog) => ( +
+ {/* Image */} +
+ {changelog.thumbnail ? ( +
+
+ Preview Image +
+
+ ) : ( +
+
+
+ +

No image

+
+
+
+ )} +
+ + {/* Content */} +
+ {/* Tags */} +
+ {/* If published is true, show published badge, else show Draft badge */} + + {changelog.published ? 'Published' : 'Draft'} + + + {/* If date is not undefined, show date */} + {changelog.publish_date ? ( + + {new Date(changelog.publish_date).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + })} + + ) : null} +
+ + {/* Title and Summary */} +
+ {changelog.title ? changelog.title : 'Untitled'} +
+ {changelog.summary ? changelog.summary : 'No changelog summary provided.'} +
+
+
+ + {/* Actions */} + + + + + + + { + event.preventDefault(); + event.stopPropagation(); + }}> + + Edit + + + { + onDuplicateChangelog(changelog); + if (changelog.published) { + setTab('Drafts'); + } + }}> + + {changelog.published ? 'Duplicate in Drafts' : 'Duplicate'} + + + + { + event.preventDefault(); + event.stopPropagation(); + }}> + + Delete + + + + + Delete Changelog + + Are you sure you want to delete this changelog? This action cannot be undone. + + + + Cancel + onDeleteChangelog(changelog)}> + Yes, delete + + + + + + +
+ ))} +
+ + ); +} diff --git a/apps/web/components/layout/unsubscribe-card.tsx b/apps/web/components/changelog/unsubscribe-card.tsx similarity index 75% rename from apps/web/components/layout/unsubscribe-card.tsx rename to apps/web/components/changelog/unsubscribe-card.tsx index 3bbe9fc..e82d7f5 100644 --- a/apps/web/components/layout/unsubscribe-card.tsx +++ b/apps/web/components/changelog/unsubscribe-card.tsx @@ -1,16 +1,16 @@ 'use client'; import { useRouter } from 'next/navigation'; -import { Button } from '@ui/components/ui/button'; +import { Button } from '@feedbase/ui/components/button'; +import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from '@feedbase/ui/components/card'; import { toast } from 'sonner'; -import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui/components/ui/card'; -import { ProjectProps } from '@/lib/types'; +import { WorkspaceProps } from '@/lib/types'; export default function UnsubscribeChangelogCard({ - project, + workspace, subId, }: { - project: ProjectProps['Row']; + workspace: WorkspaceProps['Row']; subId: string; }) { const router = useRouter(); @@ -18,7 +18,7 @@ export default function UnsubscribeChangelogCard({ // on click unsubscribe async function unsubscribe() { const promise = new Promise((resolve, reject) => { - fetch(`/api/v1/${project.slug}/changelogs/subscribers`, { + fetch(`/api/v1/${workspace.slug}/changelogs/subscribers`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', @@ -55,9 +55,9 @@ export default function UnsubscribeChangelogCard({ return ( - Unsubscribe from {project.name} Changelogs + Unsubscribe from {workspace.name} Changelogs - You will no longer receive changelog updates from {project.name}. You can resubscribe at any time. + You will no longer receive changelog updates from {workspace.name}. You can resubscribe at any time. diff --git a/apps/web/components/dashboard/analytics/bar-list.tsx b/apps/web/components/dashboard/analytics/bar-list.tsx deleted file mode 100644 index 1b7d956..0000000 --- a/apps/web/components/dashboard/analytics/bar-list.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { Button } from '@ui/components/ui/button'; -import { - ResponsiveDialog, - ResponsiveDialogClose, - ResponsiveDialogContent, - ResponsiveDialogFooter, - ResponsiveDialogHeader, - ResponsiveDialogTitle, - ResponsiveDialogTrigger, -} from '@ui/components/ui/responsive-dialog'; -import { AnalyticsProps } from '@/lib/types'; - -type BarListProps = { - data: AnalyticsProps; - showData: 'clicks' | 'visitors' | 'both'; - title?: string; - moreData?: AnalyticsProps; - maxItems?: number; -}; - -export default function BarList({ data, showData, title, maxItems }: BarListProps) { - const max = Math.max( - ...data.map((d) => - showData === 'clicks' ? d.clicks : showData === 'visitors' ? d.visitors : d.clicks + d.visitors - ) - ); - - function calcItemLength(value: number) { - return (value / max) * 100; - } - - return ( -
-
- {/* Bars */} -
- {data.slice(0, maxItems ? maxItems + 1 : data.length).map((d) => ( -
-
- - {d.key} - -
- ))} -
- - {/* Labels */} -
- {data.slice(0, maxItems ? maxItems + 1 : data.length).map((d) => - showData === 'clicks' ? ( -

- {d.clicks} -

- ) : showData === 'visitors' ? ( -

- {d.visitors} -

- ) : ( -
-

{d.visitors}

-

{d.clicks}

-
- ) - )} -
-
- - {/* If there are more than maxItems, show a "more" label */} - {maxItems && data.length > maxItems ? ( -
- - - - - - - {title} - -
- Visitors - Views -
-
- -
- -
- - - - - - -
-
-
- ) : null} -
- ); -} diff --git a/apps/web/components/dashboard/analytics/line-chart.tsx b/apps/web/components/dashboard/analytics/line-chart.tsx deleted file mode 100644 index dfcde6e..0000000 --- a/apps/web/components/dashboard/analytics/line-chart.tsx +++ /dev/null @@ -1,177 +0,0 @@ -'use client'; - -/* eslint-disable */ -import { useEffect } from 'react'; -import { - CategoryScale, - Chart as ChartJS, - ChartOptions, - Filler, - Legend, - LinearScale, - LineElement, - Plugin, - PointElement, - Title, - Tooltip, -} from 'chart.js'; -import { AnyObject } from 'chart.js/dist/types/basic'; -import { Line } from 'react-chartjs-2'; - -ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Filler, Legend); - -const options: ChartOptions<'line'> = { - responsive: true, - maintainAspectRatio: false, - elements: { - point: { - // @ts-expect-error - type definition is wrong - pointBackgroundColor: 'white', - hoverBackgroundColor: 'white', - borderWidth: 2, - hoverRadius: 4, - hoverBorderWidth: 2, - }, - line: { - tension: 0.4, - borderWidth: 2, - fill: true, - }, - }, - interaction: { - mode: 'index', - intersect: false, - }, - plugins: { - legend: { - display: false, - }, - }, - scales: { - x: { - grid: { - display: true, - drawOnChartArea: false, - drawTicks: true, - color: 'rgba(51,54,61,1)', - tickColor: 'rgba(51,54,61,1)', - }, - ticks: { - font: { - size: 10, - }, - padding: 6, - }, - }, - y: { - type: 'linear', - grid: { - display: true, - drawTicks: true, - color: 'rgba(51,54,61,1)', - tickColor: 'rgba(51,54,61,1)', - }, - border: { - display: true, - color: 'rgba(51,54,61,1)', - }, - ticks: { - font: { - size: 10, - }, - padding: 8, - precision: 0, - }, - min: 0, - }, - }, -}; - -const intersectDataVerticalLine: Plugin<'line', AnyObject>[] = [ - { - id: 'intersectDataVerticalLine', - beforeDraw: (chart: any) => { - if (chart.getActiveElements().length) { - const activePoint = chart.getActiveElements()[0]; - const chartArea = chart.chartArea; - const ctx = chart.ctx; - ctx.save(); - // grey vertical hover line - full chart height - ctx.beginPath(); - ctx.moveTo(activePoint.element.x, chartArea.top); - ctx.lineTo(activePoint.element.x, chartArea.bottom); - ctx.lineWidth = 2; - ctx.strokeStyle = 'rgba(143,160,255,0.3)'; - ctx.stroke(); - ctx.restore(); - - // colored vertical hover line - ['data point' to chart bottom] - only for charts 1 dataset - if (chart.data.datasets.length === 1) { - ctx.beginPath(); - ctx.moveTo(activePoint.element.x, activePoint.element.y); - ctx.lineTo(activePoint.element.x, chartArea.bottom); - ctx.lineWidth = 2; - ctx.strokeStyle = chart.data.datasets[0].borderColor; - ctx.stroke(); - ctx.restore(); - } - } - }, - }, -]; -/* eslint-enable */ - -export default function LineChart({ - labels, - data, - dataLabel, -}: { - labels: string[]; - data: number[]; - dataLabel: string; -}) { - // BUG: For some reason the max value is not calculated correctly and it is the same for all charts as the highest value in any chart - // options.scales!.y!.max = Math.max(...data) + 5; - - // Update step size of y axis (whole numbers only) - // options.scales!.y!.ticks!.stepSize = Math.ceil(Math.max(...data) / 2); - - // Resize event listener - useEffect(() => { - function handleResize() { - const chart = document.getElementById(dataLabel); - if (chart) { - chart.style.height = '250px'; - chart.style.width = '100%'; - } - } - - window.addEventListener('resize', handleResize); - - return () => { - window.removeEventListener('resize', handleResize); - }; - }, [dataLabel]); - - return ( - - ); -} diff --git a/apps/web/components/dashboard/changelogs/api-sheet.tsx b/apps/web/components/dashboard/changelogs/api-sheet.tsx deleted file mode 100644 index eef6751..0000000 --- a/apps/web/components/dashboard/changelogs/api-sheet.tsx +++ /dev/null @@ -1,75 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { toast } from 'sonner'; -import { Alert, AlertDescription, AlertTitle } from 'ui/components/ui/alert'; -import { Button } from 'ui/components/ui/button'; -import { Input } from 'ui/components/ui/input'; -import { Label } from 'ui/components/ui/label'; -import { - Sheet, - SheetContent, - SheetDescription, - SheetFooter, - SheetHeader, - SheetTitle, - SheetTrigger, -} from 'ui/components/ui/sheet'; -import { formatRootUrl } from '@/lib/utils'; -import { CodeIcon } from '@/components/shared/icons/icons-animated'; -import LottiePlayer from '@/components/shared/lottie-player'; - -export function ApiSheet({ projectSlug }: { projectSlug: string }) { - const [isHover, setIsHover] = useState(false); - - return ( - - - - - - - - Early Feature - - Kindly note that this feature is currently in alpha and thus has limited customizability. - - - Changelogs API - - The Changelogs API enables seamless integration of public project updates into your custom - websites and apps for enhanced personalization. - - -
- {/* Endpoint */} -
- - - -
-
- -
-
- ); -} diff --git a/apps/web/components/dashboard/changelogs/changelog-list.tsx b/apps/web/components/dashboard/changelogs/changelog-list.tsx deleted file mode 100644 index 86ac7b9..0000000 --- a/apps/web/components/dashboard/changelogs/changelog-list.tsx +++ /dev/null @@ -1,194 +0,0 @@ -'use client'; - -import Image from 'next/image'; -import { PhotoIcon } from '@heroicons/react/24/outline'; -import { MoreVertical } from 'lucide-react'; -import { toast } from 'sonner'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from 'ui/components/ui/alert-dialog'; -import { Badge } from 'ui/components/ui/badge'; -import { Button } from 'ui/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from 'ui/components/ui/dropdown-menu'; -import { ChangelogProps } from '@/lib/types'; -import { AddChangelogModal } from '@/components/dashboard/modals/add-edit-changelog-modal'; - -export default function ChangelogList({ - changelogs, - projectSlug, -}: { - changelogs: ChangelogProps['Row'][]; - projectSlug: string; -}) { - async function onDeleteChangelog(changelog: ChangelogProps['Row']) { - const promise = new Promise((resolve, reject) => { - fetch(`/api/v1/projects/${projectSlug}/changelogs/${changelog.id}`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - }) - .then((res) => res.json()) - .then((data) => { - if (data.error) { - reject(data.error); - } else { - resolve(data); - } - }) - .catch((err) => { - reject(err.message); - }); - }); - - toast.promise(promise, { - loading: 'Deleting changelog...', - success: `${changelog.title !== '' ? changelog.title : 'Draft'} was deleted successfully!`, - error: (err) => { - return err; - }, - }); - - promise.then(() => { - window.location.reload(); - }); - } - - return ( -
- {changelogs.map((changelog) => ( -
- {/* Image */} -
- {changelog.image ? ( -
-
- Preview Image -
-
- ) : ( -
-
-
- -

No image

-
-
-
- )} -
- - {/* Content */} -
- {/* Tags */} -
-
- {/* If published is true, show published badge, else show draft badge */} - - {changelog.published ? 'Published' : 'Draft'} - - - {/* If date is not undefined, show date */} - {changelog.publish_date ? ( - - {new Date(changelog.publish_date).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - })} - - ) : null} -
- - {/* Title and Summary */} -
{changelog.title ? changelog.title : 'Untitled'}
-
- {changelog.summary ? changelog.summary : 'No changelog summary provided.'} -
-
- - {/* Actions */} - {/* Top right corner at very top of card */} -
- - - - - - { - event.preventDefault(); - event.stopPropagation(); - }}> - Edit - - } - changelogData={changelog} - isEdit - /> - - - - { - event.preventDefault(); - event.stopPropagation(); - }} - className='text-destructive focus:text-destructive/90'> - Delete - - - - - Delete Changlog - - Are you sure you want to delete this changelog? This action cannot be undone. - - - - Cancel - onDeleteChangelog(changelog)}> - Yes, delete - - - - - - -
-
-
- ))} -
- ); -} diff --git a/apps/web/components/dashboard/changelogs/content-editor.tsx b/apps/web/components/dashboard/changelogs/content-editor.tsx deleted file mode 100644 index e3dce43..0000000 --- a/apps/web/components/dashboard/changelogs/content-editor.tsx +++ /dev/null @@ -1,49 +0,0 @@ -'use client'; - -import React from 'react'; -import { Highlight } from '@tiptap/extension-highlight'; -import { Link } from '@tiptap/extension-link'; -import { Typography } from '@tiptap/extension-typography'; -import { AnyExtension, EditorContent, useEditor } from '@tiptap/react'; -import { StarterKit } from '@tiptap/starter-kit'; -import { ChangelogProps } from '@/lib/types'; -import TooltipLabel from '@/components/shared/tooltip-label'; - -export default function RichTextEditor({ - data, - setData, -}: { - data: ChangelogProps['Row']; - setData: React.Dispatch>; -}) { - const editor = useEditor({ - extensions: [ - StarterKit as AnyExtension, - Highlight, - Link.configure({ - HTMLAttributes: { - class: 'cursor-pointer', - }, - }), - Typography, - ], - content: data.content, - editorProps: { - attributes: { - class: 'prose prose-invert prose-sm sm:prose-base dark:prose-invert m-5 focus:outline-none', - }, - }, - onUpdate: ({ editor }) => { - setData({ ...data, content: editor.getHTML() }); - }, - }); - - return ( -
- -
- -
-
- ); -} diff --git a/apps/web/components/dashboard/changelogs/date-picker.tsx b/apps/web/components/dashboard/changelogs/date-picker.tsx deleted file mode 100644 index b7ad92f..0000000 --- a/apps/web/components/dashboard/changelogs/date-picker.tsx +++ /dev/null @@ -1,48 +0,0 @@ -'use client'; - -import * as React from 'react'; -import { cn } from '@ui/lib/utils'; -import { format } from 'date-fns'; -import { Button } from 'ui/components/ui/button'; -import { Calendar } from 'ui/components/ui/calendar'; -import { Popover, PopoverContent, PopoverTrigger } from 'ui/components/ui/popover'; -import { ChangelogProps } from '@/lib/types'; - -export function PublishDatePicker({ - className, - data, - setData, -}: { - className?: string; - data: ChangelogProps['Row']; - setData: React.Dispatch>; -}) { - return ( - - - - - - { - setData({ ...data, publish_date: date?.toISOString() ?? null }); - }} - /> - - - ); -} diff --git a/apps/web/components/dashboard/changelogs/image-upload.tsx b/apps/web/components/dashboard/changelogs/image-upload.tsx deleted file mode 100644 index 014d393..0000000 --- a/apps/web/components/dashboard/changelogs/image-upload.tsx +++ /dev/null @@ -1,137 +0,0 @@ -'use client'; - -/* - This component is a modified version of Steven Tey's OGSection component from the dub project. - Big shoutout to him for making this component open source! - Credits: https://github.com/steven-tey/dub/blob/23cea302493a2e240fb31a75d3bf0da3979a0abc/components/app/modals/add-edit-link-modal/og-section.tsx#L4 -*/ -import { Dispatch, useCallback, useState } from 'react'; -import Image from 'next/image'; -import { CloudArrowUpIcon } from '@heroicons/react/24/outline'; -import { ChangelogProps } from '@/lib/types'; -import TooltipLabel from '@/components/shared/tooltip-label'; - -export default function FileDrop({ - data, - setData, -}: { - data: ChangelogProps['Row']; - setData: Dispatch>; -}) { - const [fileError, setFileError] = useState(null); - const [dragActive, setDragActive] = useState(false); - - const onChangePicture = useCallback( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (e: any) => { - setFileError(null); - const file = e.target.files[0]; - if (file) { - if (file.size / 1024 / 1024 > 5) { - setFileError('File size too big (max 5MB)'); - } else if (file.type !== 'image/png' && file.type !== 'image/jpeg') { - setFileError('File type not supported.'); - } else { - const reader = new FileReader(); - reader.onload = (e) => { - setData((prev) => ({ ...prev, image: e.target?.result as string })); - }; - reader.readAsDataURL(file); - } - } - }, - [setData] - ); - - return ( -
-
- - {fileError ?

{fileError}

: null} -
- {/* BUG: Fix this so that its not a fixed height but rather just takes over full height properly */} - {/* As currently it is not on the same level as the other inputs */} -