├── .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 | ![DB Schema ER Diagram](https://raw.githubusercontent.com/vignesh-gupta/projectify/master/docs/assets/er-diagram.svg) -------------------------------------------------------------------------------- /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 | 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 |