├── .npmrc
├── apps
├── web
│ ├── app
│ │ ├── favicon.ico
│ │ ├── (auth)
│ │ │ ├── sign-in
│ │ │ │ └── [[...sign-in]]
│ │ │ │ │ └── page.tsx
│ │ │ ├── sign-up
│ │ │ │ └── [[...sign-up]]
│ │ │ │ │ └── page.tsx
│ │ │ └── layout.tsx
│ │ ├── (main)
│ │ │ ├── layout.tsx
│ │ │ ├── dashboard
│ │ │ │ ├── settings
│ │ │ │ │ ├── organization-settings
│ │ │ │ │ │ └── [[...rest]]
│ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── _component
│ │ │ │ │ ├── sidebar
│ │ │ │ │ │ └── orgs-sidebar
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ ├── org-list.tsx
│ │ │ │ │ │ │ ├── new-org-button.tsx
│ │ │ │ │ │ │ └── org-item.tsx
│ │ │ │ │ ├── invite-button.tsx
│ │ │ │ │ └── dashboard-top-bar.tsx
│ │ │ │ ├── page-client.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ └── layout.tsx
│ │ │ └── projects
│ │ │ │ └── [id]
│ │ │ │ ├── resources
│ │ │ │ └── page.tsx
│ │ │ │ ├── feedbacks
│ │ │ │ └── page.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── changelogs
│ │ │ │ └── page.tsx
│ │ │ │ ├── dashboard
│ │ │ │ ├── page.tsx
│ │ │ │ └── page-client.tsx
│ │ │ │ ├── error.tsx
│ │ │ │ ├── messages
│ │ │ │ └── page.tsx
│ │ │ │ ├── work-items
│ │ │ │ └── page.tsx
│ │ │ │ ├── _components
│ │ │ │ ├── project-sidebar.tsx
│ │ │ │ ├── project-mobile-bar.tsx
│ │ │ │ └── owned-task-table.tsx
│ │ │ │ └── settings
│ │ │ │ ├── layout.tsx
│ │ │ │ └── danger-zone
│ │ │ │ └── page.tsx
│ │ ├── api
│ │ │ ├── kafka
│ │ │ │ └── produce
│ │ │ │ │ └── route.ts
│ │ │ ├── feedback
│ │ │ │ ├── [id]
│ │ │ │ │ └── route.ts
│ │ │ │ └── route.ts
│ │ │ ├── changelog
│ │ │ │ └── [id]
│ │ │ │ │ └── route.ts
│ │ │ └── webhooks
│ │ │ │ └── clerk
│ │ │ │ └── utils
│ │ │ │ ├── organization.ts
│ │ │ │ ├── team-membership.ts
│ │ │ │ └── user.ts
│ │ ├── global-error.tsx
│ │ ├── layout.tsx
│ │ └── globals.css
│ ├── public
│ │ ├── hero.png
│ │ ├── logo.png
│ │ └── thumbnail.png
│ ├── postcss.config.js
│ ├── docs
│ │ ├── README.md
│ │ └── code-export-5-25-2024-7_52_31-PM.txt
│ ├── components
│ │ ├── loading.tsx
│ │ ├── theme
│ │ │ ├── theme-provider.tsx
│ │ │ └── theme-switch.tsx
│ │ ├── ui
│ │ │ ├── skeleton.tsx
│ │ │ ├── textarea.tsx
│ │ │ ├── label.tsx
│ │ │ ├── input.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── progress.tsx
│ │ │ ├── toaster.tsx
│ │ │ ├── sonner.tsx
│ │ │ ├── checkbox.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── tooltip.tsx
│ │ │ ├── popover.tsx
│ │ │ ├── date-picker.tsx
│ │ │ ├── avatar.tsx
│ │ │ ├── toggle.tsx
│ │ │ ├── scroll-area.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── button.tsx
│ │ │ └── card.tsx
│ │ ├── resources
│ │ │ ├── link
│ │ │ │ ├── add-link.tsx
│ │ │ │ ├── link-list.tsx
│ │ │ │ └── link-card.tsx
│ │ │ ├── file
│ │ │ │ ├── file-icon.tsx
│ │ │ │ ├── file-list.tsx
│ │ │ │ ├── file-card.tsx
│ │ │ │ └── file-upload.tsx
│ │ │ └── resource-list.tsx
│ │ ├── projects
│ │ │ ├── project-status.tsx
│ │ │ ├── project-card.tsx
│ │ │ └── project-list.tsx
│ │ ├── task
│ │ │ ├── task-status.tsx
│ │ │ ├── task-priority.tsx
│ │ │ └── task-title.tsx
│ │ ├── landing-page
│ │ │ ├── mobile-nav.tsx
│ │ │ ├── footer.tsx
│ │ │ └── features-section.tsx
│ │ ├── feedback
│ │ │ ├── feedback-type.tsx
│ │ │ ├── feedback-status.tsx
│ │ │ ├── feedback-header.tsx
│ │ │ ├── feedback-card.tsx
│ │ │ ├── feedback-list.tsx
│ │ │ └── feedback-integration.tsx
│ │ ├── providers
│ │ │ ├── auth-load-provider.tsx
│ │ │ ├── convex-client-provider.tsx
│ │ │ └── modal-provider.tsx
│ │ ├── empty-states
│ │ │ ├── no-org.tsx
│ │ │ └── no-projects.tsx
│ │ ├── hint.tsx
│ │ ├── changelogs
│ │ │ ├── changelog-list.tsx
│ │ │ ├── changelog-card.tsx
│ │ │ └── changelog-actions.tsx
│ │ ├── md
│ │ │ ├── link-modal.tsx
│ │ │ └── mdx-editor.tsx
│ │ ├── messages
│ │ │ ├── message-chat-action.tsx
│ │ │ ├── message-input.tsx
│ │ │ └── message-chat.tsx
│ │ ├── work-items
│ │ │ ├── data-table-view-options.tsx
│ │ │ ├── data-table-row-actions.tsx
│ │ │ └── data-table-column-header.tsx
│ │ ├── modals
│ │ │ ├── confirm-modal.tsx
│ │ │ └── input-modal.tsx
│ │ └── code-with-copy.tsx
│ ├── lib
│ │ ├── hooks
│ │ │ ├── use-constructions.ts
│ │ │ ├── use-current-user.ts
│ │ │ ├── use-scroll.ts
│ │ │ ├── use-messages.ts
│ │ │ ├── use-api-mutation.ts
│ │ │ ├── use-event.ts
│ │ │ ├── use-files-uploads.ts
│ │ │ └── use-workItem-table.tsx
│ │ ├── form-schemas.ts
│ │ ├── store
│ │ │ ├── use-file-modal.ts
│ │ │ ├── use-link-modal.ts
│ │ │ ├── use-feedback-modal.ts
│ │ │ ├── use-changelog-modal.ts
│ │ │ └── use-task-modal.ts
│ │ ├── kafka.ts
│ │ ├── ratelimit.ts
│ │ └── types.ts
│ ├── .eslintrc.js
│ ├── instrumentation.ts
│ ├── tsconfig.json
│ ├── middleware.ts
│ ├── components.json
│ ├── .gitignore
│ ├── .env.example
│ ├── sentry.server.config.ts
│ ├── sentry.client.config.ts
│ ├── sentry.edge.config.ts
│ ├── README.md
│ └── next.config.js
└── kafka-consumer
│ ├── .env.example
│ ├── src
│ ├── utils.ts
│ └── index.ts
│ └── package.json
├── packages
├── eslint-config
│ ├── README.md
│ ├── package.json
│ ├── library.js
│ ├── next.js
│ └── react-internal.js
├── backend
│ ├── .env.example
│ ├── lib
│ │ ├── types.ts
│ │ └── utils.ts
│ ├── package.json
│ ├── convex
│ │ ├── auth.config.ts
│ │ ├── _generated
│ │ │ ├── api.js
│ │ │ ├── dataModel.d.ts
│ │ │ └── api.d.ts
│ │ ├── http.ts
│ │ ├── types.ts
│ │ ├── resources
│ │ │ ├── storage.ts
│ │ │ ├── file.ts
│ │ │ └── link.ts
│ │ ├── changelog.ts
│ │ ├── feedback.ts
│ │ ├── message.ts
│ │ └── work_item.ts
│ └── tsconfig.json
└── typescript-config
│ ├── package.json
│ ├── react-library.json
│ ├── nextjs.json
│ └── base.json
├── scripts
└── create-topic.sh
├── .github
├── workflows
│ ├── cron-jobs.yml
│ └── pr-checks.yml
├── actions
│ └── ci-setup
│ │ └── action.yml
└── dependabot.yml
├── .gitignore
├── package.json
├── docker-compose.yml
├── turbo.json
├── LICENSE
└── CODE_OF_CONDUCT.md
/.npmrc:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vignesh-gupta/projectify/HEAD/apps/web/app/favicon.ico
--------------------------------------------------------------------------------
/apps/web/public/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vignesh-gupta/projectify/HEAD/apps/web/public/hero.png
--------------------------------------------------------------------------------
/apps/web/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vignesh-gupta/projectify/HEAD/apps/web/public/logo.png
--------------------------------------------------------------------------------
/apps/web/public/thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vignesh-gupta/projectify/HEAD/apps/web/public/thumbnail.png
--------------------------------------------------------------------------------
/packages/eslint-config/README.md:
--------------------------------------------------------------------------------
1 | # `@turbo/eslint-config`
2 |
3 | Collection of internal eslint configurations.
4 |
--------------------------------------------------------------------------------
/apps/web/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/packages/backend/.env.example:
--------------------------------------------------------------------------------
1 | CONVEX_DEPLOYMENT=
2 | NEXT_PUBLIC_CONVEX_URL=
3 | NEXT_PUBLIC_CONVEX_DEPLOYMENT_SITE=
4 |
5 | UNASSIGNED_USER_ID=
--------------------------------------------------------------------------------
/apps/web/app/(auth)/sign-in/[[...sign-in]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignIn } from "@clerk/nextjs";
2 |
3 | export default function Page() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/apps/web/app/(auth)/sign-up/[[...sign-up]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignUp } from "@clerk/nextjs";
2 |
3 | export default function Page() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/scripts/create-topic.sh:
--------------------------------------------------------------------------------
1 | docker exec -it local-kafka /opt/kafka/bin/kafka-topics.sh --create --zookeeper zookeeper:2181 --replication-factor 1 --partitions 1 --topic test
--------------------------------------------------------------------------------
/apps/kafka-consumer/.env.example:
--------------------------------------------------------------------------------
1 | KAFKA_BROKER=
2 | KAFKA_USERNAME=
3 | KAFKA_PASSWORD=
4 | KAFKA_TOPIC=
5 |
6 | CONVEX_DEPLOYMENT=
7 | CONVEX_URL=
8 | CONVEX_DEPLOYMENT_SITE=
--------------------------------------------------------------------------------
/apps/kafka-consumer/src/utils.ts:
--------------------------------------------------------------------------------
1 | var { ConvexClient } = require("convex/browser");
2 |
3 | export const convexClient = new ConvexClient(
4 | process.env.CONVEX_URL!
5 | );
6 |
--------------------------------------------------------------------------------
/apps/web/docs/README.md:
--------------------------------------------------------------------------------
1 | # Projectify Docs
2 |
3 | ## DB Schema ER Diagram
4 |
5 | 
--------------------------------------------------------------------------------
/packages/typescript-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/typescript-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "publishConfig": {
7 | "access": "public"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/typescript-config/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "React Library",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "jsx": "react-jsx"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/apps/web/components/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Loader2 } from "lucide-react";
2 |
3 | export const LoadingSpinner = () => (
4 |
5 |
6 |
7 | );
8 |
--------------------------------------------------------------------------------
/packages/backend/lib/types.ts:
--------------------------------------------------------------------------------
1 | import { Id } from "../convex/_generated/dataModel";
2 |
3 | export type Resource = "project";
4 |
5 | export type Action = "delete";
6 |
7 | export type KafkaMessage = {
8 | action: Action;
9 | id: Id<"projects">;
10 | resource: Resource;
11 | };
12 |
--------------------------------------------------------------------------------
/apps/web/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | export default function AuthLayout({
2 | children,
3 | }: {
4 | children: React.ReactNode;
5 | }) {
6 | return (
7 |
8 | {children}
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/apps/web/lib/hooks/use-constructions.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { toast } from "sonner";
3 |
4 | export const useConstructions = (text: "page" | "area" | "feature") => {
5 | return useEffect(() => {
6 | toast.warning(`🚧 This ${text} is under construction`);
7 | }, [text]);
8 | };
9 |
--------------------------------------------------------------------------------
/packages/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/backend",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "dev": "convex dev",
6 | "setup": "convex dev --until-success"
7 | },
8 | "author": "",
9 | "license": "ISC",
10 | "dependencies": {
11 | "convex": "^1.14.4"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/apps/web/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /** @type {import("eslint").Linter.Config} */
2 | module.exports = {
3 | root: true,
4 | extends: ["@repo/eslint-config/next.js"],
5 | parser: "@typescript-eslint/parser",
6 | parserOptions: {
7 | project: true,
8 | ecmaVersion: 2024,
9 | sourceType: "module",
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/.github/workflows/cron-jobs.yml:
--------------------------------------------------------------------------------
1 | name: "Cron jobs"
2 | on:
3 | workflow_dispatch:
4 |
5 | jobs:
6 | health_check:
7 | name: Health check
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Consumer health check
11 | run: curl --location --request GET 'https://projectify-im8j.onrender.com/health'
12 |
--------------------------------------------------------------------------------
/apps/web/components/theme/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ThemeProvider as NextThemesProvider, type ThemeProviderProps } from "next-themes"
5 |
6 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
7 | return {children}
8 | }
9 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/layout.tsx:
--------------------------------------------------------------------------------
1 | import AuthLoadProvider from "@/components/providers/auth-load-provider";
2 | import React from "react";
3 |
4 | const AppLayout = ({
5 | children,
6 | }: Readonly<{
7 | children: React.ReactNode;
8 | }>) => {
9 | return {children} ;
10 | };
11 |
12 | export default AppLayout;
13 |
--------------------------------------------------------------------------------
/apps/web/lib/hooks/use-current-user.ts:
--------------------------------------------------------------------------------
1 | import { api } from "@repo/backend/convex/_generated/api";
2 | import { useAuth } from "@clerk/nextjs";
3 | import { useQuery } from "convex/react";
4 |
5 | export const useCurrentUser = () => {
6 | const { userId } = useAuth();
7 |
8 | return useQuery(api.user.get, { clerkId: userId as string });
9 | };
10 |
--------------------------------------------------------------------------------
/apps/web/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/dashboard/settings/organization-settings/[[...rest]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { OrganizationProfile } from "@clerk/nextjs";
2 |
3 | const AccountSettings = () => {
4 | return (
5 |
6 |
7 |
8 | );
9 | };
10 |
11 | export default AccountSettings;
12 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/projects/[id]/resources/page.tsx:
--------------------------------------------------------------------------------
1 | import ResourceList from "@/components/resources/resource-list";
2 |
3 | const ResourcesPage = () => {
4 | return (
5 |
6 |
Resources
7 |
8 |
9 | );
10 | };
11 |
12 | export default ResourcesPage;
13 |
--------------------------------------------------------------------------------
/apps/web/instrumentation.ts:
--------------------------------------------------------------------------------
1 | import * as Sentry from '@sentry/nextjs';
2 |
3 | export async function register() {
4 | if (process.env.NEXT_RUNTIME === 'nodejs') {
5 | await import('./sentry.server.config');
6 | }
7 |
8 | if (process.env.NEXT_RUNTIME === 'edge') {
9 | await import('./sentry.edge.config');
10 | }
11 | }
12 |
13 | export const onRequestError = Sentry.captureRequestError;
--------------------------------------------------------------------------------
/packages/backend/convex/auth.config.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/no-anonymous-default-export
2 | export default {
3 | providers: [
4 | {
5 | domain: "https://devoted-hare-63.clerk.accounts.dev",
6 | applicationID: "convex",
7 | },{
8 | domain: "https://clerk.projectifyauth.vigneshgupta.me",
9 | applicationID: "convex",
10 | }
11 |
12 | ],
13 | };
14 |
--------------------------------------------------------------------------------
/apps/web/app/api/kafka/produce/route.ts:
--------------------------------------------------------------------------------
1 | import { produceMessage } from "@/lib/kafka";
2 | import { KafkaMessage } from "@repo/backend/lib/types";
3 |
4 | export const POST = async (req: Request) => {
5 | const body: KafkaMessage = await req.json();
6 |
7 | console.log("body from route", body);
8 |
9 | const res = await produceMessage(body);
10 |
11 | return Response.json({ message: "Message sent", res });
12 | };
13 |
--------------------------------------------------------------------------------
/packages/typescript-config/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Next.js",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "lib": ["dom", "dom.iterable", "esnext"],
7 | "allowJs": true,
8 | "noEmit": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "jsx": "preserve",
12 | "incremental": true,
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "plugins": [
5 | {
6 | "name": "next"
7 | }
8 | ],
9 | "paths": {
10 | "@/*": ["./*"]
11 | }
12 | },
13 | "include": [
14 | "next-env.d.ts",
15 | "next.config.js",
16 | "**/*.ts",
17 | "**/*.tsx",
18 | ".next/types/**/*.ts"
19 | ],
20 | "exclude": ["node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/dashboard/_component/sidebar/orgs-sidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import NewButton from "./new-org-button";
2 | import OrganizationList from "./org-list";
3 |
4 | const OrganizationsSideBar = () => {
5 | return (
6 |
10 | );
11 | };
12 |
13 | export default OrganizationsSideBar;
14 |
--------------------------------------------------------------------------------
/apps/web/components/resources/link/add-link.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { useLinkModal } from "@/lib/store/use-link-modal";
5 | import { Plus } from "lucide-react";
6 |
7 | const AddLink = () => {
8 | const { onOpen } = useLinkModal();
9 | return (
10 | onOpen()}>
11 |
12 | Add
13 |
14 | );
15 | };
16 |
17 | export default AddLink;
18 |
--------------------------------------------------------------------------------
/apps/web/lib/form-schemas.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const feedbackFormSchema = z.object({
4 | id: z.string().optional(),
5 | content: z.string().min(10).max(500),
6 | projectId: z.string().min(10),
7 | senderName: z.string(),
8 | senderEmail: z.string().email(),
9 | status: z.enum(["open", "reviewed", "closed"]).optional().default("open"),
10 | type: z
11 | .enum(["documentation", "feature", "issue", "question", "idea", "other"])
12 | .optional()
13 | .default("feature"),
14 | });
15 |
--------------------------------------------------------------------------------
/apps/web/middleware.ts:
--------------------------------------------------------------------------------
1 | import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
2 |
3 | const isPublicRoute = createRouteMatcher([
4 | "/",
5 | "/sign-in(.*)",
6 | "/sign-up(.*)",
7 | "/changelog(.*)",
8 | "/api(.*)",
9 | ]);
10 |
11 | export default clerkMiddleware( async (auth, request) => {
12 | if (!isPublicRoute(request)) {
13 | await auth.protect();
14 | }
15 |
16 | });
17 |
18 | export const config = {
19 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
20 | };
21 |
--------------------------------------------------------------------------------
/apps/web/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/packages/backend/convex/_generated/api.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * Generated `api` utility.
4 | *
5 | * THIS CODE IS AUTOMATICALLY GENERATED.
6 | *
7 | * To regenerate, run `npx convex dev`.
8 | * @module
9 | */
10 |
11 | import { anyApi } from "convex/server";
12 |
13 | /**
14 | * A utility for referencing Convex functions in your app's API.
15 | *
16 | * Usage:
17 | * ```js
18 | * const myFunctionReference = api.myModule.myFunction;
19 | * ```
20 | */
21 | export const api = anyApi;
22 | export const internal = anyApi;
23 |
--------------------------------------------------------------------------------
/packages/backend/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { Id } from "../convex/_generated/dataModel";
2 |
3 | export function generateAPIKey(length: number): string {
4 | const charset =
5 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
6 | let key = "pk_live_";
7 | for (let i = 0; i < length; i++) {
8 | key += charset.charAt(Math.floor(Math.random() * charset.length));
9 | }
10 | return key;
11 | }
12 |
13 | export const UNASSIGNED_USER = {
14 | label: "Unassigned",
15 | value: process.env.UNASSIGNED_USER_ID as Id<"users">,
16 | };
17 |
--------------------------------------------------------------------------------
/apps/web/lib/hooks/use-scroll.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 |
3 | export const useScroll = (
4 | behavior?: ScrollBehavior | undefined,
5 | block?: ScrollLogicalPosition | undefined,
6 | dependencies: any[] = []
7 | ) => {
8 | const scrollRef = useRef(null);
9 |
10 | useEffect(() => {
11 | if (!scrollRef.current) return;
12 |
13 | scrollRef.current.scrollIntoView({
14 | block,
15 | behavior,
16 | });
17 | }, [behavior, block, ...dependencies]);
18 |
19 | return scrollRef;
20 | };
21 |
--------------------------------------------------------------------------------
/packages/eslint-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/eslint-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "files": [
6 | "library.js",
7 | "next.js",
8 | "react-internal.js"
9 | ],
10 | "devDependencies": {
11 | "@vercel/style-guide": "^6.0.0",
12 | "eslint-config-turbo": "^2.0.0",
13 | "eslint-config-prettier": "^10.0.2",
14 | "eslint-plugin-only-warn": "^1.1.0",
15 | "@typescript-eslint/parser": "^8.26.0",
16 | "@typescript-eslint/eslint-plugin": "^8.26.0",
17 | "typescript": "^5.3.3"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # Dependencies
4 | node_modules
5 | .pnp
6 | .pnp.js
7 |
8 | # Local env files
9 | .env
10 | .env.local
11 | .env.development.local
12 | .env.test.local
13 | .env.production.local
14 |
15 | # Testing
16 | coverage
17 |
18 | # Turbo
19 | .turbo
20 |
21 | # Vercel
22 | .vercel
23 |
24 | # Build Outputs
25 | .next/
26 | out/
27 | build
28 | dist
29 |
30 |
31 | # Debug
32 | npm-debug.log*
33 | yarn-debug.log*
34 | yarn-error.log*
35 |
36 | # Misc
37 | .DS_Store
38 | *.pem
39 |
--------------------------------------------------------------------------------
/apps/web/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 |
29 | # vercel
30 | .vercel
31 |
32 | # typescript
33 | *.tsbuildinfo
34 | next-env.d.ts
35 |
36 | # Sentry Config File
37 | .env.sentry-build-plugin
38 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/dashboard/page-client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import NoOrg from '@/components/empty-states/no-org'
4 | import ProjectList from '@/components/projects/project-list'
5 | import { useOrganization } from '@clerk/nextjs'
6 | import React from 'react'
7 |
8 | const DashboardClientPage = () => {
9 | const { organization } = useOrganization()
10 |
11 | return (
12 |
13 | {!organization ?
:
}
14 |
15 | )
16 | }
17 |
18 | export default DashboardClientPage
--------------------------------------------------------------------------------
/apps/web/app/(main)/projects/[id]/feedbacks/page.tsx:
--------------------------------------------------------------------------------
1 | import FeedbacksPageHeader from "@/components/feedback/feedback-header";
2 | import { Skeleton } from "@/components/ui/skeleton";
3 | import { lazy, Suspense } from "react";
4 |
5 | const FeedbacksList = lazy(() => import("@/components/feedback/feedback-list"));
6 |
7 | const FeedbacksPage = () => {
8 | return (
9 | <>
10 |
11 | }>
12 |
13 |
14 | >
15 | );
16 | };
17 |
18 | export default FeedbacksPage;
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "projectify",
3 | "private": true,
4 | "scripts": {
5 | "build": "turbo build",
6 | "app:build": "turbo app:build",
7 | "dev": "turbo dev",
8 | "lint": "turbo lint",
9 | "type-check": "turbo type-check",
10 | "format": "prettier --write \"**/*.{ts,tsx,md}\""
11 | },
12 | "devDependencies": {
13 | "prettier": "^3.2.5",
14 | "turbo": "^2.2.3",
15 | "typescript": "^5.4.5"
16 | },
17 | "engines": {
18 | "node": ">=20"
19 | },
20 | "packageManager": "yarn@1.22.21",
21 | "workspaces": [
22 | "apps/*",
23 | "packages/*"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/packages/typescript-config/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "declaration": true,
6 | "declarationMap": true,
7 | "esModuleInterop": true,
8 | "incremental": false,
9 | "isolatedModules": true,
10 | "lib": ["es2022", "DOM", "DOM.Iterable"],
11 | "module": "NodeNext",
12 | "moduleDetection": "force",
13 | "moduleResolution": "NodeNext",
14 | "noUncheckedIndexedAccess": true,
15 | "resolveJsonModule": true,
16 | "skipLibCheck": true,
17 | "strict": true,
18 | "target": "ES2022"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { OrganizationProfile } from "@clerk/nextjs";
3 | import DashboardClientPage from "./page-client";
4 |
5 | type DashboardPageProps = {
6 | searchParams: Promise<{
7 | settings?: string;
8 | }>
9 | };
10 |
11 | const DashboardPage = async ({ searchParams } : DashboardPageProps) => {
12 |
13 | const { settings} = await searchParams;
14 |
15 | if (settings) {
16 | return (
17 |
18 |
19 |
20 | );
21 | }
22 |
23 | return
24 | };
25 |
26 | export default DashboardPage;
27 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/projects/[id]/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren } from "react";
2 | import ProjectSidebar from "./_components/project-sidebar";
3 | import ProjectTopBar from "./_components/project-top-bar";
4 |
5 | const DashboardLayout = ({ children }: PropsWithChildren) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 | {children}
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default DashboardLayout;
20 |
--------------------------------------------------------------------------------
/apps/web/lib/store/use-file-modal.ts:
--------------------------------------------------------------------------------
1 | import { Doc } from "@repo/backend/convex/_generated/dataModel";
2 | import { create } from "zustand";
3 |
4 | type TValue = Pick, "_id" | "title">;
5 |
6 | type TModal = {
7 | isOpen: boolean;
8 | values?: TValue;
9 | // eslint-disable-next-line no-unused-vars
10 | onOpen: (values?: TValue) => void;
11 | onClose: () => void;
12 | };
13 |
14 | export const useFileModal = create((set) => {
15 | return {
16 | isOpen: false,
17 | values: undefined,
18 | onOpen: (values) => set({ isOpen: true, values }),
19 | onClose: () => set({ isOpen: false, values: undefined }),
20 | };
21 | });
22 |
--------------------------------------------------------------------------------
/packages/backend/convex/http.ts:
--------------------------------------------------------------------------------
1 | import { httpRouter } from "convex/server";
2 | import { httpAction } from "./_generated/server";
3 |
4 | const http = httpRouter();
5 |
6 | http.route({
7 | path: "/getFile",
8 | method: "GET",
9 | handler: httpAction(async (ctx, request) => {
10 | const { searchParams } = new URL(request.url);
11 | const storageId = searchParams.get("storageId")!;
12 | const blob = await ctx.storage.get(storageId);
13 | if (blob === null) {
14 | return new Response("File not found", {
15 | status: 404,
16 | });
17 | }
18 | return new Response(blob);
19 | }),
20 | });
21 |
22 | export default http;
23 |
--------------------------------------------------------------------------------
/apps/web/lib/store/use-link-modal.ts:
--------------------------------------------------------------------------------
1 | import { Doc } from "@repo/backend/convex/_generated/dataModel";
2 | import { create } from "zustand";
3 |
4 | type TValue = Pick, "_id" | "title" | "url">;
5 |
6 | type TModal = {
7 | isOpen: boolean;
8 | values?: TValue;
9 | // eslint-disable-next-line no-unused-vars
10 | onOpen: (values?: TValue) => void;
11 | onClose: () => void;
12 | };
13 |
14 | export const useLinkModal = create((set) => {
15 | return {
16 | isOpen: false,
17 | values: undefined,
18 | onOpen: (values) => set({ isOpen: true, values }),
19 | onClose: () => set({ isOpen: false, values: undefined }),
20 | };
21 | });
22 |
--------------------------------------------------------------------------------
/apps/web/.env.example:
--------------------------------------------------------------------------------
1 | CONVEX_DEPLOYMENT=
2 | NEXT_PUBLIC_CONVEX_URL=
3 | NEXT_PUBLIC_CONVEX_DEPLOYMENT_SITE=
4 |
5 | # Clerk Auth\
6 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
7 | CLERK_SECRET_KEY=
8 | NEXT_PUBLIC_CLERK_ISSUER_BASE_URL=
9 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
10 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
11 | WEBHOOK_SECRET=
12 |
13 |
14 |
15 | UNASSIGNED_USER_ID=
16 |
17 | # Redis - For Rate Limiting
18 | UPSTASH_REDIS_REST_URL=
19 | UPSTASH_REDIS_REST_TOKEN=
20 | MAX_REQUESTS=5
21 |
22 | #Kafka
23 | KAFKA_BROKER=
24 | KAFKA_USERNAME=
25 | KAFKA_PASSWORD=
26 | KAFKA_TOPIC=
27 |
28 | # E2E CLERK TEST USER
29 | E2E_CLERK_USER_USERNAME=
30 | E2E_CLERK_USER_PASSWORD=
--------------------------------------------------------------------------------
/apps/web/lib/hooks/use-messages.ts:
--------------------------------------------------------------------------------
1 | import { api } from "@repo/backend/convex/_generated/api";
2 | import type { Id } from "@repo/backend/convex/_generated/dataModel";
3 | import { usePaginatedQuery } from "convex/react";
4 |
5 | export const useMessages = (projectId: Id<"projects">) => {
6 | const messages = usePaginatedQuery(
7 | api.message.list,
8 | { projectId },
9 | {
10 | initialNumItems: MESSAGES_PER_REQ,
11 | }
12 | );
13 |
14 | return {
15 | messages: messages.results,
16 | isLoading: messages.isLoading,
17 | fetchMore: () => messages.loadMore(MESSAGES_PER_REQ),
18 | status: messages.status,
19 | };
20 | };
21 |
22 | const MESSAGES_PER_REQ = 10;
23 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/dashboard/_component/invite-button.tsx:
--------------------------------------------------------------------------------
1 | import { OrganizationProfile } from "@clerk/nextjs";
2 | import { Plus } from "lucide-react";
3 |
4 | import ResponsiveModel from "@/components/responsive-model";
5 | import { Button } from "@/components/ui/button";
6 |
7 | const InviteButton = () => {
8 | return (
9 |
13 | Invite members
14 |
15 | }
16 | asChild
17 | >
18 |
19 |
20 | );
21 | };
22 |
23 | export default InviteButton;
24 |
--------------------------------------------------------------------------------
/apps/web/sentry.server.config.ts:
--------------------------------------------------------------------------------
1 | // This file configures the initialization of Sentry on the server.
2 | // The config you add here will be used whenever the server handles a request.
3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
4 |
5 | import * as Sentry from "@sentry/nextjs";
6 |
7 | Sentry.init({
8 | dsn: "https://f1b467610d3ffa6d3c75c4e2e90760d7@o4506822757253120.ingest.us.sentry.io/4506822758957056",
9 |
10 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
11 | tracesSampleRate: 1,
12 |
13 | // Setting this option to true will print useful information to the console while you're setting up Sentry.
14 | debug: false,
15 | });
16 |
--------------------------------------------------------------------------------
/apps/web/sentry.client.config.ts:
--------------------------------------------------------------------------------
1 | // This file configures the initialization of Sentry on the client.
2 | // The config you add here will be used whenever a users loads a page in their browser.
3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
4 |
5 | import * as Sentry from "@sentry/nextjs";
6 |
7 | Sentry.init({
8 | dsn: "https://f1b467610d3ffa6d3c75c4e2e90760d7@o4506822757253120.ingest.us.sentry.io/4506822758957056",
9 |
10 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
11 | tracesSampleRate: 1,
12 |
13 | // Setting this option to true will print useful information to the console while you're setting up Sentry.
14 | debug: false,
15 | });
16 |
--------------------------------------------------------------------------------
/apps/web/lib/store/use-feedback-modal.ts:
--------------------------------------------------------------------------------
1 | import { Doc } from "@repo/backend/convex/_generated/dataModel";
2 | import { create } from "zustand";
3 | import { OptionalProperty } from "../types";
4 |
5 | type TValue = Omit, "_id">, "_creationTime">;
6 |
7 | type TModal = {
8 | isOpen: boolean;
9 | values?: TValue;
10 | // eslint-disable-next-line no-unused-vars
11 | onOpen: (values?: TValue) => void;
12 | onClose: () => void;
13 | };
14 |
15 | export const useFeedbackModal = create((set) => {
16 | return {
17 | isOpen: false,
18 | values: undefined,
19 | onOpen: (values) => set({ isOpen: true, values }),
20 | onClose: () => set({ isOpen: false, values: undefined }),
21 | };
22 | });
23 |
--------------------------------------------------------------------------------
/apps/web/lib/store/use-changelog-modal.ts:
--------------------------------------------------------------------------------
1 | import { Doc } from "@repo/backend/convex/_generated/dataModel";
2 | import { create } from "zustand";
3 | import { OptionalProperty } from "../types";
4 |
5 | type TValue = Omit, "_id">, "_creationTime">;
6 |
7 | type TModal = {
8 | isOpen: boolean;
9 | values?: TValue;
10 | // eslint-disable-next-line no-unused-vars
11 | onOpen: (values?: TValue) => void;
12 | onClose: () => void;
13 | };
14 |
15 | export const useChangelogModal = create((set) => {
16 | return {
17 | isOpen: false,
18 | values: undefined,
19 | onOpen: (values) => set({ isOpen: true, values }),
20 | onClose: () => set({ isOpen: false, values: undefined }),
21 | };
22 | });
23 |
--------------------------------------------------------------------------------
/apps/web/lib/store/use-task-modal.ts:
--------------------------------------------------------------------------------
1 | import { Doc } from "@repo/backend/convex/_generated/dataModel";
2 | import { create } from "zustand";
3 | import { OptionalProperty } from "../types";
4 |
5 | type TValue = Omit, "_id">, "_creationTime">;
6 |
7 | type TModalProvider = {
8 | isOpen: boolean;
9 | values?: TValue;
10 | // eslint-disable-next-line no-unused-vars
11 | onOpen: (values?: TValue) => void;
12 | onClose: () => void;
13 | };
14 |
15 | export const useTaskModal = create((set) => {
16 | return {
17 | isOpen: false,
18 | values: undefined,
19 | onOpen: (values) => set({ isOpen: true, values }),
20 | onClose: () => set({ isOpen: false, values: undefined }),
21 | };
22 | });
23 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/projects/[id]/changelogs/page.tsx:
--------------------------------------------------------------------------------
1 | import ChangelogsHeader from "@/components/changelogs/changelogs-header";
2 | import { Skeleton } from "@/components/ui/skeleton";
3 | import { PagePropsWithProjectId } from "@/lib/types";
4 | import dynamic from "next/dynamic";
5 |
6 | const ChangelogList = dynamic(
7 | () => import("@/components/changelogs/changelog-list"),
8 | {
9 | loading: () => ,
10 | }
11 | );
12 |
13 | const ChangelogsPage = async ({ params }: PagePropsWithProjectId) => {
14 | const { id } = await params;
15 |
16 | return (
17 | <>
18 |
19 |
20 | >
21 | );
22 | };
23 |
24 | export default ChangelogsPage;
25 |
--------------------------------------------------------------------------------
/packages/eslint-config/library.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /** @type {import("eslint").Linter.Config} */
6 | module.exports = {
7 | extends: ["eslint:recommended", "prettier", "turbo"],
8 | plugins: ["only-warn"],
9 | globals: {
10 | React: true,
11 | JSX: true,
12 | },
13 | env: {
14 | node: true,
15 | },
16 | settings: {
17 | "import/resolver": {
18 | typescript: {
19 | project,
20 | },
21 | },
22 | },
23 | ignorePatterns: [
24 | // Ignore dotfiles
25 | ".*.js",
26 | "node_modules/",
27 | "dist/",
28 | ],
29 | overrides: [
30 | {
31 | files: ["*.js?(x)", "*.ts?(x)"],
32 | },
33 | ],
34 | };
35 |
--------------------------------------------------------------------------------
/apps/kafka-consumer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kafka-consumer",
3 | "version": "1.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "non-dev": "tsx watch --env-file=.env src/index.ts ",
7 | "build": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js --external:express --external:cors",
8 | "start": "node dist/index.js",
9 | "type-check": "tsc"
10 | },
11 | "dependencies": {
12 | "@repo/backend": "*",
13 | "convex": "^1.15.0",
14 | "cors": "^2.8.5",
15 | "esbuild": "^0.25.1",
16 | "express": "^5.1.0",
17 | "express-actuator": "^1.8.4",
18 | "kafkajs": "^2.2.4",
19 | "tsx": "^4.18.0"
20 | },
21 | "devDependencies": {
22 | "@types/cors": "^2.8.17",
23 | "@types/express": "^5.0.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/apps/web/app/global-error.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as Sentry from "@sentry/nextjs";
4 | import NextError from "next/error";
5 | import { useEffect } from "react";
6 |
7 | export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
8 | useEffect(() => {
9 | Sentry.captureException(error);
10 | }, [error]);
11 |
12 | return (
13 |
14 |
15 | {/* `NextError` is the default Next.js error page component. Its type
16 | definition requires a `statusCode` prop. However, since the App Router
17 | does not expose status codes for errors, we simply pass 0 to render a
18 | generic error message. */}
19 |
20 |
21 |
22 | );
23 | }
--------------------------------------------------------------------------------
/apps/web/components/projects/project-status.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Badge } from "@/components/ui/badge";
3 | import { cn } from "@/lib/utils";
4 |
5 | type ProjectStatusProps = {
6 | status: string;
7 | };
8 |
9 | const ProjectStatus = ({ status }: ProjectStatusProps) => {
10 | return (
11 |
19 | {status}
20 |
21 | );
22 | };
23 |
24 | export default ProjectStatus;
25 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/projects/[id]/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import type { PagePropsWithProjectId } from "@/lib/types";
2 | import dynamic from "next/dynamic";
3 | import { Skeleton } from "@/components/ui/skeleton";
4 |
5 | const ProjectDashboardClientPage = dynamic(() => import("./page-client"), {
6 | loading: () => , // Show a loading indicator
7 | });
8 |
9 | const ProjectDashboardPage = async ({ params }: PagePropsWithProjectId) => {
10 | const id = (await params).id;
11 |
12 | return (
13 |
14 |
15 | Project Dashboard
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default ProjectDashboardPage;
23 |
--------------------------------------------------------------------------------
/apps/web/components/task/task-status.tsx:
--------------------------------------------------------------------------------
1 | import { STATUSES } from "@/lib/constants";
2 | import type { TaskStatus as TTaskStatus } from "@/lib/types";
3 | import { cn } from "@/lib/utils";
4 | import { ClassNameValue } from "tailwind-merge";
5 |
6 | type TaskStatusProps = {
7 | status: TTaskStatus;
8 | className?: ClassNameValue;
9 | };
10 |
11 | const TaskStatus = ({ status, className }: TaskStatusProps) => {
12 | const currStatus = STATUSES.find((s) => s.value === status);
13 |
14 | return (
15 |
16 | {currStatus?.icon && (
17 |
18 | )}
19 | {currStatus?.label}
20 |
21 | );
22 | };
23 |
24 | export default TaskStatus;
25 |
--------------------------------------------------------------------------------
/apps/web/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<"textarea">
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | })
20 | Textarea.displayName = "Textarea"
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/apps/web/app/api/feedback/[id]/route.ts:
--------------------------------------------------------------------------------
1 | import { api } from "@repo/backend/convex/_generated/api";
2 | import { ProjectId } from "@/lib/types";
3 | import { fetchQuery } from "convex/nextjs";
4 |
5 | export async function GET(_: Request, context: { params: Promise }) {
6 | const id = (await context.params).id;
7 | try {
8 | const feedbacks = await fetchQuery(api.feedback.list, {
9 | projectId: id,
10 | });
11 |
12 | return Response.json({
13 | message: "Successfully fetched feedbacks",
14 | data: feedbacks,
15 | });
16 | } catch (e: any) {
17 | return Response.json(
18 | {
19 | message: "Failed to get feedbacks",
20 | error: "Invalid Project ID",
21 | },
22 | {
23 | status: 500,
24 | }
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | zookeeper:
3 | container_name: local-zookeeper
4 | image: wurstmeister/zookeeper:latest
5 | ports:
6 | - "2181:2181"
7 |
8 | kafka:
9 | container_name: local-kafka
10 | image: wurstmeister/kafka:latest
11 | ports:
12 | - "9092:9092"
13 | expose:
14 | - "9093"
15 | environment:
16 | KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:9093,OUTSIDE://localhost:9092
17 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT
18 | KAFKA_LISTENERS: INSIDE://0.0.0.0:9093,OUTSIDE://0.0.0.0:9092
19 | KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE
20 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
21 | KAFKA_CREATE_TOPICS: "delete-child:1:1"
22 | volumes:
23 | - /var/run/docker.sock:/var/run/docker.sock
--------------------------------------------------------------------------------
/apps/web/app/(main)/dashboard/layout.tsx:
--------------------------------------------------------------------------------
1 | import DashboardTopBar from "./_component/dashboard-top-bar";
2 | import DashboardSidebar from "./_component/sidebar";
3 | import OrganizationsSideBar from "./_component/sidebar/orgs-sidebar";
4 |
5 | const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {children}
15 |
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default DashboardLayout;
23 |
--------------------------------------------------------------------------------
/packages/eslint-config/next.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /** @type {import("eslint").Linter.Config} */
6 | module.exports = {
7 | extends: [
8 | "eslint:recommended",
9 | "prettier",
10 | require.resolve("@vercel/style-guide/eslint/next"),
11 | "turbo",
12 | ],
13 | globals: {
14 | React: true,
15 | JSX: true,
16 | },
17 | env: {
18 | node: true,
19 | browser: true,
20 | },
21 | plugins: ["only-warn"],
22 | settings: {
23 | "import/resolver": {
24 | typescript: {
25 | project,
26 | },
27 | },
28 | },
29 | ignorePatterns: [
30 | // Ignore dotfiles
31 | ".*.js",
32 | "node_modules/",
33 | ],
34 | overrides: [{ files: ["*.js?(x)", "*.ts?(x)"] }],
35 | };
36 |
--------------------------------------------------------------------------------
/apps/web/components/task/task-priority.tsx:
--------------------------------------------------------------------------------
1 | import { PRIORITIES } from "@/lib/constants";
2 | import type { TaskPriority as TTaskPriority } from "@/lib/types";
3 | import { cn } from "@/lib/utils";
4 | import { ClassNameValue } from "tailwind-merge";
5 |
6 | type TaskPriorityProps = {
7 | priority: TTaskPriority;
8 | className?: ClassNameValue;
9 | };
10 |
11 | const TaskPriority = ({ priority, className }: TaskPriorityProps) => {
12 | const currPriority = PRIORITIES.find((p) => p.value === priority);
13 |
14 | return (
15 |
16 | {currPriority?.icon && (
17 |
18 | )}
19 | {currPriority?.label}
20 |
21 | );
22 | };
23 |
24 | export default TaskPriority;
25 |
--------------------------------------------------------------------------------
/apps/web/components/theme/theme-switch.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Moon, Sun } from "lucide-react";
4 | import { useTheme } from "next-themes";
5 |
6 | import { Button } from "@/components/ui/button";
7 |
8 | export function ThemeSwitch() {
9 | const { theme, setTheme } = useTheme();
10 |
11 | const handleToggle = () => {
12 | setTheme(theme === "dark" ? "light" : "dark");
13 | };
14 |
15 | return (
16 |
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/apps/web/lib/hooks/use-api-mutation.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useMutation } from "convex/react";
4 | import { FunctionReference, OptionalRestArgs } from "convex/server";
5 | import { useState } from "react";
6 |
7 | export default function useApiMutation<
8 | Mutation extends FunctionReference<"mutation">,
9 | >(mutateFunction: Mutation) {
10 | const [isPending, setIsPending] = useState(false);
11 | const apiMutation = useMutation(mutateFunction);
12 |
13 | const mutate = async (...payload: OptionalRestArgs) => {
14 | setIsPending(true);
15 | return apiMutation(...payload)
16 | .then((res) => res)
17 | .catch((err) => {
18 | throw err;
19 | })
20 | .finally(() => {
21 | setIsPending(false);
22 | });
23 | };
24 |
25 | return { isPending, mutate };
26 | }
27 |
--------------------------------------------------------------------------------
/apps/web/components/resources/file/file-icon.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | File,
3 | FileArchive,
4 | FileAudio,
5 | FileImage,
6 | FileText,
7 | FileVideo
8 | } from "lucide-react";
9 |
10 | type FileIconProps = {
11 | type: string;
12 | };
13 |
14 | const FileIcon = ({ type }: FileIconProps) => {
15 | const className = "w-8 h-8";
16 |
17 | if (type.includes("image")) return ;
18 |
19 | if (type.includes("pdf")) return ;
20 |
21 | if (type.includes("video")) return ;
22 |
23 | if (type.includes("audio")) return ;
24 |
25 | if (type.includes("zip")) return ;
26 |
27 | return ;
28 | };
29 |
30 | export default FileIcon;
31 |
--------------------------------------------------------------------------------
/apps/web/app/api/changelog/[id]/route.ts:
--------------------------------------------------------------------------------
1 | import { api } from "@repo/backend/convex/_generated/api";
2 | import { ProjectId } from "@/lib/types";
3 | import { fetchQuery } from "convex/nextjs";
4 |
5 | export async function GET(_: Request, context: { params: Promise }) {
6 | const id = (await context.params).id;
7 |
8 | try {
9 | const changelogs = await fetchQuery(api.changelog.list, {
10 | projectId: id,
11 | showPublished: true,
12 | });
13 |
14 | return Response.json({
15 | message: "Successfully fetched changelogs",
16 | data: changelogs,
17 | });
18 | } catch (e: any) {
19 | return Response.json(
20 | {
21 | message: "Failed to get changelogs",
22 | error: "Invalid Project ID",
23 | },
24 | {
25 | status: 500,
26 | }
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/apps/web/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "ui": "tui",
4 | "globalEnv": [
5 | "WEBHOOK_SECRET",
6 | "NEXT_PUBLIC_CONVEX_URL",
7 | "NEXT_PUBLIC_CONVEX_DEPLOYMENT_SITE",
8 | "UNASSIGNED_USER_ID",
9 | "MAX_REQUESTS",
10 | "NEXT_RUNTIME",
11 | "NEXT_PUBLIC_SENTRY_DSN",
12 | "KAFKA_BROKER",
13 | "KAFKA_USERNAME",
14 | "KAFKA_PASSWORD",
15 | "CI"
16 | ],
17 | "tasks": {
18 | "build": {
19 | "dependsOn": ["^build"],
20 | "inputs": ["$TURBO_DEFAULT$", ".env*"],
21 | "outputs": [".next/**", "!.next/cache/**" , "dist/**"],
22 | "env": ["SENTRY_AUTH_TOKEN"]
23 | },
24 | "lint": {
25 | "dependsOn": ["^lint"]
26 | },
27 | "dev": {
28 | "cache": false,
29 | "persistent": true,
30 | "env": ["KAFKA_TOPIC"]
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/projects/[id]/error.tsx:
--------------------------------------------------------------------------------
1 | "use client"; // Error components must be Client Components
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { DASHBOARD_ROUTE } from "@/lib/constants";
5 | import Image from "next/image";
6 | import { useRouter } from "next/navigation";
7 |
8 | export default function Error() {
9 | const router = useRouter();
10 | return (
11 |
12 |
13 |
Oops, Something went wrong!
14 | router.push(DASHBOARD_ROUTE)
18 | }
19 | >
20 | Back to Dashboard
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/dashboard/_component/sidebar/orgs-sidebar/org-list.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useOrganizationList } from "@clerk/nextjs";
4 |
5 | import OrganizationItem from "./org-item";
6 |
7 | const OrganizationList = () => {
8 | const { userMemberships } = useOrganizationList({
9 | userMemberships: {
10 | infinite: true,
11 | },
12 | });
13 |
14 | if (!userMemberships.data?.length) return null;
15 |
16 | return (
17 |
18 | {userMemberships.data.map((membership) => (
19 |
25 | ))}
26 |
27 | );
28 | };
29 |
30 | export default OrganizationList;
31 |
--------------------------------------------------------------------------------
/apps/web/components/landing-page/mobile-nav.tsx:
--------------------------------------------------------------------------------
1 | import { SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
2 | import Link from "next/link";
3 | import { navLinks } from "./navbar";
4 |
5 | const MobileNav = () => {
6 | return (
7 |
8 |
9 | Projectify
10 |
11 |
12 | {navLinks.map((link) => (
13 |
19 | {link.title}
20 |
21 | ))}
22 |
23 |
24 | );
25 | };
26 |
27 | export default MobileNav;
28 |
--------------------------------------------------------------------------------
/apps/web/lib/kafka.ts:
--------------------------------------------------------------------------------
1 | import { KafkaMessage } from "@repo/backend/lib/types";
2 | import { Kafka, logLevel } from "kafkajs";
3 |
4 | const kafka = new Kafka({
5 | brokers: [process.env.KAFKA_BROKER!],
6 | connectionTimeout: 10000,
7 | ssl: true,
8 | sasl: {
9 | mechanism: "scram-sha-256",
10 | username: process.env.KAFKA_USERNAME!,
11 | password: process.env.KAFKA_PASSWORD!,
12 | },
13 | logLevel: logLevel.ERROR,
14 | }).producer();
15 |
16 | export const produceMessage = async (message: KafkaMessage) => {
17 | const topic = process.env.KAFKA_TOPIC || "delete-child";
18 | try {
19 | await kafka.connect();
20 | await kafka.send({
21 | topic,
22 | messages: [{ value: JSON.stringify(message) }],
23 | });
24 | } catch (error) {
25 | console.error(error);
26 | } finally {
27 | await kafka.disconnect();
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/apps/web/sentry.edge.config.ts:
--------------------------------------------------------------------------------
1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
2 | // The config you add here will be used whenever one of the edge features is loaded.
3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
5 |
6 | import * as Sentry from "@sentry/nextjs";
7 |
8 | Sentry.init({
9 | dsn: "https://f1b467610d3ffa6d3c75c4e2e90760d7@o4506822757253120.ingest.us.sentry.io/4506822758957056",
10 |
11 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
12 | tracesSampleRate: 1,
13 |
14 | // Setting this option to true will print useful information to the console while you're setting up Sentry.
15 | debug: false,
16 | });
17 |
--------------------------------------------------------------------------------
/apps/web/components/feedback/feedback-type.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "@/components/ui/badge";
2 | import type { FeedbackType as TFeedbackType } from "@/lib/types";
3 | import { cn } from "@/lib/utils";
4 |
5 | const FeedbackType = ({ type }: { type: TFeedbackType | undefined }) => {
6 | if (!type) return null;
7 |
8 | return (
9 |
19 | {type}
20 |
21 | );
22 | };
23 |
24 | export default FeedbackType;
25 |
--------------------------------------------------------------------------------
/apps/web/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | }
19 | )
20 | Input.displayName = "Input"
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/packages/backend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | /* This TypeScript project config describes the environment that
3 | * Convex functions run in and is used to typecheck them.
4 | * You can modify it, but some settings required to use Convex.
5 | */
6 | "compilerOptions": {
7 | /* These settings are not required by Convex and can be modified. */
8 | "allowJs": true,
9 | "strict": true,
10 |
11 | /* These compiler options are required by Convex */
12 | "target": "ESNext",
13 | "lib": ["ES2021", "dom"],
14 | "forceConsistentCasingInFileNames": true,
15 | "allowSyntheticDefaultImports": true,
16 | "module": "ESNext",
17 | "moduleResolution": "Bundler",
18 | "isolatedModules": true,
19 | "skipLibCheck": true,
20 | "noEmit": true,
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["./**/*"],
26 | "exclude": ["./_generated"]
27 | }
--------------------------------------------------------------------------------
/apps/web/components/feedback/feedback-status.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "@/components/ui/badge";
2 | import type { FeedbackStatus as TFeedbackStatus } from "@/lib/types";
3 | import { cn } from "@/lib/utils";
4 |
5 | const FeedbackStatus = ({
6 | status,
7 | }: {
8 | status: TFeedbackStatus | undefined;
9 | }) => {
10 | if (!status) return null;
11 |
12 | return (
13 |
23 | {status}
24 |
25 | );
26 | };
27 |
28 | export default FeedbackStatus;
29 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/projects/[id]/messages/page.tsx:
--------------------------------------------------------------------------------
1 | import MessageInput from "@/components/messages/message-input";
2 | import { Skeleton } from "@/components/ui/skeleton";
3 | import type { PagePropsWithProjectId } from "@/lib/types";
4 | import { lazy, Suspense } from "react";
5 |
6 | const MessagesList = lazy(() => import("@/components/messages/messages-list"));
7 |
8 | const MessagesPage = async ({ params }: PagePropsWithProjectId) => {
9 | const { id } = await params;
10 | return (
11 |
12 |
13 | Project Chat
14 |
15 | }>
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default MessagesPage;
24 |
--------------------------------------------------------------------------------
/apps/web/lib/ratelimit.ts:
--------------------------------------------------------------------------------
1 | import { Ratelimit } from "@upstash/ratelimit";
2 | import { Redis } from "@upstash/redis";
3 | import { NextRequest, NextResponse } from "next/server";
4 |
5 | const ratelimit = new Ratelimit({
6 | redis: Redis.fromEnv(),
7 | limiter: Ratelimit.slidingWindow(
8 | Number(process.env.MAX_REQUESTS) ?? 5,
9 | "30 s"
10 | ),
11 | });
12 |
13 | export async function ApiRateLimit(request: NextRequest) {
14 | if (!request.url.includes("/api")) {
15 | return NextResponse.next();
16 | }
17 |
18 | // const ip = request.ip ?? "127.0.0.1";
19 | const { success } = await ratelimit.limit("ip");
20 |
21 | return success
22 | ? NextResponse.next()
23 | : NextResponse.json(
24 | {
25 | message: "Rate limit exceeded",
26 | error: "Too many requests",
27 | },
28 | {
29 | status: 429,
30 | }
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/apps/web/components/resources/resource-list.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { api } from "@repo/backend/convex/_generated/api";
4 | import type { Id } from "@repo/backend/convex/_generated/dataModel";
5 | import { useQuery } from "convex/react";
6 | import { useParams } from "next/navigation";
7 | import FileList from "./file/file-list";
8 | import LinkList from "./link/link-list";
9 |
10 | const ResourceList = () => {
11 | const param = useParams();
12 |
13 | const links = useQuery(api.resources.link.list, {
14 | projectId: param.id as Id<"projects">,
15 | });
16 |
17 | const files = useQuery(api.resources.file.list, {
18 | projectId: param.id as Id<"projects">,
19 | });
20 |
21 | return (
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default ResourceList;
30 |
--------------------------------------------------------------------------------
/apps/web/components/providers/auth-load-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { AuthLoading, Authenticated } from "convex/react";
4 | import Image from "next/image";
5 | import type { PropsWithChildren } from "react";
6 |
7 | const AuthLoadProvider = ({ children }: PropsWithChildren) => {
8 | return (
9 |
10 |
11 |
12 |
13 |
{children}
14 |
15 | );
16 | };
17 |
18 | export default AuthLoadProvider;
19 |
20 |
21 | const Loading = () => {
22 | return (
23 |
24 |
32 |
33 | );
34 | };
--------------------------------------------------------------------------------
/apps/web/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/apps/web/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ))
26 | Progress.displayName = ProgressPrimitive.Root.displayName
27 |
28 | export { Progress }
29 |
--------------------------------------------------------------------------------
/.github/actions/ci-setup/action.yml:
--------------------------------------------------------------------------------
1 | name: "Setup Continuous Integration"
2 | description: "Cache Dependencies"
3 | runs:
4 | using: "composite"
5 | steps:
6 | - name: Setup Node.js
7 | uses: actions/setup-node@v4
8 | with:
9 | node-version: ${{ env.NODE_VERSION }}
10 | cache: "yarn"
11 |
12 | - name: Get yarn cache directory path
13 | id: yarn-cache-dir-path
14 | run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
15 | shell: bash
16 |
17 | - uses: actions/cache@v4
18 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
19 | with:
20 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
21 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
22 | restore-keys: |
23 | ${{ runner.os }}-yarn-
24 |
25 | - name: Install Dependencies
26 | run: yarn install
27 | shell: bash
--------------------------------------------------------------------------------
/apps/web/components/task/task-title.tsx:
--------------------------------------------------------------------------------
1 | import { LABELS } from "@/lib/constants";
2 | import React from "react";
3 | import { Badge } from "@/components/ui/badge";
4 | import type { TaskType } from "@/lib/types";
5 | import { ClassNameValue } from "tailwind-merge";
6 | import { cn } from "@/lib/utils";
7 |
8 | type TaskTitleProps = {
9 | type: TaskType;
10 | title: string;
11 | className?: ClassNameValue;
12 | };
13 |
14 | const TaskTitle = ({ title, type, className }: TaskTitleProps) => {
15 | const label = LABELS.find((label) => label.value === type);
16 |
17 | return (
18 |
19 | {label && (
20 |
21 | {label.label}
22 |
23 | )}
24 | {title}
25 |
26 | );
27 | };
28 |
29 | export default TaskTitle;
30 |
--------------------------------------------------------------------------------
/apps/web/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useToast } from "@/hooks/use-toast"
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "@/components/ui/toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title} }
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/packages/eslint-config/react-internal.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /*
6 | * This is a custom ESLint configuration for use with
7 | * internal (bundled by their consumer) libraries
8 | * that utilize React.
9 | */
10 |
11 | /** @type {import("eslint").Linter.Config} */
12 | module.exports = {
13 | extends: ["eslint:recommended", "prettier", "turbo"],
14 | plugins: ["only-warn"],
15 | globals: {
16 | React: true,
17 | JSX: true,
18 | },
19 | env: {
20 | browser: true,
21 | },
22 | settings: {
23 | "import/resolver": {
24 | typescript: {
25 | project,
26 | },
27 | },
28 | },
29 | ignorePatterns: [
30 | // Ignore dotfiles
31 | ".*.js",
32 | "node_modules/",
33 | "dist/",
34 | ],
35 | overrides: [
36 | // Force ESLint to detect .tsx files
37 | { files: ["*.js?(x)", "*.ts?(x)"] },
38 | ],
39 | };
40 |
--------------------------------------------------------------------------------
/.github/workflows/pr-checks.yml:
--------------------------------------------------------------------------------
1 | name: Pull Request Checks
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize]
6 |
7 | env:
8 | NODE_VERSION: 20.11.0
9 |
10 | jobs:
11 | linting:
12 | name: Lint
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v4
17 |
18 | - name: Setup Continuous integration
19 | uses: ./.github/actions/ci-setup
20 |
21 | - name: Lint Application
22 | run: yarn lint
23 |
24 | build:
25 | name: Build
26 | runs-on: ubuntu-latest
27 | steps:
28 | - name: Checkout
29 | uses: actions/checkout@v4
30 |
31 | - name: Setup Continuous Integration
32 | uses: ./.github/actions/ci-setup
33 |
34 | - name: Build Application
35 | env:
36 | NEXT_PUBLIC_CONVEX_URL: ${{ secrets.NEXT_PUBLIC_CONVEX_URL }}
37 | CONVEX_DEPLOYMENT: ${{ secrets.CONVEX_DEPLOYMENT }}
38 | run: yarn build
--------------------------------------------------------------------------------
/apps/web/lib/types.ts:
--------------------------------------------------------------------------------
1 | import type { Id } from "@repo/backend/convex/_generated/dataModel";
2 | import { PROJECTS_STAGES } from "./constants";
3 |
4 | export type ProjectId = {
5 | id: Id<"projects">;
6 | };
7 |
8 | export type PagePropsWithProjectId = {
9 | params: Promise;
10 | };
11 |
12 | export type OptionalProperty = Omit & {
13 | [P in K]?: T[P];
14 | };
15 |
16 | export type TaskStatus =
17 | | "backlog"
18 | | "todo"
19 | | "in-progress"
20 | | "done"
21 | | "canceled";
22 |
23 | export type TaskPriority = "low" | "medium" | "high";
24 |
25 | export type TaskType = "documentation" | "bug" | "feature";
26 |
27 | export type ProjectStatus = (typeof PROJECTS_STAGES)[number];
28 |
29 | export type ResourceType = "link" | "file";
30 |
31 | export type FeedbackType =
32 | | "documentation"
33 | | "feature"
34 | | "issue"
35 | | "question"
36 | | "idea"
37 | | "other";
38 |
39 | export type FeedbackStatus = "open" | "reviewed" | "closed";
40 |
--------------------------------------------------------------------------------
/apps/web/components/empty-states/no-org.tsx:
--------------------------------------------------------------------------------
1 | import { CreateOrganization } from "@clerk/nextjs";
2 | import Image from "next/image";
3 |
4 | import { Button } from "@/components/ui/button";
5 | import ResponsiveModel from "../responsive-model";
6 |
7 | const NoOrg = () => {
8 | return (
9 |
10 |
11 |
12 |
Welcome to Board
13 |
14 | Create an organization to get started
15 |
16 |
17 | Create organization}
19 | asChild
20 | className="p-0 bg-transparent border-none max-w-[510px]"
21 | >
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default NoOrg;
30 |
--------------------------------------------------------------------------------
/apps/web/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/packages/backend/convex/types.ts:
--------------------------------------------------------------------------------
1 | import { v } from "convex/values";
2 |
3 | export const ProjectStatus = v.union(
4 | v.literal("development"),
5 | v.literal("live"),
6 | v.literal("stale"),
7 | v.literal("archived")
8 | );
9 |
10 | export const TaskStatus = v.union(
11 | v.literal("backlog"),
12 | v.literal("todo"),
13 | v.literal("in-progress"),
14 | v.literal("done"),
15 | v.literal("canceled")
16 | );
17 |
18 | export const TaskPriority = v.union(
19 | v.literal("low"),
20 | v.literal("medium"),
21 | v.literal("high")
22 | );
23 |
24 | export const TaskType = v.union(
25 | v.literal("documentation"),
26 | v.literal("bug"),
27 | v.literal("feature")
28 | );
29 |
30 | export const FeedbackStatus = v.union(
31 | v.literal("open"),
32 | v.literal("reviewed"),
33 | v.literal("closed")
34 | );
35 |
36 | export const FeedbackType = v.union(
37 | v.literal("issue"),
38 | v.literal("feature"),
39 | v.literal("documentation"),
40 | v.literal("question"),
41 | v.literal("idea"),
42 | v.literal("other")
43 | );
44 |
--------------------------------------------------------------------------------
/apps/web/components/feedback/feedback-header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import FeedbackIntegration from "@/components/feedback/feedback-integration";
4 | import { Button } from "@/components/ui/button";
5 | import { useFeedbackModal } from "@/lib/store/use-feedback-modal";
6 | import { FileText, Plus } from "lucide-react";
7 |
8 | const FeedbacksPageHeader = () => {
9 | const { onOpen } = useFeedbackModal();
10 |
11 | return (
12 |
13 |
Feedbacks
14 |
15 |
16 |
onOpen()}>
17 | Add Feedback
18 |
19 |
20 |
21 |
22 | Feedback Integration
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default FeedbacksPageHeader;
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Vignesh gupta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/apps/web/components/hint.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import {
4 | Tooltip,
5 | TooltipContent,
6 | TooltipProvider,
7 | TooltipTrigger,
8 | } from "@/components/ui/tooltip";
9 |
10 | type HintProps = {
11 | label: string;
12 | children: React.ReactNode;
13 | side?: "left" | "right" | "top" | "bottom";
14 | align?: "start" | "center" | "end";
15 | sideOffset?: number;
16 | alignOffset?: number;
17 | };
18 |
19 | const Hint = ({
20 | align,
21 | children,
22 | label,
23 | side,
24 | alignOffset,
25 | sideOffset,
26 | }: HintProps) => {
27 | return (
28 |
29 |
30 | {children}
31 |
38 | {label}
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default Hint;
46 |
--------------------------------------------------------------------------------
/apps/web/components/providers/convex-client-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ClerkProvider, useAuth } from "@clerk/nextjs";
4 | import { dark } from "@clerk/themes";
5 | import { ConvexReactClient } from "convex/react";
6 | import { ConvexProviderWithClerk } from "convex/react-clerk";
7 | import { useTheme } from "next-themes";
8 | import { useEffect, useState, type PropsWithChildren } from "react";
9 |
10 | const convexURL = process.env.NEXT_PUBLIC_CONVEX_URL!;
11 |
12 | const convex = new ConvexReactClient(convexURL);
13 |
14 | const ConvexClientProvider = ({ children }: PropsWithChildren) => {
15 | const { theme } = useTheme();
16 | const [isClient, setIsClient] = useState(false)
17 |
18 | useEffect(() => {
19 | setIsClient(true)
20 | }, [])
21 |
22 |
23 | if (!isClient) return null
24 |
25 | return (
26 |
29 |
30 | {children}
31 |
32 |
33 | );
34 | };
35 |
36 | export default ConvexClientProvider;
37 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/projects/[id]/dashboard/page-client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { api } from "@repo/backend/convex/_generated/api";
4 | import { useCurrentUser } from "@/lib/hooks/use-current-user";
5 | import type { ProjectId } from "@/lib/types";
6 | import { useQuery } from "convex/react";
7 | import { Loader2 } from "lucide-react";
8 | import OwnedTaskTable from "../_components/owned-task-table";
9 |
10 | const ProjectDashboardClientPage = ({ id }: ProjectId) => {
11 | const currentUser = useCurrentUser();
12 |
13 | const myTasks = useQuery(api.work_item.list, {
14 | projectId: id,
15 | assigneeId: currentUser?._id,
16 | ignoreCompleted: true,
17 | });
18 |
19 | return (
20 |
21 | Your task list
22 | {myTasks ? (
23 |
24 | ) : (
25 |
26 |
27 |
28 | )}
29 |
30 | );
31 | };
32 |
33 | export default ProjectDashboardClientPage;
34 |
--------------------------------------------------------------------------------
/packages/backend/convex/resources/storage.ts:
--------------------------------------------------------------------------------
1 | import { api } from "../_generated/api";
2 | import { action, mutation } from "../_generated/server";
3 | import { RegisteredMutation } from "convex/server";
4 | import { v } from "convex/values";
5 | import { EmptyObject } from "react-hook-form";
6 |
7 | export const generateUploadUrl: RegisteredMutation<"public", EmptyObject, Promise>
8 | = mutation((ctx) => {
9 | return ctx.storage.generateUploadUrl();
10 | });
11 |
12 | export const saveFavicon = action({
13 | args: {
14 | id: v.id("links"),
15 | url: v.string(),
16 | },
17 | handler: async (ctx, args) => {
18 | const iconReq = await fetch(
19 | `https://icons.duckduckgo.com/ip3/${new URL(args.url).hostname}.ico`
20 | );
21 |
22 | console.log({ iconReq });
23 |
24 | if (!iconReq.ok) return;
25 |
26 | const icon = await iconReq.blob();
27 |
28 | console.log({ icon });
29 |
30 | const storageRes = await ctx.storage.store(icon);
31 |
32 | console.log({ storageRes });
33 |
34 | await ctx.runMutation(api.resources.link.update, {
35 | _id: args.id,
36 | icon: storageRes,
37 | });
38 |
39 | console.log("done");
40 | },
41 | });
42 |
--------------------------------------------------------------------------------
/apps/web/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/apps/web/app/api/webhooks/clerk/utils/organization.ts:
--------------------------------------------------------------------------------
1 | import { api } from "@repo/backend/convex/_generated/api";
2 | import { DeletedObjectJSON, OrganizationJSON } from "@clerk/nextjs/server";
3 | import { fetchMutation } from "convex/nextjs";
4 |
5 | type Organization = {
6 | clerkId: string;
7 | name: string;
8 | imageUrl: string;
9 | createdBy: string;
10 | };
11 |
12 | export const createOrg = async (orgJson: OrganizationJSON) => {
13 | const org = mapOrgJsonToOrg(orgJson);
14 |
15 | await fetchMutation(api.team.create, org);
16 | };
17 |
18 | export const updateOrg = async (orgJson: OrganizationJSON) => {
19 | const org = mapOrgJsonToOrg(orgJson);
20 |
21 | await fetchMutation(api.team.update, {
22 | imageUrl: org.imageUrl,
23 | name: org.name,
24 | clerkId: org.clerkId,
25 | });
26 | };
27 |
28 | export const deleteOrg = async (orgJson: DeletedObjectJSON) => {
29 | await fetchMutation(api.team.remove, { clerkId: orgJson.id || "" });
30 | };
31 |
32 | function mapOrgJsonToOrg(orgJson: OrganizationJSON): Organization {
33 | return {
34 | clerkId: orgJson.id,
35 | name: orgJson.name,
36 | imageUrl: orgJson.image_url || "",
37 | createdBy: orgJson.created_by || "",
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/dashboard/_component/sidebar/orgs-sidebar/new-org-button.tsx:
--------------------------------------------------------------------------------
1 | import { CreateOrganization } from "@clerk/nextjs";
2 | import { Plus } from "lucide-react";
3 |
4 | import Hint from "@/components/hint";
5 | import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
6 | import { DASHBOARD_ROUTE } from "@/lib/constants";
7 |
8 | const NewButton = () => {
9 | return (
10 |
11 |
12 |
13 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default NewButton;
36 |
--------------------------------------------------------------------------------
/apps/web/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/apps/web/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/apps/web/app/api/webhooks/clerk/utils/team-membership.ts:
--------------------------------------------------------------------------------
1 | import { api } from "@repo/backend/convex/_generated/api";
2 | import { OrganizationMembershipJSON } from "@clerk/nextjs/server";
3 | import { fetchMutation } from "convex/nextjs";
4 |
5 | type OrganizationMembership = {
6 | orgId: string;
7 | userId: string;
8 | userRole: string;
9 | };
10 |
11 | export const addMember = async (memJSON: OrganizationMembershipJSON) => {
12 | const mem = mapMemJsonToMem(memJSON);
13 |
14 | await fetchMutation(api.team_membership.addMember, mem);
15 | };
16 |
17 | export const updateMemberRole = async (memJSON: OrganizationMembershipJSON) => {
18 | const mem = mapMemJsonToMem(memJSON);
19 |
20 | await fetchMutation(api.team_membership.updateMemberRole, mem);
21 | };
22 |
23 | export const removeMember = async (memJSON: OrganizationMembershipJSON) => {
24 | const mem = mapMemJsonToMem(memJSON);
25 |
26 | await fetchMutation(api.team_membership.removeMember, {
27 | orgId: mem.orgId,
28 | userId: mem.userId,
29 | });
30 | };
31 |
32 | function mapMemJsonToMem(
33 | memJson: OrganizationMembershipJSON
34 | ): OrganizationMembership {
35 | return {
36 | orgId: memJson.organization.id,
37 | userId: memJson.public_user_data.user_id,
38 | userRole: memJson.role,
39 | };
40 | }
41 |
--------------------------------------------------------------------------------
/apps/web/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/apps/web/app/api/webhooks/clerk/utils/user.ts:
--------------------------------------------------------------------------------
1 | import { api } from "@repo/backend/convex/_generated/api";
2 | import { DeletedObjectJSON, UserJSON } from "@clerk/nextjs/server";
3 | import { fetchMutation } from "convex/nextjs";
4 |
5 | type User = {
6 | clerkId: string;
7 | email: string;
8 | firstName: string;
9 | imageUrl: string;
10 | };
11 |
12 | export const createUser = async (userJson: UserJSON) => {
13 | const user = mapUserJsonToUser(userJson);
14 | await fetchMutation(api.user.create, user);
15 | };
16 |
17 | export const updateUser = async (userJson: UserJSON) => {
18 | const user = mapUserJsonToUser(userJson);
19 | await fetchMutation(api.user.update, user);
20 | };
21 |
22 | export const deleteUser = async (userJson: DeletedObjectJSON) => {
23 | const clerkId = userJson.id;
24 | if (!clerkId) return;
25 |
26 | await fetchMutation(api.user.remove, { clerkId });
27 | };
28 |
29 | function mapUserJsonToUser(userJson: UserJSON): User {
30 | return {
31 | clerkId: userJson.id,
32 | email:
33 | userJson.email_addresses.find(
34 | (email) => email.id === userJson.primary_email_address_id
35 | )?.email_address || `${userJson.id}@nothanks.com`,
36 | firstName: userJson.first_name ?? userJson.id,
37 | imageUrl: userJson.image_url,
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/dashboard/_component/sidebar/orgs-sidebar/org-item.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useOrganization, useOrganizationList } from "@clerk/nextjs";
3 | import Image from "next/image";
4 |
5 | import Hint from "@/components/hint";
6 | import { cn } from "@/lib/utils";
7 |
8 | type OrganizationItemProps = {
9 | id: string;
10 | name: string;
11 | imageUrl: string;
12 | };
13 |
14 | const OrganizationItem = ({ id, imageUrl, name }: OrganizationItemProps) => {
15 | const { organization } = useOrganization();
16 | const { setActive } = useOrganizationList();
17 |
18 | const isActive = organization?.id === id;
19 |
20 | const onClick = () => {
21 | if (!setActive) return;
22 |
23 | setActive({ organization: id });
24 | };
25 |
26 | return (
27 |
28 |
29 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default OrganizationItem;
46 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/projects/[id]/work-items/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { columns } from "@/components/work-items/columns";
5 | import { DataTable } from "@/components/work-items/data-table";
6 | import { api } from "@repo/backend/convex/_generated/api";
7 | import { useTaskModal } from "@/lib/store/use-task-modal";
8 | import type { ProjectId } from "@/lib/types";
9 | import { useQuery } from "convex/react";
10 | import { useParams } from "next/navigation";
11 |
12 | const WorkItemsPage = () => {
13 | const { id } = useParams()
14 |
15 | const tasks = useQuery(api.work_item.list, { projectId: id });
16 |
17 | const { onOpen } = useTaskModal();
18 |
19 | if (!tasks) return Loading...
;
20 |
21 | return (
22 |
23 |
24 |
25 | Work Items
26 |
27 | onOpen()}>
28 | Add Item
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default WorkItemsPage;
39 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/dashboard/settings/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Separator } from "@/components/ui/separator";
4 | import { cn } from "@/lib/utils";
5 | import Link from "next/link";
6 | import { usePathname } from "next/navigation";
7 | import type { PropsWithChildren } from "react";
8 |
9 | const AccountSettingsLayout = ({ children }: PropsWithChildren) => {
10 | const url = usePathname();
11 | const isOrgSettings = url.includes("/settings/organization-settings");
12 |
13 | const settingUrl = "/dashboard/settings";
14 |
15 | return (
16 |
17 |
18 |
23 | Account Settings
24 |
25 |
30 |
31 | Organization settings
32 |
33 |
34 |
35 |
36 | {children}
37 |
38 | );
39 | };
40 |
41 | export default AccountSettingsLayout;
42 |
--------------------------------------------------------------------------------
/apps/web/components/landing-page/footer.tsx:
--------------------------------------------------------------------------------
1 | import { Star } from "lucide-react";
2 | import Link from "next/link";
3 |
4 | const Footer = () => {
5 | return (
6 |
36 | );
37 | };
38 |
39 | export default Footer;
40 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/projects/[id]/_components/project-sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Poppins } from "next/font/google";
4 | import Link from "next/link";
5 | import { useParams, usePathname } from "next/navigation";
6 |
7 | import { Button } from "@/components/ui/button";
8 | import { cn, getNavLinks } from "@/lib/utils";
9 |
10 | const poppinsFont = Poppins({
11 | weight: ["600"],
12 | subsets: ["latin"],
13 | });
14 |
15 | const ProjectSidebar = () => {
16 | const param = useParams();
17 | const pathname = usePathname();
18 |
19 | const navLinks = getNavLinks(param.id as string);
20 |
21 | return (
22 |
28 |
29 | {navLinks.map(({ href, Icon, name }) => (
30 |
36 |
37 | {name}
38 |
39 |
40 | ))}
41 |
42 |
43 | );
44 | };
45 |
46 | export default ProjectSidebar;
47 |
--------------------------------------------------------------------------------
/apps/web/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/apps/web/components/ui/date-picker.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | "use client";
3 |
4 | import { format } from "date-fns";
5 | import { Calendar as CalendarIcon } from "lucide-react";
6 |
7 | import { Button } from "@/components/ui/button";
8 | import { Calendar } from "@/components/ui/calendar";
9 | import {
10 | Popover,
11 | PopoverContent,
12 | PopoverTrigger,
13 | } from "@/components/ui/popover";
14 | import { cn } from "@/lib/utils";
15 |
16 | type DatePickerProps = {
17 | value?: Date;
18 | onSelect: (date: Date | undefined) => void;
19 | };
20 |
21 | export function DatePicker({ value, onSelect }: DatePickerProps) {
22 |
23 | return (
24 |
25 |
26 |
33 |
34 | {value ? format(value, "PPP") : Pick a date }
35 |
36 |
37 |
38 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/apps/web/components/feedback/feedback-card.tsx:
--------------------------------------------------------------------------------
1 | import { Doc } from "@repo/backend/convex/_generated/dataModel";
2 | import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
3 | import FeedbackActions from "./feedback-actions";
4 | import FeedbackStatus from "./feedback-status";
5 | import FeedbackType from "./feedback-type";
6 |
7 | type FeedbackCardProps = {
8 | feedback: Doc<"feedbacks"> | undefined;
9 | };
10 |
11 | const FeedbackCard = ({ feedback }: FeedbackCardProps) => {
12 | return (
13 |
14 |
15 |
16 |
{feedback?.senderName}
17 |
18 | {feedback?.senderEmail}
19 |
20 |
21 |
22 |
23 |
24 |
25 | {feedback?.content}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default FeedbackCard;
37 |
--------------------------------------------------------------------------------
/apps/web/components/changelogs/changelog-list.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { api } from "@repo/backend/convex/_generated/api";
4 | import { useChangelogModal } from "@/lib/store/use-changelog-modal";
5 | import { useQuery } from "convex/react";
6 | import { Button } from "../ui/button";
7 | import ChangelogCard from "./changelog-card";
8 | import { ScrollArea } from "../ui/scroll-area";
9 | import { ProjectId } from "@/lib/types";
10 |
11 | const ChangelogList = ({ id }: ProjectId) => {
12 | const changelogs = useQuery(api.changelog.list, { projectId: id });
13 |
14 | if (!changelogs) {
15 | return <>Loading...>;
16 | }
17 |
18 | if (changelogs.length === 0) {
19 | return ;
20 | }
21 |
22 | return (
23 |
24 | {changelogs?.map((log) => (
25 |
26 | ))}
27 |
28 | );
29 | };
30 |
31 | export default ChangelogList;
32 |
33 | const NoChangelogs = () => {
34 | const { onOpen } = useChangelogModal();
35 |
36 | return (
37 |
38 |
39 |
There are no Changelogs yet!
40 | onOpen()}>Add a Changelog
41 |
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/apps/web/app/api/feedback/route.ts:
--------------------------------------------------------------------------------
1 | import { api } from "@repo/backend/convex/_generated/api";
2 | import { Id } from "@repo/backend/convex/_generated/dataModel";
3 | import { feedbackFormSchema } from "@/lib/form-schemas";
4 | import { fetchMutation } from "convex/nextjs";
5 |
6 | export async function POST(request: Request) {
7 | const body = await request.json();
8 |
9 | const feedback = feedbackFormSchema.safeParse(body);
10 | if (!feedback.success) {
11 | return Response.json(
12 | {
13 | message: "Invalid feedback",
14 | errors: feedback.error.issues.map((issue) => {
15 | return {
16 | path: issue.path.join("."),
17 | message: issue.message,
18 | };
19 | }),
20 | },
21 | {
22 | status: 400,
23 | }
24 | );
25 | }
26 |
27 | try {
28 | const res = await fetchMutation(api.feedback.create, {
29 | ...feedback.data,
30 | projectId: feedback.data.projectId as Id<"projects">,
31 | });
32 |
33 | return Response.json(
34 | {
35 | message: "Feedback received",
36 | body: res,
37 | },
38 | {
39 | status: 201,
40 | }
41 | );
42 | } catch (e: any) {
43 | return Response.json(
44 | {
45 | message: "Failed to create feedback",
46 | error: "Malformed JSON or Invalid Project ID",
47 | },
48 | {
49 | status: 500,
50 | }
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/apps/web/components/md/link-modal.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { DialogClose } from "@/components/ui/dialog";
3 | import { Input } from "@/components/ui/input";
4 | import { Label } from "@/components/ui/label";
5 | import { useState, type ReactNode } from "react";
6 | import ResponsiveModel, { ResponsiveModelTitle } from "../responsive-model";
7 |
8 | type LinkModalProps = {
9 | value?: string;
10 | children: ReactNode;
11 | // eslint-disable-next-line no-unused-vars
12 | onSave: (link: string) => void;
13 | };
14 |
15 | const LinkModal = ({ value, children, onSave }: LinkModalProps) => {
16 | const [link, setLink] = useState(value || "");
17 |
18 | const onLinkSave = (link: string) => {
19 | onSave(link);
20 | setLink("");
21 | };
22 |
23 | return (
24 |
25 | Enter destination
26 |
27 |
28 | Link
29 |
30 | setLink(e.target.value)}
34 | />
35 |
36 |
37 | onLinkSave(link)}>
38 | Save
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default LinkModal;
46 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/projects/[id]/settings/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Separator } from "@/components/ui/separator";
4 | import type { ProjectId } from "@/lib/types";
5 | import { cn } from "@/lib/utils";
6 | import Link from "next/link";
7 | import { useParams, usePathname } from "next/navigation";
8 | import type { PropsWithChildren } from "react";
9 |
10 |
11 | const ProjectSettingsLayout = ({
12 | children,
13 | }: PropsWithChildren) => {
14 |
15 | const { id } = useParams()
16 | const url = usePathname();
17 |
18 | const isDangerZone = url.includes("danger-zone");
19 |
20 | const settingsURL = `/projects/${id}/settings`;
21 |
22 | return (
23 |
24 |
25 | Project Settings
26 |
27 |
28 |
33 | General
34 |
35 |
40 | Danger
41 |
42 |
43 |
44 | {children}
45 |
46 | );
47 | };
48 |
49 | export default ProjectSettingsLayout;
50 |
--------------------------------------------------------------------------------
/apps/web/components/messages/message-chat-action.tsx:
--------------------------------------------------------------------------------
1 | import { Copy, Trash2 } from "lucide-react";
2 | import { Button } from "@/components/ui/button";
3 | import type { Id } from "@repo/backend/convex/_generated/dataModel";
4 | import useApiMutation from "@/lib/hooks/use-api-mutation";
5 | import { api } from "@repo/backend/convex/_generated/api";
6 | import { toast } from "sonner";
7 | import ConfirmModal from "../modals/confirm-modal";
8 |
9 | type MessageChatActionProps = {
10 | messageId: Id<"messages">;
11 | content: string;
12 | };
13 |
14 | const MessageChatAction = ({ content, messageId }: MessageChatActionProps) => {
15 | const { mutate: deleteMessage, isPending } = useApiMutation(
16 | api.message.remove
17 | );
18 |
19 | const handleCopy = () => {
20 | navigator.clipboard.writeText(content);
21 | toast.success("Message copied to clipboard");
22 | };
23 |
24 | const handleDelete = async () => deleteMessage({ messageId });
25 |
26 | return (
27 | <>
28 |
29 |
30 |
31 |
36 |
37 |
38 |
39 |
40 | >
41 | );
42 | };
43 |
44 | export default MessageChatAction;
45 |
--------------------------------------------------------------------------------
/apps/web/components/changelogs/changelog-card.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "@/components/ui/badge";
2 | import { Card, CardContent, CardHeader } from "@/components/ui/card";
3 | import { Doc } from "@repo/backend/convex/_generated/dataModel";
4 | import { format } from "date-fns";
5 | import { CalendarDays } from "lucide-react";
6 | import MDXEditor from "../md/mdx-editor";
7 | import ChangelogActions from "./changelog-actions";
8 |
9 | type ChangelogCardProps = {
10 | changelog: Doc<"changeLogs">;
11 | };
12 |
13 | const ChangelogCard = ({ changelog }: ChangelogCardProps) => {
14 | return (
15 |
16 |
17 |
18 |
19 | v{changelog.version}
20 |
{changelog.title}
21 |
22 |
23 |
24 |
25 | {format(changelog.date, "PPP")}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default ChangelogCard;
40 |
--------------------------------------------------------------------------------
/apps/web/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
37 |
--------------------------------------------------------------------------------
/apps/web/components/feedback/feedback-list.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import FeedbackCard from "@/components/feedback/feedback-card";
4 | import { Button } from "@/components/ui/button";
5 | import { ScrollArea } from "@/components/ui/scroll-area";
6 | import { api } from "@repo/backend/convex/_generated/api";
7 | import { useFeedbackModal } from "@/lib/store/use-feedback-modal";
8 | import type { ProjectId } from "@/lib/types";
9 | import { useQuery } from "convex/react";
10 | import { useParams } from "next/navigation";
11 |
12 | const FeedbacksList = () => {
13 | const { id } = useParams();
14 |
15 | const feedbacks = useQuery(api.feedback.list, { projectId: id });
16 |
17 | return (
18 |
19 | {!feedbacks || !feedbacks?.length ? (
20 |
21 | ) : (
22 |
23 | {feedbacks.map((feedback, index) => (
24 |
25 | ))}
26 |
27 | )}
28 |
29 | );
30 | };
31 |
32 | export default FeedbacksList;
33 |
34 | const NoFeedbacks = () => {
35 | const { onOpen } = useFeedbackModal();
36 |
37 | return (
38 |
39 |
There are no feedbacks yet!
40 | onOpen()}>Add a feedback
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/apps/web/components/providers/modal-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import FileModal from "@/components/modals/file-model";
4 | import LinkModal from "@/components/modals/link-modal";
5 | import TaskModal from "@/components/modals/task-modal";
6 | import { useFeedbackModal } from "@/lib/store/use-feedback-modal";
7 | import { useFileModal } from "@/lib/store/use-file-modal";
8 | import { useLinkModal } from "@/lib/store/use-link-modal";
9 | import { useTaskModal } from "@/lib/store/use-task-modal";
10 | import { useEffect, useState } from "react";
11 | import FeedbackModal from "../modals/feedback-modal";
12 | import { useChangelogModal } from "@/lib/store/use-changelog-modal";
13 | import ChangelogModal from "../modals/changelog-modal";
14 |
15 | const ModelProvider = () => {
16 | const [isMounted, setIsMounted] = useState(false);
17 | const taskModal = useTaskModal();
18 | const linkModal = useLinkModal();
19 | const fileModal = useFileModal();
20 | const feedbackModal = useFeedbackModal();
21 | const changelogModal = useChangelogModal();
22 |
23 | useEffect(() => {
24 | setIsMounted(true);
25 | return () => setIsMounted(false);
26 | }, []);
27 |
28 | if (!isMounted) return null;
29 |
30 | if (fileModal.isOpen) return ;
31 |
32 | if (linkModal.isOpen) return ;
33 |
34 | if (taskModal.isOpen) return ;
35 |
36 | if (feedbackModal.isOpen) return ;
37 |
38 | if (changelogModal.isOpen) return ;
39 |
40 | return null;
41 | };
42 |
43 | export default ModelProvider;
44 |
--------------------------------------------------------------------------------
/apps/web/components/messages/message-input.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Input } from "@/components/ui/input";
5 | import { api } from "@repo/backend/convex/_generated/api";
6 | import type { Id } from "@repo/backend/convex/_generated/dataModel";
7 | import useApiMutation from "@/lib/hooks/use-api-mutation";
8 | import { useAuth } from "@clerk/nextjs";
9 | import { FormEvent, useState } from "react";
10 | import { toast } from "sonner";
11 |
12 | type MessageInputProps = {
13 | projectId: Id<"projects">;
14 | };
15 |
16 | const MessageInput = ({ projectId }: MessageInputProps) => {
17 | const [message, setMessage] = useState("");
18 |
19 | const { mutate: sendMessage, isPending } = useApiMutation(api.message.create);
20 |
21 | const { userId } = useAuth();
22 |
23 | const handleSubmit = (e: FormEvent) => {
24 | if (!userId) return toast.error("You must be logged in to send a message");
25 |
26 | e.preventDefault();
27 | sendMessage({
28 | content: message,
29 | projectId,
30 | senderClerkId: userId,
31 | });
32 | setMessage("");
33 | };
34 |
35 | return (
36 |
47 | );
48 | };
49 |
50 | export default MessageInput;
51 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/dashboard/_component/dashboard-top-bar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | OrganizationSwitcher,
5 | UserButton,
6 | useOrganization,
7 | } from "@clerk/nextjs";
8 | import InviteButton from "./invite-button";
9 | import { ThemeSwitch } from "@/components/theme/theme-switch";
10 |
11 | const DashboardTopBar = () => {
12 | const { organization } = useOrganization();
13 | return (
14 |
15 |
16 |
37 |
38 |
39 | {organization &&
}
40 |
49 |
50 | );
51 | };
52 |
53 | export default DashboardTopBar;
54 |
--------------------------------------------------------------------------------
/apps/web/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/apps/web/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Inter } from "next/font/google";
2 |
3 | import ConvexClientProvider from "@/components/providers/convex-client-provider";
4 | import { ThemeProvider } from "@/components/theme/theme-provider";
5 | import { cn, constructMetaTags } from "@/lib/utils";
6 |
7 | import ModelProvider from "@/components/providers/modal-provider";
8 | import { Toaster } from "@/components/ui/sonner";
9 | import "./globals.css";
10 |
11 | const inter = Inter({ subsets: ["latin"] });
12 |
13 | export const metadata = constructMetaTags({
14 | keywords: [
15 | "project management software",
16 | "free project planner",
17 | "online project management tool",
18 | "best project planner app",
19 | "project planning for small business",
20 | "project management for teams",
21 | "simple project planner",
22 | "free project planner",
23 | "project management app",
24 | ],
25 | });
26 |
27 | export default function RootLayout({
28 | children,
29 | }: Readonly<{
30 | children: React.ReactNode;
31 | }>) {
32 | return (
33 |
34 |
41 |
46 |
47 | {children}
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/apps/web/components/landing-page/features-section.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const features = [
4 | {
5 | title: "Task Management",
6 | description:
7 | "Easily create, assign, and track tasks to keep your team organized and on track.",
8 | },
9 | {
10 | title: "Multiple Teams & Projects",
11 | description:
12 | "Manage multiple teams and projects in one place, keeping everything organized and accessible.",
13 | },
14 | {
15 | title: "Seamless Collaboration",
16 | description:
17 | "Invite your team members and collaborate in real-time on projects, tasks, and deadlines.",
18 | },
19 | {
20 | title: "Intuitive Dashboard",
21 | description:
22 | "Get a clear overview of project status, upcoming deadlines, and team performance at a glance.",
23 | },
24 | {
25 | title: "Resource Management ",
26 | description:
27 | "Share documents, images, and other files securely within the platform, keeping all project-related information in one place.",
28 | },
29 | {
30 | title: "Team Communication",
31 | description:
32 | "Stay connected with your team members through built-in chat and messaging features.",
33 | },
34 | ];
35 |
36 | const FeatureSection = () => {
37 | return (
38 | <>
39 | {features.map((feature, index) => (
40 |
41 |
{feature.title}
42 |
43 | {feature.description}
44 |
45 |
46 | ))}
47 | >
48 | );
49 | };
50 |
51 | export default FeatureSection;
52 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 |
2 | # Code of Conduct
3 |
4 | ## Our Pledge
5 |
6 | In the interest of fostering an open and welcoming environment, we as
7 | contributors and maintainers pledge to making participation in our project and
8 | our community a friendly experience for everyone, regardless of any experience
9 | to give everyone an opportunity to contribute in this project.
10 |
11 | ## Our Responsibilities
12 |
13 | The primary responsibility of contributors is to provide high-quality code contributions to the project. This involves writing, reviewing, and submitting code changes that improve the project's functionality, fix bugs, or implement new features.
14 |
15 | Contributors should actively participate in project discussions and communicate effectively with other contributors, maintainers, and users. Contributors should be respectful of others when commenting on issues and pull requests, as well as when interacting with other community members in any other forum.
16 |
17 | Contributors can play a role in the long-term maintenance of the project by actively monitoring the project's issue tracker, addressing bug reports and feature requests, and collaborating with other contributors to ensure the project remains healthy and sustainable.
18 |
19 | ## Scope
20 |
21 | This Code of Conduct applies both within project spaces and in public spaces
22 | when an individual is representing the project or its community. Examples of
23 | representing a project or community include using an official project e-mail
24 | address, posting via an official social media account, or acting as an appointed
25 | representative at an online or offline event. Representation of a project may be
26 | further defined and clarified by project maintainers.
--------------------------------------------------------------------------------
/apps/web/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TogglePrimitive from "@radix-ui/react-toggle"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const toggleVariants = cva(
10 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
17 | },
18 | size: {
19 | default: "h-10 px-3 min-w-10",
20 | sm: "h-9 px-2.5 min-w-9",
21 | lg: "h-11 px-5 min-w-11",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | }
29 | )
30 |
31 | const Toggle = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef &
34 | VariantProps
35 | >(({ className, variant, size, ...props }, ref) => (
36 |
41 | ))
42 |
43 | Toggle.displayName = TogglePrimitive.Root.displayName
44 |
45 | export { Toggle, toggleVariants }
46 |
--------------------------------------------------------------------------------
/apps/web/components/messages/message-chat.tsx:
--------------------------------------------------------------------------------
1 | import { Doc } from "@repo/backend/convex/_generated/dataModel";
2 | import { useCurrentUser } from "@/lib/hooks/use-current-user";
3 | import { cn } from "@/lib/utils";
4 | import Hint from "../hint";
5 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
6 | import MessageChatAction from "./message-chat-action";
7 |
8 | type MessageChatProps = {
9 | message: Doc<"messages">;
10 | };
11 |
12 | const MessageChat = ({ message }: MessageChatProps) => {
13 | const currentUser = useCurrentUser();
14 |
15 | if(!currentUser) return null
16 |
17 | return (
18 |
23 |
24 |
25 |
31 |
32 | {message.senderName[0]}
33 |
34 |
35 |
40 |
{message.content}
41 |
42 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default MessageChat;
54 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "github-actions" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 | reviewers:
13 | - "vignesh-gupta"
14 | labels:
15 | - "dependencies"
16 |
17 | - package-ecosystem: "npm" # See documentation for possible values
18 | directory: "apps/web" # Location of package manifests
19 | schedule:
20 | interval: "weekly"
21 | reviewers:
22 | - "vignesh-gupta"
23 | labels:
24 | - "dependencies"
25 |
26 | - package-ecosystem: "npm" # See documentation for possible values
27 | directory: "apps/kafka-consumer" # Location of package manifests
28 | schedule:
29 | interval: "weekly"
30 | reviewers:
31 | - "vignesh-gupta"
32 | labels:
33 | - "dependencies"
34 |
35 | - package-ecosystem: "npm" # See documentation for possible values
36 | directory: "packages/eslint-config" # Location of package manifests
37 | schedule:
38 | interval: "weekly"
39 | reviewers:
40 | - "vignesh-gupta"
41 | labels:
42 | - "dependencies"
43 |
44 | - package-ecosystem: "npm" # See documentation for possible values
45 | directory: "packages/backend" # Location of package manifests
46 | schedule:
47 | interval: "weekly"
48 | reviewers:
49 | - "vignesh-gupta"
50 | labels:
51 | - "dependencies"
52 |
--------------------------------------------------------------------------------
/apps/web/components/feedback/feedback-integration.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Sheet,
5 | SheetContent,
6 | SheetDescription,
7 | SheetHeader,
8 | SheetTitle,
9 | SheetTrigger,
10 | } from "@/components/ui/sheet";
11 | import type { ProjectId } from "@/lib/types";
12 | import { useParams } from "next/navigation";
13 |
14 | import type { PropsWithChildren } from "react";
15 | import CodeWithCopy from "../code-with-copy";
16 |
17 | const FeedbackIntegration = ({ children }: PropsWithChildren) => {
18 | const { id } = useParams();
19 |
20 | const code = `await fetch("https://projectify.vigneshguta.me/api/feedback", {
21 | method: "POST",
22 | headers: {
23 | "Content-Type": "application/json",
24 | },
25 | body: JSON.stringify({
26 | projectId: "${id}", // required
27 | content: "I really enjoy your application", // required
28 | senderName: "John Deo", // required,
29 | senderEmail: "test@example.com", // required,
30 | type: "feature" // optional - "documentation" | "feature" | "issue" | "question" | "idea" | "other" - default is "feature"
31 | status: "open" // optional - "open" | "reviewed" | "closed" - default is "open"
32 | }),
33 | });`;
34 |
35 | return (
36 |
37 | {children}
38 |
39 |
40 | Feedback Integration
41 |
42 | Integrate feedbacks into your project.
43 |
44 |
45 |
You can send a request like following:
46 |
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default FeedbackIntegration;
55 |
--------------------------------------------------------------------------------
/apps/web/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/apps/web/components/empty-states/no-projects.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useOrganization } from "@clerk/nextjs";
4 | import Image from "next/image";
5 |
6 | import InputModal from "@/components/modals/input-modal";
7 | import { Button } from "@/components/ui/button";
8 | import { api } from "@repo/backend/convex/_generated/api";
9 | import useApiMutation from "@/lib/hooks/use-api-mutation";
10 |
11 | const NoProject = () => {
12 | const { organization } = useOrganization();
13 | const { mutate: createProject, isPending } = useApiMutation(
14 | api.project.create
15 | );
16 |
17 | const handleCreateProject = (title?: string, description?: string) => {
18 | if (!organization) throw new Error("Organization not found");
19 |
20 | return createProject({
21 | orgId: organization.id,
22 | title: title ?? "New Project",
23 | description: description ?? "Planning to do something awesome!",
24 | status: "development",
25 | });
26 | };
27 |
28 | return (
29 |
30 |
31 |
32 | Create your first Project
33 |
34 |
35 | Start by creating a Project for your team
36 |
37 |
38 |
45 | Create Project
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default NoProject;
53 |
--------------------------------------------------------------------------------
/packages/backend/convex/changelog.ts:
--------------------------------------------------------------------------------
1 | import { v } from "convex/values";
2 | import { mutation, query } from "./_generated/server";
3 |
4 | export const list = query({
5 | args: {
6 | projectId: v.id("projects"),
7 | showPublished: v.optional(v.boolean()),
8 | },
9 | handler: async (ctx, args) => {
10 | const project = await ctx.db.get(args.projectId);
11 |
12 | if (!project) {
13 | throw new Error("Project not found");
14 | }
15 |
16 | const changeLogs = ctx.db
17 | .query("changeLogs")
18 | .withIndex("by_project", (q) => q.eq("projectId", args.projectId))
19 | .order("desc");
20 |
21 | if (args.showPublished) {
22 | return changeLogs.filter((q) => q.eq(q.field("isPublished"), true));
23 | }
24 |
25 | return await changeLogs.collect();
26 | },
27 | });
28 |
29 | export const create = mutation({
30 | args: {
31 | title: v.string(),
32 | version: v.string(),
33 | changes: v.string(),
34 | date: v.string(),
35 | projectId: v.id("projects"),
36 | isPublished: v.boolean(),
37 | },
38 | handler: async (ctx, args) => {
39 | const project = await ctx.db.get(args.projectId);
40 |
41 | if (!project) {
42 | throw new Error("Project not found");
43 | }
44 |
45 | return await ctx.db.insert("changeLogs", args);
46 | },
47 | });
48 |
49 | export const update = mutation({
50 | args: {
51 | _id: v.id("changeLogs"),
52 | title: v.optional(v.string()),
53 | version: v.optional(v.string()),
54 | changes: v.optional(v.string()),
55 | date: v.optional(v.string()),
56 | isPublished: v.optional(v.boolean()),
57 | },
58 | handler: async (ctx, args) => await ctx.db.patch(args._id, args),
59 | });
60 |
61 | export const remove = mutation({
62 | args: {
63 | _id: v.id("changeLogs"),
64 | },
65 | handler: async (ctx, args) => await ctx.db.delete(args._id),
66 | });
67 |
--------------------------------------------------------------------------------
/packages/backend/convex/feedback.ts:
--------------------------------------------------------------------------------
1 | import { v } from "convex/values";
2 | import { mutation, query } from "./_generated/server";
3 | import { FeedbackStatus, FeedbackType } from "./types";
4 |
5 | export const list = query({
6 | args: {
7 | projectId: v.id("projects"),
8 | },
9 | handler: async (ctx, args) => {
10 |
11 | const project = await ctx.db.get(args.projectId);
12 |
13 | if (!project) {
14 | throw new Error("Project not found");
15 | }
16 |
17 | return await ctx.db
18 | .query("feedbacks")
19 | .withIndex("by_project", (q) => q.eq("projectId", args.projectId))
20 | .order("desc")
21 | .collect();
22 | },
23 | });
24 |
25 | export const create = mutation({
26 | args: {
27 | content: v.string(),
28 | projectId: v.id("projects"),
29 | senderName: v.string(),
30 | senderEmail: v.string(),
31 | status: FeedbackStatus,
32 | type: FeedbackType,
33 | },
34 | handler: async (ctx, args) => {
35 | console.log("Project not found", args.projectId);
36 | const project = await ctx.db.get(args.projectId);
37 |
38 | if (!project) {
39 | throw new Error("Project not found");
40 | }
41 |
42 | return await ctx.db.insert("feedbacks", args);
43 | },
44 | });
45 |
46 | export const update = mutation({
47 | args: {
48 | _id: v.id("feedbacks"),
49 | status: v.optional(FeedbackStatus),
50 | type: v.optional(FeedbackType),
51 | content: v.optional(v.string()),
52 | senderName: v.optional(v.string()),
53 | senderEmail: v.optional(v.string()),
54 | projectId: v.optional(v.id("projects")),
55 | },
56 | handler: async (ctx, args) => {
57 | return await ctx.db.patch(args._id, args);
58 | },
59 | });
60 |
61 | export const remove = mutation({
62 | args: {
63 | id: v.id("feedbacks"),
64 | },
65 | handler: async (ctx, args) => await ctx.db.delete(args.id),
66 | });
67 |
--------------------------------------------------------------------------------
/packages/backend/convex/resources/file.ts:
--------------------------------------------------------------------------------
1 | import { v } from "convex/values";
2 | import { mutation, query } from "../_generated/server";
3 |
4 | export const create = mutation({
5 | args: {
6 | title: v.string(),
7 | storageId: v.id("_storage"),
8 | projectId: v.id("projects"),
9 | type: v.string(),
10 | },
11 | handler: async (ctx, { title, storageId, projectId, type }) => {
12 | const identity = await ctx.auth.getUserIdentity();
13 |
14 | if (!identity) {
15 | throw new Error("Unauthenticated");
16 | }
17 |
18 | const project = await ctx.db.get(projectId);
19 |
20 | if (!project) {
21 | ctx.storage.delete(storageId);
22 | throw new Error("Project not found");
23 | }
24 |
25 | return await ctx.db.insert("files", {
26 | title,
27 | storageId,
28 | projectId,
29 | type,
30 | });
31 | },
32 | });
33 |
34 | export const update = mutation({
35 | args: {
36 | id: v.id("files"),
37 | title: v.string(),
38 | },
39 | handler: async (ctx, { id, title }) => {
40 | const file = await ctx.db.get(id);
41 |
42 | if (!file) {
43 | throw new Error("File not found");
44 | }
45 |
46 | return await ctx.db.patch(id, { title });
47 | },
48 | });
49 |
50 | export const remove = mutation({
51 | args: {
52 | _id: v.id("files"),
53 | },
54 | handler: async (ctx, { _id }) => {
55 | const file = await ctx.db.get(_id);
56 |
57 | if (!file) {
58 | throw new Error("File not found");
59 | }
60 |
61 | ctx.storage.delete(file.storageId);
62 |
63 | return await ctx.db.delete(_id);
64 | },
65 | });
66 |
67 | export const list = query({
68 | args: {
69 | projectId: v.id("projects"),
70 | },
71 | handler: async (ctx, args) =>
72 | await ctx.db
73 | .query("files")
74 | .withIndex("by_project", (q) => q.eq("projectId", args.projectId))
75 | .collect(),
76 | });
77 |
--------------------------------------------------------------------------------
/apps/web/components/work-items/data-table-view-options.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
4 | import { Table } from "@tanstack/react-table";
5 |
6 | import { Button } from "@/components/ui/button";
7 | import {
8 | DropdownMenu,
9 | DropdownMenuCheckboxItem,
10 | DropdownMenuContent,
11 | DropdownMenuLabel,
12 | DropdownMenuSeparator,
13 | } from "@/components/ui/dropdown-menu";
14 | import { Settings2 } from "lucide-react";
15 |
16 | interface DataTableViewOptionsProps {
17 | table: Table;
18 | }
19 |
20 | export function DataTableViewOptions({
21 | table,
22 | }: DataTableViewOptionsProps) {
23 | return (
24 |
25 |
26 |
31 |
32 | View
33 |
34 |
35 |
36 | Toggle columns
37 |
38 | {table
39 | .getAllColumns()
40 | .filter(
41 | (column) =>
42 | typeof column.accessorFn !== "undefined" && column.getCanHide()
43 | )
44 | .map((column) => {
45 | return (
46 | column.toggleVisibility(!!value)}
51 | >
52 | {column.id}
53 |
54 | );
55 | })}
56 |
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/projects/[id]/settings/danger-zone/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import ConfirmModal from "@/components/modals/confirm-modal";
4 | import { Button } from "@/components/ui/button";
5 | import { DASHBOARD_ROUTE } from "@/lib/constants";
6 | import useApiMutation from "@/lib/hooks/use-api-mutation";
7 | import type { ProjectId } from "@/lib/types";
8 | import { api } from "@repo/backend/convex/_generated/api";
9 | import { KafkaMessage } from "@repo/backend/lib/types";
10 | import { useParams, useRouter } from "next/navigation";
11 | import { toast } from "sonner";
12 |
13 | const ProjectSettingsDangerZonePage = () => {
14 |
15 | const { id } = useParams()
16 |
17 | const { isPending, mutate: deleteProject } = useApiMutation(
18 | api.project.remove
19 | );
20 | const router = useRouter();
21 |
22 | const handleDeleteProject = async () => {
23 | const message: KafkaMessage = {
24 | resource: "project",
25 | id,
26 | action: "delete",
27 | };
28 |
29 | try {
30 | await fetch("/api/kafka/produce", {
31 | method: "POST",
32 | body: JSON.stringify(message),
33 | });
34 | await deleteProject({ id });
35 | router.push(DASHBOARD_ROUTE);
36 | } catch (e) {
37 | toast.error("Something went wrong, try again later!");
38 | }
39 | };
40 |
41 | return (
42 |
43 |
44 |
Delete the project
45 |
51 |
52 | Delete Project
53 |
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | export default ProjectSettingsDangerZonePage;
61 |
--------------------------------------------------------------------------------
/apps/web/docs/code-export-5-25-2024-7_52_31-PM.txt:
--------------------------------------------------------------------------------
1 |
2 | title Project Management System
3 |
4 | projects [icon: briefcase, color: lightblue]{
5 | id string pk
6 | title string
7 | description string
8 | status string
9 | creatorId string
10 | creatorName string
11 | orgId string
12 | }
13 |
14 | workItems [icon: check-square, color: yellow]{
15 | id string pk
16 | title string
17 | description string
18 | assigneeId string fk
19 | assignee string
20 | label string
21 | priority string
22 | projectId string fk
23 | status string
24 | }
25 |
26 | users [icon: user, color: green]{
27 | id string pk
28 | email string
29 | firstName string
30 | imageUrl string
31 | clerkId string
32 | }
33 |
34 | teams [icon: users, color: blue]{
35 | id string pk
36 | name string
37 | clerkId string
38 | imageUrl string
39 | createdBy string fk
40 | }
41 |
42 | team_memberships [icon: user-check, color: purple]{
43 | id string pk
44 | teamId string fk
45 | userId string fk
46 | isAdmin boolean
47 | }
48 |
49 | links [icon: link, color: orange]{
50 | id string pk
51 | title string
52 | url string
53 | icon string
54 | projectId string fk
55 | }
56 |
57 | files [icon: file, color: red]{
58 | id string pk
59 | title string
60 | storageId string
61 | projectId string fk
62 | type string
63 | }
64 |
65 | messages [icon: message-circle, color: pink]{
66 | id string pk
67 | content string
68 | projectId string fk
69 | senderId string fk
70 | senderName string
71 | senderImageUrl string
72 | }
73 | // End of tables
74 | // define relationships
75 | projects.orgId > orgs.id
76 | workItems.projectId > projects.id
77 | workItems.assigneeId > users.id
78 | teams.createdBy > users.id
79 | team_memberships.teamId > teams.id
80 | team_memberships.userId > users.id
81 | links.projectId > projects.id
82 | files.projectId > projects.id
83 | messages.projectId > projects.id
84 | messages.senderId > users.id
85 |
--------------------------------------------------------------------------------
/apps/web/components/modals/confirm-modal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AlertDialog,
3 | AlertDialogAction,
4 | AlertDialogCancel,
5 | AlertDialogContent,
6 | AlertDialogDescription,
7 | AlertDialogFooter,
8 | AlertDialogHeader,
9 | AlertDialogTrigger,
10 | } from "@/components/ui/alert-dialog";
11 | import type { ReactNode } from "react";
12 | import { buttonVariants } from "@/components/ui/button";
13 | import { toast } from "sonner";
14 |
15 | type ConfirmModalProps = {
16 | children: ReactNode;
17 | disabled?: boolean;
18 | onConfirm: () => Promise;
19 | header: string;
20 | description?: string;
21 | toastMessage?: string;
22 | };
23 |
24 | const ConfirmModal = ({
25 | children,
26 | description = "The action cannot be undone. Are you sure you want to proceed?",
27 | disabled,
28 | header,
29 | onConfirm,
30 | toastMessage = "Action completed successfully.",
31 | }: ConfirmModalProps) => {
32 | const handleConfirm = () => {
33 | onConfirm()
34 | .then(() => toast.success(toastMessage))
35 | .catch(() => toast.error("Failed to perform action. Please try again."));
36 | };
37 |
38 | return (
39 |
40 | {children}
41 |
42 | {header}
43 | {description}
44 |
45 | Cancel
46 |
51 | Confirm
52 |
53 |
54 |
55 |
56 | );
57 | };
58 |
59 | export default ConfirmModal;
60 |
--------------------------------------------------------------------------------
/apps/web/components/resources/file/file-list.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Card, CardContent, CardHeader } from "@/components/ui/card";
3 | import { Skeleton } from "@/components/ui/skeleton";
4 | import { Doc } from "@repo/backend/convex/_generated/dataModel";
5 | import { Edit, Trash } from "lucide-react";
6 | import FileCard from "./file-card";
7 | import FileUpload from "./file-upload";
8 |
9 | type FileListProps = {
10 | files: Doc<"files">[] | undefined;
11 | };
12 |
13 | const FileList = ({ files }: FileListProps) => {
14 | return (
15 |
16 |
17 | Files
18 |
19 |
20 |
21 | {!files ? (
22 |
23 | ) : files.length <= 0 ? (
24 |
25 | ) : (
26 | files.map((res) => )
27 | )}
28 |
29 |
30 | );
31 | };
32 |
33 | export default FileList;
34 |
35 | const NoFiles = () => (
36 |
37 |
There are no files
38 |
39 |
40 | );
41 |
42 | const FileSkeleton = () => (
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | );
56 |
--------------------------------------------------------------------------------
/packages/backend/convex/_generated/dataModel.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * Generated data model types.
4 | *
5 | * THIS CODE IS AUTOMATICALLY GENERATED.
6 | *
7 | * To regenerate, run `npx convex dev`.
8 | * @module
9 | */
10 |
11 | import type {
12 | DataModelFromSchemaDefinition,
13 | DocumentByName,
14 | TableNamesInDataModel,
15 | SystemTableNames,
16 | } from "convex/server";
17 | import type { GenericId } from "convex/values";
18 | import schema from "../schema.js";
19 |
20 | /**
21 | * The names of all of your Convex tables.
22 | */
23 | export type TableNames = TableNamesInDataModel;
24 |
25 | /**
26 | * The type of a document stored in Convex.
27 | *
28 | * @typeParam TableName - A string literal type of the table name (like "users").
29 | */
30 | export type Doc = DocumentByName<
31 | DataModel,
32 | TableName
33 | >;
34 |
35 | /**
36 | * An identifier for a document in Convex.
37 | *
38 | * Convex documents are uniquely identified by their `Id`, which is accessible
39 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
40 | *
41 | * Documents can be loaded using `db.get(id)` in query and mutation functions.
42 | *
43 | * IDs are just strings at runtime, but this type can be used to distinguish them from other
44 | * strings when type checking.
45 | *
46 | * @typeParam TableName - A string literal type of the table name (like "users").
47 | */
48 | export type Id =
49 | GenericId;
50 |
51 | /**
52 | * A type describing your Convex data model.
53 | *
54 | * This type includes information about what tables you have, the type of
55 | * documents stored in those tables, and the indexes defined on them.
56 | *
57 | * This type is used to parameterize methods like `queryGeneric` and
58 | * `mutationGeneric` to make them type-safe.
59 | */
60 | export type DataModel = DataModelFromSchemaDefinition;
61 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/projects/[id]/_components/project-mobile-bar.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeSwitch } from "@/components/theme/theme-switch";
2 | import { Button, buttonVariants } from "@/components/ui/button";
3 | import {
4 | Sheet,
5 | SheetClose,
6 | SheetContent,
7 | SheetHeader,
8 | SheetTitle,
9 | SheetTrigger,
10 | } from "@/components/ui/sheet";
11 | import { getNavLinks } from "@/lib/utils";
12 | import { UserButton } from "@clerk/nextjs";
13 | import { Menu } from "lucide-react";
14 | import Link from "next/link";
15 | import { useParams } from "next/navigation";
16 |
17 | const ProjectMobileBar = () => {
18 | const param = useParams();
19 |
20 | const navLinks = getNavLinks(param.id as string);
21 |
22 | return (
23 |
24 |
25 |
31 |
32 |
33 |
34 |
35 |
36 | Project Menu
37 |
38 |
39 |
40 | {navLinks.map(({ href, Icon, name }) => (
41 |
49 |
50 | {name}
51 |
52 |
53 | ))}
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | );
62 | };
63 |
64 | export default ProjectMobileBar;
65 |
--------------------------------------------------------------------------------
/apps/web/components/resources/link/link-list.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Card, CardContent, CardHeader } from "@/components/ui/card";
3 | import { Skeleton } from "@/components/ui/skeleton";
4 | import { Doc } from "@repo/backend/convex/_generated/dataModel";
5 | import { Edit, Trash } from "lucide-react";
6 | import AddLink from "./add-link";
7 | import LinkCard from "./link-card";
8 |
9 | type LinkListProps = {
10 | links: Doc<"links">[] | undefined;
11 | };
12 |
13 | const LinkList = ({ links }: LinkListProps) => {
14 | return (
15 |
16 |
17 | Links
18 |
19 |
20 |
21 | {!links ? (
22 |
23 | ) : links.length === 0 ? (
24 |
25 | ) : (
26 | links.map((res) => )
27 | )}
28 |
29 |
30 | );
31 | };
32 |
33 | export default LinkList;
34 |
35 | const NoLinks = () => (
36 |
37 |
There are no links
38 |
39 |
40 | );
41 |
42 | const LinkSkeleton = () => {
43 | return (
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | );
59 | };
60 |
--------------------------------------------------------------------------------
/apps/web/components/work-items/data-table-row-actions.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DropdownMenu,
3 | DropdownMenuContent,
4 | DropdownMenuItem,
5 | DropdownMenuTrigger,
6 | } from "@/components/ui/dropdown-menu";
7 | import { useTaskModal } from "@/lib/store/use-task-modal";
8 | import { Row } from "@tanstack/react-table";
9 | import { Edit, MoreHorizontal, Trash } from "lucide-react";
10 | import { Button } from "@/components/ui/button";
11 | import { Task } from "./data-table";
12 | import useApiMutation from "@/lib/hooks/use-api-mutation";
13 | import { api } from "@repo/backend/convex/_generated/api";
14 | import { toast } from "sonner";
15 |
16 | type DataTableRowActionsProps = {
17 | row: Row;
18 | };
19 |
20 | const DataTableRowActions = ({ row }: DataTableRowActionsProps) => {
21 | const { onOpen } = useTaskModal();
22 | const { mutate: deleteTask, isPending } = useApiMutation(
23 | api.work_item.remove
24 | );
25 |
26 | const handleEdit = () => {
27 | onOpen(row.original);
28 | };
29 |
30 | const handleDelete = () => {
31 | deleteTask({ _id: row.original._id })
32 | .then(() => toast.success("Task deleted successfully"))
33 | .catch(() => toast.error("Failed to delete task. Please try again."));
34 | };
35 |
36 | return (
37 |
38 |
39 |
40 | Open menu
41 |
42 |
43 |
44 |
45 |
46 | Edit
47 |
48 |
49 | Delete
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export default DataTableRowActions;
57 |
--------------------------------------------------------------------------------
/packages/backend/convex/_generated/api.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * Generated `api` utility.
4 | *
5 | * THIS CODE IS AUTOMATICALLY GENERATED.
6 | *
7 | * To regenerate, run `npx convex dev`.
8 | * @module
9 | */
10 |
11 | import type {
12 | ApiFromModules,
13 | FilterApi,
14 | FunctionReference,
15 | } from "convex/server";
16 | import type * as api_key from "../api_key.js";
17 | import type * as changelog from "../changelog.js";
18 | import type * as feedback from "../feedback.js";
19 | import type * as http from "../http.js";
20 | import type * as message from "../message.js";
21 | import type * as project from "../project.js";
22 | import type * as resources_file from "../resources/file.js";
23 | import type * as resources_link from "../resources/link.js";
24 | import type * as resources_storage from "../resources/storage.js";
25 | import type * as team from "../team.js";
26 | import type * as team_membership from "../team_membership.js";
27 | import type * as types from "../types.js";
28 | import type * as user from "../user.js";
29 | import type * as work_item from "../work_item.js";
30 |
31 | /**
32 | * A utility for referencing Convex functions in your app's API.
33 | *
34 | * Usage:
35 | * ```js
36 | * const myFunctionReference = api.myModule.myFunction;
37 | * ```
38 | */
39 | declare const fullApi: ApiFromModules<{
40 | api_key: typeof api_key;
41 | changelog: typeof changelog;
42 | feedback: typeof feedback;
43 | http: typeof http;
44 | message: typeof message;
45 | project: typeof project;
46 | "resources/file": typeof resources_file;
47 | "resources/link": typeof resources_link;
48 | "resources/storage": typeof resources_storage;
49 | team: typeof team;
50 | team_membership: typeof team_membership;
51 | types: typeof types;
52 | user: typeof user;
53 | work_item: typeof work_item;
54 | }>;
55 | export declare const api: FilterApi<
56 | typeof fullApi,
57 | FunctionReference
58 | >;
59 | export declare const internal: FilterApi<
60 | typeof fullApi,
61 | FunctionReference
62 | >;
63 |
--------------------------------------------------------------------------------
/apps/web/app/(main)/projects/[id]/_components/owned-task-table.tsx:
--------------------------------------------------------------------------------
1 | import TaskPriority from "@/components/task/task-priority";
2 | import TaskStatus from "@/components/task/task-status";
3 | import TaskTitle from "@/components/task/task-title";
4 | import { Button } from "@/components/ui/button";
5 | import {
6 | Table,
7 | TableBody,
8 | TableCell,
9 | TableHead,
10 | TableHeader,
11 | TableRow,
12 | } from "@/components/ui/table";
13 | import { Doc } from "@repo/backend/convex/_generated/dataModel";
14 | import { useTaskModal } from "@/lib/store/use-task-modal";
15 |
16 | type OwnedTaskTableProps = {
17 | tasks: Doc<"workItems">[];
18 | };
19 |
20 | const OwnedTaskTable = ({ tasks }: OwnedTaskTableProps) => {
21 | if (tasks.length === 0) return ;
22 |
23 | return (
24 |
25 |
26 |
27 | Title
28 | Status
29 | Priority
30 |
31 |
32 |
33 | {tasks.map((task) => (
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | ))}
46 |
47 |
48 | );
49 | };
50 |
51 | export default OwnedTaskTable;
52 |
53 | const NoTask = () => {
54 | const { onOpen } = useTaskModal();
55 |
56 | return (
57 |
58 |
There are no task assigned
59 | onOpen()}>
60 | Add Task
61 |
62 |
63 | );
64 | };
65 |
--------------------------------------------------------------------------------
/packages/backend/convex/message.ts:
--------------------------------------------------------------------------------
1 | import { v } from "convex/values";
2 | import { mutation, query } from "./_generated/server";
3 | import { paginationOptsValidator } from "convex/server";
4 |
5 | export const create = mutation({
6 | args: {
7 | content: v.string(),
8 | senderClerkId: v.string(),
9 | projectId: v.id("projects"),
10 | },
11 | handler: async (ctx, args) => {
12 | const identity = ctx.auth.getUserIdentity();
13 | if (!identity) {
14 | throw new Error("Unauthenticated user cannot create messages");
15 | }
16 |
17 | const project = await ctx.db.get(args.projectId);
18 | if (!project) {
19 | throw new Error("Project not found");
20 | }
21 |
22 | const sender = await ctx.db
23 | .query("users")
24 | .withIndex("by_clerk", (q) => q.eq("clerkId", args.senderClerkId))
25 | .first();
26 |
27 | if (!sender) {
28 | throw new Error("Sender not found");
29 | }
30 |
31 | return ctx.db.insert("messages", {
32 | content: args.content,
33 | projectId: args.projectId,
34 | senderId: sender._id,
35 | senderName: sender.firstName,
36 | senderImageUrl: sender.imageUrl,
37 | });
38 | },
39 | });
40 |
41 | export const remove = mutation({
42 | args: {
43 | messageId: v.id("messages"),
44 | },
45 | handler: async (ctx, args) => ctx.db.delete(args.messageId),
46 | });
47 |
48 | export const list = query({
49 | args: {
50 | projectId: v.id("projects"),
51 | paginationOpts: paginationOptsValidator,
52 | },
53 | handler: async (ctx, args) => {
54 | return ctx.db
55 | .query("messages")
56 | .withIndex("by_project", (q) => q.eq("projectId", args.projectId))
57 | .order("desc")
58 | .paginate(args.paginationOpts);
59 | },
60 | });
61 |
62 | export const listAll = query({
63 | args: {
64 | projectId: v.id("projects"),
65 | },
66 | handler: async (ctx, args) => {
67 | return ctx.db
68 | .query("messages")
69 | .withIndex("by_project", (q) => q.eq("projectId", args.projectId))
70 | .order("desc")
71 | .collect();
72 | },
73 | });
74 |
75 |
--------------------------------------------------------------------------------
/apps/web/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/apps/web/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/apps/web/components/code-with-copy.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useRef } from "react";
4 | import { CheckIcon, CopyIcon } from "lucide-react";
5 | import { cn } from "@/lib/utils";
6 | import { Button } from "./ui/button";
7 |
8 | interface CodeWithCopyProps {
9 | code: string;
10 | language?: string;
11 | showLineNumbers?: boolean;
12 | maxHeight?: string;
13 | title?: string;
14 | }
15 |
16 | const CodeWithCopy = ({
17 | code,
18 | language = "json",
19 | showLineNumbers = false,
20 | title,
21 | }: CodeWithCopyProps) => {
22 | const [isCopied, setIsCopied] = useState(false);
23 | const codeRef = useRef(null);
24 |
25 | const copyToClipboard = async () => {
26 | if (!navigator.clipboard) {
27 | console.error("Clipboard API not available");
28 | return;
29 | }
30 |
31 | try {
32 | await navigator.clipboard.writeText(code);
33 | setIsCopied(true);
34 | setTimeout(() => setIsCopied(false), 2000);
35 | } catch (err) {
36 | console.error("Failed to copy text: ", err);
37 | }
38 | };
39 |
40 | return (
41 |
42 | {title && (
43 |
44 | {title}
45 |
46 | )}
47 |
48 |
54 | {isCopied ? : }
55 |
56 |
63 | {code}
64 |
65 |
66 |
67 | );
68 | };
69 |
70 | export default CodeWithCopy;
71 |
--------------------------------------------------------------------------------
/apps/kafka-consumer/src/index.ts:
--------------------------------------------------------------------------------
1 | const { Kafka, logLevel } = require("kafkajs");
2 | const { postDeleteProject } = require("./service/delete-child");
3 | const actuator = require("express-actuator");
4 | const express = require("express");
5 |
6 | import { KafkaMessage } from "@repo/backend/lib/types";
7 | import { Response } from "express";
8 |
9 | const kafka = new Kafka({
10 | brokers: [process.env.KAFKA_BROKER!],
11 | connectionTimeout: 10000,
12 | sasl: {
13 | mechanism: "scram-sha-256",
14 | username: process.env.KAFKA_USERNAME!,
15 | password: process.env.KAFKA_PASSWORD!,
16 | },
17 | logLevel: logLevel.ERROR,
18 | });
19 |
20 | const consumer = kafka.consumer({ groupId: "group-1" });
21 |
22 | const run = async () => {
23 | await consumer.connect().then(() => console.log("Connected"));
24 | const topic = process.env.KAFKA_TOPIC || "delete-child";
25 | await consumer
26 | .subscribe({
27 | topic,
28 | fromBeginning: true,
29 | })
30 | .then(() => console.log("Subscribed to topic: "+ topic));
31 |
32 | await consumer.run({
33 | eachMessage: async ({ topic, partition, message }) => {
34 | console.log({
35 | partition,
36 | offset: message.offset,
37 | topic,
38 | value: message?.value?.toString() || "Missing value",
39 | });
40 |
41 | if (!message.value?.keys) return;
42 |
43 | const messageValue: KafkaMessage = JSON.parse(message.value.toString());
44 | const { id, resource } = messageValue;
45 |
46 | if (!id || !resource) return;
47 |
48 | switch (resource) {
49 | case "project":
50 | postDeleteProject(id);
51 | break;
52 |
53 | default:
54 | break;
55 | }
56 | },
57 | });
58 | };
59 |
60 | run().catch((e) => console.error("[consumer] e.message", e));
61 |
62 | const app = express();
63 | const port = process.env.PORT || 8080;
64 |
65 | app.use(actuator());
66 |
67 | app.get("/", (_: any, res: Response) => {
68 | res.send("Welcome to the Projectify Kaka consumer!");
69 | });
70 |
71 | app.listen(port, () => {
72 | console.log(`App listening on port ${port}`);
73 | });
74 |
--------------------------------------------------------------------------------
/apps/web/components/md/mdx-editor.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | import Bold from "@tiptap/extension-bold";
6 | import BulletList from "@tiptap/extension-bullet-list";
7 | import CodeBlock from "@tiptap/extension-code-block";
8 | import Document from "@tiptap/extension-document";
9 | import History from "@tiptap/extension-history";
10 | import Italic from "@tiptap/extension-italic";
11 | import Link from "@tiptap/extension-link";
12 | import ListItem from "@tiptap/extension-list-item";
13 | import Paragraph from "@tiptap/extension-paragraph";
14 | import Text from "@tiptap/extension-text";
15 | import { EditorContent, useEditor } from "@tiptap/react";
16 |
17 | import Toolbar from "./toolbar";
18 |
19 | type MDXEditorProps = {
20 | content: string;
21 | readonly?: boolean;
22 | rows?: number;
23 | // eslint-disable-next-line no-unused-vars
24 | onChange?: (...event: any[]) => void;
25 | };
26 |
27 | const MDXEditor = ({ content, readonly, onChange, rows }: MDXEditorProps) => {
28 | const editor = useEditor({
29 | extensions: [
30 | Document,
31 | Paragraph,
32 | History,
33 | Text,
34 | ListItem,
35 | BulletList,
36 | CodeBlock,
37 | Link,
38 | Italic,
39 | Bold.extend({
40 | renderHTML({ HTMLAttributes }) {
41 | return ["b", HTMLAttributes, 0];
42 | },
43 | }),
44 | ],
45 | content,
46 | editable: !readonly,
47 | editorProps: {
48 | attributes: {
49 | class: "prose focus:outline-none dark:text-white *:my-1",
50 | },
51 | },
52 | onUpdate: ({ editor }) => {
53 | if (readonly || !onChange) return;
54 | onChange(editor.getHTML());
55 | },
56 | });
57 |
58 | return (
59 | <>
60 | {!readonly && editor && }
61 |
70 | >
71 | );
72 | };
73 |
74 | export default MDXEditor;
75 |
--------------------------------------------------------------------------------
/apps/web/components/changelogs/changelog-actions.tsx:
--------------------------------------------------------------------------------
1 | import { api } from "@repo/backend/convex/_generated/api";
2 | import { Doc } from "@repo/backend/convex/_generated/dataModel";
3 | import useApiMutation from "@/lib/hooks/use-api-mutation";
4 | import { useChangelogModal } from "@/lib/store/use-changelog-modal";
5 | import { Edit, Trash } from "lucide-react";
6 | import ConfirmModal from "../modals/confirm-modal";
7 | import { Button } from "../ui/button";
8 | import { Switch } from "../ui/switch";
9 |
10 | type ChangelogActionsProps = {
11 | changelog: Doc<"changeLogs">;
12 | };
13 |
14 | const ChangelogActions = ({ changelog }: ChangelogActionsProps) => {
15 | const { mutate: deleteLog, isPending: isDeleting } = useApiMutation(
16 | api.changelog.remove
17 | );
18 |
19 | const { mutate: togglePublish, isPending: isUpdating } = useApiMutation(
20 | api.changelog.update
21 | );
22 |
23 | const { onOpen } = useChangelogModal();
24 |
25 | return (
26 |
27 |
onOpen(changelog)}
32 | >
33 |
34 |
35 |
deleteLog({ _id: changelog._id })}
38 | disabled={isDeleting || isUpdating}
39 | toastMessage="Changelog deleted successfully"
40 | >
41 |
42 |
43 |
44 |
45 |
46 |
47 | {changelog.isPublished ? "Public" : "Private"}
48 |
49 |
52 | togglePublish({ _id: changelog._id, isPublished: value })
53 | }
54 | disabled={isUpdating || isDeleting}
55 | />
56 |
57 |
58 | );
59 | };
60 |
61 | export default ChangelogActions;
62 |
--------------------------------------------------------------------------------
/apps/web/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLDivElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLDivElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/packages/backend/convex/resources/link.ts:
--------------------------------------------------------------------------------
1 | import { mutation, query } from "../_generated/server";
2 | import { v } from "convex/values";
3 |
4 | export const create = mutation({
5 | args: {
6 | title: v.string(),
7 | url: v.string(),
8 | projectId: v.id("projects"),
9 | icon: v.optional(v.id("_storage")),
10 | },
11 | handler: async (ctx, args) => {
12 | const identity = ctx.auth.getUserIdentity();
13 | if (!identity) {
14 | throw new Error("Unauthenticated user cannot create resources");
15 | }
16 |
17 | const project = await ctx.db.get(args.projectId);
18 |
19 | if (!project) {
20 | throw new Error("Project not found");
21 | }
22 |
23 | return ctx.db.insert("links", args);
24 | },
25 | });
26 |
27 | export const update = mutation({
28 | args: {
29 | _id: v.id("links"),
30 | title: v.optional(v.string()),
31 | url: v.optional(v.string()),
32 | icon: v.optional(v.id("_storage")),
33 | },
34 | handler: async (ctx, args) => {
35 | const identity = ctx.auth.getUserIdentity();
36 | if (!identity) {
37 | throw new Error("Unauthenticated user cannot update resources");
38 | }
39 |
40 | const link = await ctx.db.get(args._id);
41 |
42 | if (!link) throw new Error("Link resource not found");
43 |
44 | await ctx.db.patch(args._id, args);
45 |
46 | return args._id;
47 | },
48 | });
49 |
50 | export const remove = mutation({
51 | args: {
52 | _id: v.id("links"),
53 | },
54 | handler: async (ctx, args) => {
55 | const link = await ctx.db.get(args._id);
56 |
57 | if (!link) {
58 | throw new Error("Link resource not found");
59 | }
60 |
61 | if (link.icon) ctx.storage.delete(link.icon);
62 |
63 | return ctx.db.delete(args._id);
64 | },
65 | });
66 |
67 | export const list = query({
68 | args: {
69 | projectId: v.id("projects"),
70 | },
71 | handler: async (ctx, args) => {
72 | const project = await ctx.db.get(args.projectId);
73 | if (!project) {
74 | throw new Error("Project not found");
75 | }
76 |
77 | return ctx.db
78 | .query("links")
79 | .withIndex("by_project", (q) => q.eq("projectId", args.projectId))
80 | .collect();
81 | },
82 | });
83 |
--------------------------------------------------------------------------------
/apps/web/lib/hooks/use-event.ts:
--------------------------------------------------------------------------------
1 | // This file is from https://github.com/get-convex/uploadstuff/blob/main/lib/useEvent.ts
2 |
3 | // Ripped from https://github.com/scottrippey/react-use-event-hook
4 | import { useInsertionEffect, useLayoutEffect, useRef } from "react";
5 |
6 | // eslint-disable-next-line no-unused-vars
7 | type AnyFunction = (...args: any[]) => any;
8 | const noop = () => void 0;
9 |
10 | /**
11 | * Suppress the warning when using useLayoutEffect with SSR. (https://reactjs.org/link/uselayouteffect-ssr)
12 | * Make use of useInsertionEffect if available.
13 | */
14 | const useInsertionEffect_ =
15 | typeof window !== "undefined"
16 | ? // useInsertionEffect is available in React 18+
17 | useInsertionEffect || useLayoutEffect
18 | : noop;
19 |
20 | /**
21 | * Similar to useCallback, with a few subtle differences:
22 | * - The returned function is a stable reference, and will always be the same between renders
23 | * - No dependency lists required
24 | * - Properties or state accessed within the callback will always be "current"
25 | */
26 | export function useEvent(
27 | callback: TCallback
28 | ): TCallback {
29 | // Keep track of the latest callback:
30 | const latestRef = useRef(
31 | useEvent_shouldNotBeInvokedBeforeMount as any
32 | );
33 | useInsertionEffect_(() => {
34 | latestRef.current = callback;
35 | }, [callback]);
36 |
37 | // Create a stable callback that always calls the latest callback:
38 | // using useRef instead of useCallback avoids creating and empty array on every render
39 | const stableRef = useRef(null);
40 | if (!stableRef.current) {
41 | // eslint-disable-next-line no-unused-vars
42 | stableRef.current = function (this: unknown) {
43 | return latestRef.current.apply(this, arguments as any);
44 | } as TCallback;
45 | }
46 |
47 | return stableRef.current;
48 | }
49 |
50 | /**
51 | * Render methods should be pure, especially when concurrency is used,
52 | * so we will throw this error if the callback is called while rendering.
53 | */
54 | function useEvent_shouldNotBeInvokedBeforeMount() {
55 | throw new Error(
56 | "INVALID_USEEVENT_INVOCATION: the callback from useEvent cannot be invoked before the component has mounted."
57 | );
58 | }
--------------------------------------------------------------------------------
/apps/web/app/(main)/dashboard/settings/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Input } from "@/components/ui/input";
5 | import { api } from "@repo/backend/convex/_generated/api";
6 | import useApiMutation from "@/lib/hooks/use-api-mutation";
7 | import { useQuery } from "convex/react";
8 | import { Copy, RefreshCcw, Trash } from "lucide-react";
9 | import { toast } from "sonner";
10 |
11 | const AccountSettingsPage = () => {
12 | const existingAPIKey = useQuery(api.api_key.get);
13 |
14 | const { mutate: createAPIKey, isPending } = useApiMutation(
15 | api.api_key.create
16 | );
17 |
18 | const { mutate: revokeAPIKey, isPending: isRevoking } = useApiMutation(
19 | api.api_key.revoke
20 | );
21 |
22 | const handleCopy = () => {
23 | navigator.clipboard
24 | .writeText(existingAPIKey?.key || "")
25 | .then(() => toast.success("API key copied to clipboard"))
26 | .catch(() => toast.error("Failed to copy API key to clipboard"));
27 | };
28 |
29 | const handleCreate = () => {
30 | createAPIKey()
31 | .then(() => toast.success("API key generated"))
32 | .catch(() => toast.error("Failed to generate API key"));
33 | };
34 |
35 | const handleRevoke = () => {
36 | revokeAPIKey()
37 | .then(() => toast.success("API key revoked"))
38 | .catch(() => toast.error("Failed to revoke API key"));
39 | };
40 |
41 | return (
42 | <>
43 | {existingAPIKey ? (
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | ) : (
59 |
60 | Generate a token
61 |
62 | )}
63 | >
64 | );
65 | };
66 |
67 | export default AccountSettingsPage;
68 |
--------------------------------------------------------------------------------
/apps/web/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 0 0% 3.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 0 0% 3.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 0 0% 3.9%;
15 |
16 | --primary: 0 0% 9%;
17 | --primary-foreground: 0 0% 98%;
18 |
19 | --secondary: 0 0% 96.1%;
20 | --secondary-foreground: 0 0% 9%;
21 |
22 | --muted: 0 0% 96.1%;
23 | --muted-foreground: 0 0% 45.1%;
24 |
25 | --accent: 0 0% 96.1%;
26 | --accent-foreground: 0 0% 9%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 0 0% 98%;
30 |
31 | --border: 0 0% 89.8%;
32 | --input: 0 0% 89.8%;
33 | --ring: 0 0% 3.9%;
34 |
35 | --radius: 0.5rem;
36 |
37 | --chart-1: 12 76% 61%;
38 |
39 | --chart-2: 173 58% 39%;
40 |
41 | --chart-3: 197 37% 24%;
42 |
43 | --chart-4: 43 74% 66%;
44 |
45 | --chart-5: 27 87% 67%;
46 | }
47 |
48 | .dark {
49 | --background: 0 0% 3.9%;
50 | --foreground: 0 0% 98%;
51 |
52 | --card: 0 0% 3.9%;
53 | --card-foreground: 0 0% 98%;
54 |
55 | --popover: 0 0% 3.9%;
56 | --popover-foreground: 0 0% 98%;
57 |
58 | --primary: 0 0% 98%;
59 | --primary-foreground: 0 0% 9%;
60 |
61 | --secondary: 0 0% 14.9%;
62 | --secondary-foreground: 0 0% 98%;
63 |
64 | --muted: 0 0% 14.9%;
65 | --muted-foreground: 0 0% 63.9%;
66 |
67 | --accent: 0 0% 14.9%;
68 | --accent-foreground: 0 0% 98%;
69 |
70 | --destructive: 0 62.8% 30.6%;
71 | --destructive-foreground: 0 0% 98%;
72 |
73 | --border: 0 0% 14.9%;
74 | --input: 0 0% 14.9%;
75 | --ring: 0 0% 83.1%;
76 | --chart-1: 220 70% 50%;
77 | --chart-2: 160 60% 45%;
78 | --chart-3: 30 80% 55%;
79 | --chart-4: 280 65% 60%;
80 | --chart-5: 340 75% 55%;
81 | }
82 | }
83 |
84 | @layer base {
85 | * {
86 | @apply border-border;
87 | }
88 | body {
89 | @apply bg-background text-foreground;
90 | }
91 |
92 | }
93 |
94 |
95 | .hide-scrollbar {
96 | scrollbar-width: none;
97 | -ms-overflow-style: none;
98 | }
99 |
100 | .hide-scrollbar::-webkit-scrollbar {
101 | display: none;
102 | }
--------------------------------------------------------------------------------
/apps/web/components/projects/project-card.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import {
3 | Card,
4 | CardContent,
5 | CardDescription,
6 | CardHeader,
7 | CardTitle,
8 | } from "@/components/ui/card";
9 | import {
10 | DropdownMenu,
11 | DropdownMenuContent,
12 | DropdownMenuItem,
13 | DropdownMenuTrigger,
14 | } from "@/components/ui/dropdown-menu";
15 | import type { Id } from "@repo/backend/convex/_generated/dataModel";
16 | import { MoreHorizontalIcon, NotebookPen } from "lucide-react";
17 | import Link from "next/link";
18 | import ProjectStatus from "./project-status";
19 |
20 | type ProjectCardProps = {
21 | _id: Id<"projects">;
22 | description?: string | undefined;
23 | title: string;
24 | status: string;
25 | };
26 |
27 | const ProjectCard = ({ _id, status, title, description }: ProjectCardProps) => {
28 | return (
29 |
30 |
31 |
32 |
33 |
34 | {title}
35 | {description && (
36 |
37 | {description}
38 |
39 | )}
40 |
41 |
42 |
43 |
48 |
49 | Toggle menu
50 |
51 |
52 |
53 | View Project
54 |
55 | Settings
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | );
66 | };
67 |
68 | export default ProjectCard;
69 |
--------------------------------------------------------------------------------
/apps/web/components/projects/project-list.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import NoProject from "@/components/empty-states/no-projects";
4 | import { Button } from "@/components/ui/button";
5 | import { api } from "@repo/backend/convex/_generated/api";
6 | import useApiMutation from "@/lib/hooks/use-api-mutation";
7 | import { useOrganization } from "@clerk/nextjs";
8 | import { useQuery } from "convex/react";
9 | import { Plus } from "lucide-react";
10 | import InputModal from "../modals/input-modal";
11 | import ProjectCard from "./project-card";
12 |
13 | type ProjectListProps = {
14 | orgId: string;
15 | };
16 |
17 | const ProjectList = ({ orgId }: ProjectListProps) => {
18 | const projects = useQuery(api.project.list, { orgId });
19 |
20 | if (projects?.error) {
21 | console.error("[PROJECT_FETCH_ERROR]", projects.error);
22 | }
23 |
24 | if (!projects?.data?.length) return ;
25 |
26 | return (
27 |
28 | {projects.data.map((project) => (
29 |
30 | ))}
31 |
32 |
33 | );
34 | };
35 |
36 | const AddProject = () => {
37 | const { organization } = useOrganization();
38 | const { mutate: createProject, isPending } = useApiMutation(
39 | api.project.create
40 | );
41 |
42 | const handleCreateProject = (title?: string, description?: string) => {
43 | if (!organization) throw new Error("Organization not found");
44 |
45 | return createProject({
46 | orgId: organization.id,
47 | title: title ?? "New Project",
48 | description: description ?? "Planning to do something awesome!",
49 | status: "development",
50 | });
51 | };
52 |
53 | return (
54 |
61 |
65 |
66 | Add Project
67 |
68 |
69 | );
70 | };
71 |
72 | export default ProjectList;
73 |
--------------------------------------------------------------------------------
/apps/web/lib/hooks/use-files-uploads.ts:
--------------------------------------------------------------------------------
1 | // This snippet is from https://github.com/get-convex/uploadstuff/blob/main/lib/useUploadFiles.ts
2 |
3 |
4 | /* eslint-disable no-unused-vars */
5 | import { useRef, useState } from "react";
6 | import { useEvent } from "./use-event";
7 | import { UploadFileResponse, uploadFiles } from "../upload-files";
8 |
9 | export const useUploadFiles = (
10 | uploadUrl: string | (() => Promise),
11 | opts?: {
12 | onUploadComplete?: (res: UploadFileResponse[]) => Promise;
13 | onUploadProgress?: (p: number) => void;
14 | onUploadError?: (e: unknown) => void;
15 | onUploadBegin?: (fileName: string) => void;
16 | }
17 | ): {
18 | startUpload: (files: File[]) => Promise;
19 | isUploading: boolean;
20 | } => {
21 | const [isUploading, setUploading] = useState(false);
22 | const uploadProgress = useRef(0);
23 | const fileProgress = useRef>(new Map());
24 |
25 | const startUpload = useEvent(async (files: File[]) => {
26 | setUploading(true);
27 |
28 | try {
29 | const url = typeof uploadUrl === "string" ? uploadUrl : await uploadUrl();
30 | const res = await uploadFiles({
31 | files,
32 | url,
33 | onUploadProgress: ({ file, progress }) => {
34 | if (opts?.onUploadProgress == null) {
35 | return;
36 | }
37 | fileProgress.current.set(file, progress);
38 | let sum = 0;
39 | fileProgress.current.forEach((singleFileProgress) => {
40 | sum += singleFileProgress;
41 | });
42 | const averageProgress =
43 | Math.floor(sum / fileProgress.current.size / 10) * 10;
44 | if (averageProgress !== uploadProgress.current) {
45 | opts?.onUploadProgress?.(averageProgress);
46 | uploadProgress.current = averageProgress;
47 | }
48 | },
49 | onUploadBegin({ file }) {
50 | opts?.onUploadBegin?.(file);
51 | },
52 | });
53 |
54 | await opts?.onUploadComplete?.(res);
55 | return res;
56 | } catch (error) {
57 | opts?.onUploadError?.(error);
58 | return [];
59 | } finally {
60 | setUploading(false);
61 | fileProgress.current = new Map();
62 | uploadProgress.current = 0;
63 | }
64 | });
65 |
66 | return {
67 | startUpload,
68 | isUploading,
69 | };
70 | };
--------------------------------------------------------------------------------
/packages/backend/convex/work_item.ts:
--------------------------------------------------------------------------------
1 | import { v } from "convex/values";
2 | import { mutation, query } from "./_generated/server";
3 | import { TaskPriority, TaskStatus, TaskType } from "./types";
4 |
5 | export const create = mutation({
6 | args: {
7 | title: v.string(),
8 | description: v.optional(v.string()),
9 | assignee: v.string(),
10 | assigneeId: v.id("users"),
11 | label: TaskType,
12 | priority: TaskPriority,
13 | status: TaskStatus,
14 | projectId: v.id("projects"),
15 | },
16 | handler: async (ctx, args) => {
17 | const identity = await ctx.auth.getUserIdentity();
18 | if (!identity) {
19 | throw new Error("Unauthorized");
20 | }
21 |
22 | return await ctx.db.insert("workItems", args);
23 | },
24 | });
25 |
26 | export const update = mutation({
27 | args: {
28 | _id: v.id("workItems"),
29 | title: v.string(),
30 | description: v.optional(v.string()),
31 | assignee: v.string(),
32 | assigneeId: v.id("users"),
33 | label: TaskType,
34 | priority: TaskPriority,
35 | status: TaskStatus,
36 | projectId: v.id("projects"),
37 | },
38 | handler: async (ctx, args) => {
39 | const identity = await ctx.auth.getUserIdentity();
40 | if (!identity) {
41 | throw new Error("Unauthorized");
42 | }
43 |
44 | await ctx.db.patch(args._id, args);
45 |
46 | return args._id;
47 | },
48 | });
49 |
50 | export const remove = mutation({
51 | args: {
52 | _id: v.id("workItems"),
53 | },
54 | handler: async (ctx, args) => await ctx.db.delete(args._id),
55 | });
56 |
57 | export const list = query({
58 | args: {
59 | projectId: v.id("projects"),
60 | assigneeId: v.optional(v.id("users")),
61 | ignoreCompleted: v.optional(v.boolean()),
62 | },
63 | handler: async (ctx, args) => {
64 | if (!args.projectId) throw new Error("projectId is required");
65 |
66 | let workItems = ctx.db
67 | .query("workItems")
68 | .withIndex("by_project", (q) => q.eq("projectId", args.projectId));
69 |
70 | if (args.assigneeId)
71 | workItems = workItems.filter((q) =>
72 | q.eq(q.field("assigneeId"), args.assigneeId)
73 | );
74 |
75 | if (args.ignoreCompleted)
76 | workItems = workItems.filter((q) => q.and(q.neq(q.field("status"), "canceled"), q.neq(q.field("status"), "done")));
77 |
78 | console.log("[WORK_ITEM_LIST_OPS] : Listing work items", args);
79 |
80 | return workItems.collect();
81 | },
82 | });
83 |
--------------------------------------------------------------------------------
/apps/web/components/work-items/data-table-column-header.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ArrowDownIcon,
3 | ArrowUpIcon,
4 | ChevronsUpDown,
5 | EyeOff,
6 | } from "lucide-react";
7 | import { Column } from "@tanstack/react-table";
8 |
9 | import { cn } from "@/lib/utils";
10 | import { Button } from "@/components/ui/button";
11 | import {
12 | DropdownMenu,
13 | DropdownMenuContent,
14 | DropdownMenuItem,
15 | DropdownMenuSeparator,
16 | DropdownMenuTrigger,
17 | } from "@/components/ui/dropdown-menu";
18 |
19 | interface DataTableColumnHeaderProps
20 | extends React.HTMLAttributes {
21 | column: Column;
22 | title: string;
23 | }
24 |
25 | export function DataTableColumnHeader({
26 | column,
27 | title,
28 | className,
29 | }: DataTableColumnHeaderProps) {
30 | if (!column.getCanSort()) {
31 | return {title}
;
32 | }
33 |
34 | return (
35 |
36 |
37 |
38 |
43 | {title}
44 | {column.getIsSorted() === "desc" ? (
45 |
46 | ) : column.getIsSorted() === "asc" ? (
47 |
48 | ) : (
49 |
50 | )}
51 |
52 |
53 |
54 | column.toggleSorting(false)}>
55 |
56 | Asc
57 |
58 | column.toggleSorting(true)}>
59 |
60 | Desc
61 |
62 |
63 | column.toggleVisibility(false)}>
64 |
65 | Hide
66 |
67 |
68 |
69 |
70 | );
71 | }
--------------------------------------------------------------------------------
/apps/web/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | module.exports = {
3 | env: {
4 | UNASSIGNED_USER_ID: process.env.UNASSIGNED_USER_ID,
5 | },
6 | images: {
7 | dangerouslyAllowSVG: true,
8 | remotePatterns: [
9 | {
10 | hostname: "img.clerk.com",
11 | protocol: "https",
12 | },
13 | ],
14 | },
15 | redirects: async () => [
16 | {
17 | source: "/projects",
18 | destination: "/dashboard",
19 | permanent: true,
20 | },
21 | {
22 | source: "/projects/:id",
23 | destination: "/projects/:id/dashboard",
24 | permanent: true,
25 | },
26 | ],
27 | rewrites: async () => [
28 | {
29 | source: "/changelog",
30 | destination: "/changelog/j5775aqxkjg4avhhdjgwn05jqh6rkcyp"
31 | }
32 | ]
33 | };
34 |
35 |
36 | // Injected content via Sentry wizard below
37 |
38 | const { withSentryConfig } = require("@sentry/nextjs");
39 |
40 | module.exports = withSentryConfig(
41 | module.exports,
42 | {
43 | // For all available options, see:
44 | // https://github.com/getsentry/sentry-webpack-plugin#options
45 |
46 | org: "vigneshfixes",
47 | project: "projectify",
48 | sentryUrl: "https://sentry.io/",
49 |
50 | // Only print logs for uploading source maps in CI
51 | silent: !process.env.CI,
52 |
53 | // For all available options, see:
54 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
55 |
56 | // Upload a larger set of source maps for prettier stack traces (increases build time)
57 | widenClientFileUpload: true,
58 |
59 | // Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
60 | // This can increase your server load as well as your hosting bill.
61 | // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
62 | // side errors will fail.
63 | // tunnelRoute: "/monitoring",
64 |
65 | // Hides source maps from generated client bundles
66 | hideSourceMaps: true,
67 |
68 | // Automatically tree-shake Sentry logger statements to reduce bundle size
69 | disableLogger: true,
70 |
71 | // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
72 | // See the following for more information:
73 | // https://docs.sentry.io/product/crons/
74 | // https://vercel.com/docs/cron-jobs
75 | automaticVercelMonitors: true,
76 | }
77 | );
78 |
--------------------------------------------------------------------------------
/apps/web/components/modals/input-modal.tsx:
--------------------------------------------------------------------------------
1 | import { ChangeEvent, ReactNode, useState } from "react";
2 |
3 | import { Button, buttonVariants } from "@/components/ui/button";
4 | import { Input } from "@/components/ui/input";
5 | import { Textarea } from "@/components/ui/textarea";
6 | import { toast } from "sonner";
7 | import ResponsiveModel, {
8 | ResponsiveModelDescription,
9 | ResponsiveModelTitle,
10 | } from "../responsive-model";
11 |
12 | type InputModalProps = {
13 | children: ReactNode;
14 | disabled?: boolean;
15 | // eslint-disable-next-line no-unused-vars
16 | onConfirm: (title?: string, description?: string) => Promise;
17 | header: string;
18 | description?: string;
19 | toastMessage?: string;
20 | };
21 |
22 | const InputModal = ({
23 | children,
24 | header,
25 | onConfirm,
26 | description,
27 | disabled,
28 | toastMessage,
29 | }: InputModalProps) => {
30 | const [formValue, setFormValue] = useState({
31 | title: "New Project",
32 | description: "Planning to do something awesome!",
33 | });
34 | const [isOpen, setIsOpen] = useState(false);
35 |
36 | const handleConfirm = () => {
37 | onConfirm(formValue.title, formValue.description)
38 | .then(() => {
39 | toast.success(toastMessage);
40 | })
41 | .catch(() => {
42 | toast.error("Failed to perform action. Please try again.");
43 | })
44 | .finally(() => setIsOpen(false));
45 | };
46 |
47 | const handleInputChange = (
48 | e: ChangeEvent
49 | ) => {
50 | setFormValue({ ...formValue, [e.target.name]: e.target.value });
51 | };
52 |
53 | return (
54 | setIsOpen(true)}
59 | onOpenChange={setIsOpen}
60 | >
61 | {header}
62 | {description}
63 |
68 |
69 |
74 |
79 | Confirm
80 |
81 |
82 | );
83 | };
84 |
85 | export default InputModal;
86 |
--------------------------------------------------------------------------------
/apps/web/components/resources/file/file-card.tsx:
--------------------------------------------------------------------------------
1 | import ConfirmModal from "@/components/modals/confirm-modal";
2 | import { Button } from "@/components/ui/button";
3 | import { api } from "@repo/backend/convex/_generated/api";
4 | import type { Id } from "@repo/backend/convex/_generated/dataModel";
5 | import useApiMutation from "@/lib/hooks/use-api-mutation";
6 | import { useFileModal } from "@/lib/store/use-file-modal";
7 | import { Edit, Trash } from "lucide-react";
8 | import Link from "next/link";
9 | import FileIcon from "./file-icon";
10 |
11 | type FileCardProps = {
12 | resource: {
13 | _id: Id<"files">;
14 | title: string;
15 | storageId: Id<"_storage">;
16 | projectId: Id<"projects">;
17 | type: string;
18 | };
19 | };
20 |
21 | const FileCard = ({
22 | resource: { _id, title, storageId, type },
23 | }: FileCardProps) => {
24 | const { onOpen } = useFileModal();
25 |
26 | const { mutate: deleteFile, isPending: isDeleting } = useApiMutation(
27 | api.resources.file.remove
28 | );
29 |
30 | return (
31 |
35 |
36 |
37 |
38 |
47 | {title}
48 |
49 |
50 |
12.3 MB
51 |
52 |
53 | onOpen({ _id, title })}
57 | disabled={isDeleting}
58 | >
59 |
60 |
61 | deleteFile({ _id })}
64 | disabled={isDeleting}
65 | toastMessage="File deleted successfully"
66 | >
67 |
68 |
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default FileCard;
77 |
--------------------------------------------------------------------------------
/apps/web/lib/hooks/use-workItem-table.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | ColumnDef,
5 | ColumnFiltersState,
6 | SortingState,
7 | VisibilityState,
8 | getCoreRowModel,
9 | getFilteredRowModel,
10 | getPaginationRowModel,
11 | getSortedRowModel,
12 | useReactTable,
13 | } from "@tanstack/react-table";
14 | import { useRouter, useSearchParams } from "next/navigation";
15 | import qs from "query-string";
16 | import { useEffect, useState } from "react";
17 |
18 | export const useWorkItemTable = (
19 | columns: ColumnDef[],
20 | data: TData[]
21 | ) => {
22 | const router = useRouter();
23 | const searchParams = useSearchParams();
24 |
25 | const [sorting, setSorting] = useState([]);
26 | const [columnFilters, setColumnFilters] = useState([]);
27 | const [columnVisibility, setColumnVisibility] = useState({});
28 | const [rowSelection, setRowSelection] = useState({});
29 |
30 | // Update URL query params when columnFilters change
31 | useEffect(() => {
32 | const filters = {
33 | status: columnFilters.find((filter) => filter.id === "status")?.value,
34 | assignee: columnFilters.find((filter) => filter.id === "assignee")?.value,
35 | priority: columnFilters.find((filter) => filter.id === "priority")?.value,
36 | };
37 |
38 | const query = qs.stringify(filters, {
39 | skipEmptyString: true,
40 | skipNull: true,
41 | });
42 |
43 | if (query) router.push(`?${query}`);
44 | }, [columnFilters, router]);
45 |
46 | // Update columnFilters on initial load
47 | useEffect(() => {
48 | const query = qs.parse(searchParams.toString());
49 |
50 | const newColumnFilters: ColumnFiltersState = Object.entries(query).map(
51 | ([key, value]) => ({
52 | id: key,
53 | value: typeof value === "string" ? [value] : value,
54 | })
55 | );
56 |
57 | setColumnFilters(newColumnFilters);
58 | }, []);
59 |
60 | const table = useReactTable({
61 | data,
62 | columns,
63 | state: {
64 | sorting,
65 | columnFilters,
66 | columnVisibility,
67 | rowSelection,
68 | },
69 | onRowSelectionChange: setRowSelection,
70 | onColumnVisibilityChange: setColumnVisibility,
71 | onColumnFiltersChange: setColumnFilters,
72 | onSortingChange: setSorting,
73 | getCoreRowModel: getCoreRowModel(),
74 | getPaginationRowModel: getPaginationRowModel(),
75 | getSortedRowModel: getSortedRowModel(),
76 | getFilteredRowModel: getFilteredRowModel(),
77 | });
78 |
79 | return table;
80 | };
81 |
--------------------------------------------------------------------------------
/apps/web/components/resources/file/file-upload.tsx:
--------------------------------------------------------------------------------
1 | import { api } from "@repo/backend/convex/_generated/api";
2 | import type { Id } from "@repo/backend/convex/_generated/dataModel";
3 | import useApiMutation from "@/lib/hooks/use-api-mutation";
4 | import { Button } from "@/components/ui/button";
5 | import { MAX_FILE_COUNT, MAX_FILE_SIZE } from "@/lib/constants";
6 | import { useUploadFiles } from "@/lib/hooks/use-files-uploads";
7 | import { Upload } from "lucide-react";
8 | import { useParams } from "next/navigation";
9 | import { toast } from "sonner";
10 |
11 | type FileUploadProps = {
12 | fileCount?: number;
13 | };
14 |
15 | const FileUpload = ({ fileCount = 0 }: FileUploadProps) => {
16 | const params = useParams();
17 |
18 | const { mutate: generateUploadUrl, isPending } = useApiMutation(
19 | api.resources.storage.generateUploadUrl
20 | );
21 | const { mutate: createFileResource } = useApiMutation(
22 | api.resources.file.create
23 | );
24 | const { startUpload, isUploading } = useUploadFiles(generateUploadUrl);
25 |
26 | const handleAddFile = () => {
27 | const fileInput = document.createElement("input");
28 | fileInput.type = "file";
29 | fileInput.accept = "*/*";
30 |
31 | fileInput.addEventListener("change", async (e) => {
32 | if (fileCount >= MAX_FILE_COUNT) {
33 | toast.error(`A project can have a maximum of ${MAX_FILE_COUNT} files.`);
34 | return;
35 | }
36 |
37 | const file = (e.target as HTMLInputElement).files?.[0];
38 | if (!file) return;
39 |
40 | if (file.size > MAX_FILE_SIZE) {
41 | toast.error("File size should be less than 5MB");
42 | return;
43 | }
44 |
45 | try {
46 | const [res] = await startUpload([file]);
47 |
48 | if (!res) {
49 | toast.error("Failed to upload file. Please try again.");
50 | return;
51 | }
52 |
53 | createFileResource({
54 | title: res.name,
55 | storageId: (res.response as { storageId: Id<"_storage"> }).storageId,
56 | projectId: params.id as Id<"projects">,
57 | type: res.type,
58 | });
59 |
60 | toast.success("File uploaded successfully.");
61 | } catch (e) {
62 | toast.error("Failed to upload file. Please try again.");
63 | }
64 | });
65 |
66 | fileInput.click();
67 | };
68 |
69 | return (
70 |
75 |
76 | Upload
77 |
78 | );
79 | };
80 |
81 | export default FileUpload;
82 |
--------------------------------------------------------------------------------
/apps/web/components/resources/link/link-card.tsx:
--------------------------------------------------------------------------------
1 | import ConfirmModal from "@/components/modals/confirm-modal";
2 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
3 | import { Button } from "@/components/ui/button";
4 | import { api } from "@repo/backend/convex/_generated/api";
5 | import type { Id } from "@repo/backend/convex/_generated/dataModel";
6 | import useApiMutation from "@/lib/hooks/use-api-mutation";
7 | import { useLinkModal } from "@/lib/store/use-link-modal";
8 | import { Edit, LinkIcon, Trash } from "lucide-react";
9 |
10 | type LinkCardProps = {
11 | resource: {
12 | _id: Id<"links">;
13 | title: string;
14 | url: string;
15 | projectId: Id<"projects">;
16 | icon?: Id<"_storage"> | undefined;
17 | };
18 | };
19 |
20 | const LinkCard = ({
21 | resource: { _id, title, url, icon },
22 | }: LinkCardProps) => {
23 | const { onOpen } = useLinkModal();
24 |
25 | const { mutate: deleteLink, isPending } = useApiMutation(
26 | api.resources.link.remove
27 | );
28 |
29 | return (
30 |
31 |
52 |
53 | onOpen({ _id, title, url })}
58 | >
59 |
60 |
61 | deleteLink({ _id })}
63 | header={`Delete link:${title}`}
64 | disabled={isPending}
65 | toastMessage="Link deleted successfully"
66 | >
67 |
68 |
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default LinkCard;
77 |
--------------------------------------------------------------------------------