├── .nvm ├── apps ├── dashboard │ ├── .gitignore │ ├── README.md │ ├── postcss.config.js │ ├── src │ │ ├── app │ │ │ ├── favicon.ico │ │ │ ├── (app) │ │ │ │ ├── account │ │ │ │ │ ├── teams │ │ │ │ │ │ ├── loading.tsx │ │ │ │ │ │ ├── layout.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── notifications │ │ │ │ │ │ └── page.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── settings │ │ │ │ │ ├── security │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── tags │ │ │ │ │ │ ├── layout.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── members │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ ├── pending │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ └── layout.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ ├── integrations │ │ │ │ │ │ ├── layout.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── layout.tsx │ │ │ │ ├── help │ │ │ │ │ └── layout.tsx │ │ │ │ ├── inbox │ │ │ │ │ ├── layout.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── layout.tsx │ │ │ ├── (public) │ │ │ │ ├── all-done │ │ │ │ │ ├── schema.ts │ │ │ │ │ ├── event-emitter.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── invite │ │ │ │ │ └── [inviteCode] │ │ │ │ │ │ ├── layout.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ └── login │ │ │ │ │ └── page.tsx │ │ │ ├── globals.css │ │ │ ├── not-found.tsx │ │ │ ├── api │ │ │ │ ├── trpc │ │ │ │ │ └── [trpc] │ │ │ │ │ │ └── route.ts │ │ │ │ ├── auth │ │ │ │ │ └── callback │ │ │ │ │ │ └── route.ts │ │ │ │ └── webhook │ │ │ │ │ └── new-user │ │ │ │ │ └── route.ts │ │ │ └── layout.tsx │ │ ├── utils │ │ │ ├── assertUnreachable.ts │ │ │ ├── validation │ │ │ │ └── common.ts │ │ │ ├── pluralize.ts │ │ │ ├── shortId.ts │ │ │ ├── sentencifyArray.tsx │ │ │ ├── insertIf.ts │ │ │ ├── get-role-name.ts │ │ │ ├── teamRoleEnumToWord.ts │ │ │ ├── random.ts │ │ │ ├── colors.ts │ │ │ ├── stripMarkupfromMessage.ts │ │ │ ├── parseIncomingMessageWithAI.ts │ │ │ └── parseIncomingMessage.ts │ │ ├── components │ │ │ ├── no-ticket-selected.tsx │ │ │ ├── page-wrapper.tsx │ │ │ ├── theme-provider.tsx │ │ │ ├── sheets │ │ │ │ └── index.tsx │ │ │ ├── create-seventy-seven-ticket-button.tsx │ │ │ ├── ticket-info-button.tsx │ │ │ ├── create-team-button.tsx │ │ │ ├── analytics-set-profile.tsx │ │ │ ├── invite-team-member-button.tsx │ │ │ ├── clear-all-filters-button.tsx │ │ │ ├── create-tag-button.tsx │ │ │ ├── modals │ │ │ │ ├── snooze-ticket-modal.tsx │ │ │ │ ├── assign-ticket-modal.tsx │ │ │ │ ├── create-team-modal.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── create-ticket-tag-modal.tsx │ │ │ │ ├── view-original-message-content-modal.tsx.tsx │ │ │ │ ├── edit-ticket-tag-modal.tsx │ │ │ │ └── invite-team-member-modal.tsx │ │ │ ├── edit-ticket-tag-button.tsx │ │ │ ├── ticket-filters │ │ │ │ └── use-ticket-filters.ts │ │ │ ├── accept-invitation-button.tsx │ │ │ ├── forms │ │ │ │ ├── hooks │ │ │ │ │ └── use-team-form.ts │ │ │ │ ├── create-team-form.tsx │ │ │ │ └── ticket-search-form.tsx │ │ │ ├── header.tsx │ │ │ ├── alerts │ │ │ │ ├── prohibit-last-owner-role-change-alert.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── confirm-remove-team-member-alert.tsx │ │ │ │ ├── confirm-leave-team-alert.tsx │ │ │ │ └── confirm-change-your-own-role.tsx │ │ │ ├── sign-in-buttons │ │ │ │ ├── sign-in-with-github-button.tsx │ │ │ │ └── sign-in-with-google-button.tsx │ │ │ ├── revoke-slack-integration-button.tsx │ │ │ ├── avatar.tsx │ │ │ ├── ticket-chat-header.tsx │ │ │ ├── team-list-item.tsx │ │ │ ├── ticket-tags-table.tsx │ │ │ ├── clipboard-button.tsx │ │ │ ├── invite-code-badge.tsx │ │ │ ├── account-sub-nav.tsx │ │ │ ├── add-slack-integration-button.tsx │ │ │ ├── chat-message-handler.tsx │ │ │ ├── theme-switch.tsx │ │ │ ├── main-menu.tsx │ │ │ ├── members-list-tab-nav.tsx │ │ │ ├── settings-sub-nav.tsx │ │ │ ├── pending-member-dropdown.tsx │ │ │ ├── team-actions-dropdown.tsx │ │ │ ├── team-actions-menu.tsx │ │ │ └── chat-message-user.tsx │ │ ├── store.ts │ │ ├── hooks │ │ │ ├── use-selected-ticket.ts │ │ │ ├── use-upload.ts │ │ │ └── use-realtime-query.ts │ │ ├── trpc │ │ │ ├── query-client.ts │ │ │ ├── server.ts │ │ │ ├── routers │ │ │ │ ├── _app.ts │ │ │ │ └── seventy-seven-router.ts │ │ │ ├── init.ts │ │ │ └── client.tsx │ │ ├── lib │ │ │ ├── analytics.tsx │ │ │ └── search-params.ts │ │ └── middleware.ts │ ├── next-env.d.ts │ ├── tailwind.config.ts │ ├── tsconfig.json │ ├── next.config.mjs │ ├── public │ │ ├── vercel.svg │ │ ├── circles.svg │ │ └── next.svg │ └── trigger.config.ts ├── supabase │ ├── supabase │ │ ├── seed.sql │ │ ├── .gitignore │ │ └── functions │ │ │ └── .vscode │ │ │ ├── extensions.json │ │ │ └── settings.json │ ├── tsconfig.json │ ├── supabase.code-workspace │ └── package.json └── website │ ├── README.md │ ├── postcss.config.js │ ├── public │ ├── email │ │ ├── acme.png │ │ ├── avatar.jpg │ │ └── 77-logo.png │ └── img │ │ ├── 77-dark.png │ │ ├── 77-dark.webp │ │ ├── 77-light.png │ │ └── 77-light.webp │ ├── src │ ├── app │ │ ├── favicon.ico │ │ ├── opengraph-image.jpg │ │ ├── not-found.tsx │ │ ├── globals.css │ │ └── page.tsx │ ├── utils │ │ ├── safe-action.ts │ │ └── analytics.ts │ ├── store.ts │ ├── components │ │ ├── theme-provider.tsx │ │ ├── container.tsx │ │ ├── hero-heading.tsx │ │ ├── confetti-rain.tsx │ │ ├── change-theme-button.tsx │ │ └── header.tsx │ └── actions │ │ └── waitlist.ts │ ├── next.config.mjs │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── package.json ├── .npmrc ├── packages ├── ui │ ├── src │ │ ├── components │ │ │ ├── color-picker.tsx │ │ │ ├── shadcn │ │ │ │ ├── skeleton.tsx │ │ │ │ ├── spinner.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── textarea.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ ├── time-picker │ │ │ │ │ ├── date-time-picker.tsx │ │ │ │ │ └── time-picker-inputs.tsx │ │ │ │ ├── badge.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── switch.tsx │ │ │ │ ├── tooltip.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── toggle.tsx │ │ │ │ ├── toggle-group.tsx │ │ │ │ ├── card.tsx │ │ │ │ └── tabs.tsx │ │ │ ├── alert.tsx │ │ │ ├── sheet.tsx │ │ │ ├── combobox.tsx │ │ │ ├── modal.tsx │ │ │ ├── logo.tsx │ │ │ ├── code-block.tsx │ │ │ └── modal-responsive.tsx │ │ └── utils.ts │ ├── postcss.config.js │ ├── tsconfig.json │ └── components.json ├── orm │ ├── src │ │ ├── prisma │ │ │ ├── enums.ts │ │ │ └── extensions │ │ │ │ └── loggingExtension.ts │ │ └── prisma.ts │ ├── tsconfig.json │ └── package.json ├── sdk │ ├── README.md │ ├── tsconfig.json │ ├── package.json │ └── src │ │ └── index.ts ├── integrations │ ├── README.md │ ├── tsconfig.json │ ├── package.json │ └── src │ │ └── slack │ │ └── index.ts ├── supabase │ ├── tsconfig.json │ ├── src │ │ ├── session.ts │ │ ├── clients │ │ │ ├── client.ts │ │ │ ├── middleware.ts │ │ │ └── server.ts │ │ └── types │ │ │ └── index.ts │ └── package.json ├── email │ ├── tsconfig.json │ ├── types.ts │ ├── index.ts │ ├── components │ │ └── footer.tsx │ └── package.json ├── analytics │ ├── tsconfig.json │ ├── package.json │ └── src │ │ └── index.tsx └── typescript-config │ ├── react-library.json │ ├── package.json │ ├── nextjs.json │ └── base.json ├── pnpm-workspace.yaml ├── tsconfig.json ├── turbo.json ├── .gitignore ├── package.json ├── .vscode └── settings.json ├── biome.json └── README.md /.nvm: -------------------------------------------------------------------------------- 1 | 20.11.0 -------------------------------------------------------------------------------- /apps/dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | .trigger -------------------------------------------------------------------------------- /apps/supabase/supabase/seed.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*prisma* -------------------------------------------------------------------------------- /apps/dashboard/README.md: -------------------------------------------------------------------------------- 1 | # 77 Dashboard 2 | -------------------------------------------------------------------------------- /apps/website/README.md: -------------------------------------------------------------------------------- 1 | # 77 Website 2 | -------------------------------------------------------------------------------- /packages/ui/src/components/color-picker.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'apps/*' -------------------------------------------------------------------------------- /apps/supabase/supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | -------------------------------------------------------------------------------- /packages/orm/src/prisma/enums.ts: -------------------------------------------------------------------------------- 1 | export { TEAM_ROLE_ENUM } from '@prisma/client' 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@seventy-seven/typescript-config/base.json" 3 | } 4 | -------------------------------------------------------------------------------- /apps/website/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@seventy-seven/ui/postcss.config.js') 2 | -------------------------------------------------------------------------------- /apps/dashboard/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@seventy-seven/ui/postcss.config.js') 2 | -------------------------------------------------------------------------------- /apps/supabase/supabase/functions/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["denoland.vscode-deno"] 3 | } 4 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christianalares/seventy-seven/HEAD/apps/dashboard/src/app/favicon.ico -------------------------------------------------------------------------------- /apps/website/public/email/acme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christianalares/seventy-seven/HEAD/apps/website/public/email/acme.png -------------------------------------------------------------------------------- /apps/website/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christianalares/seventy-seven/HEAD/apps/website/src/app/favicon.ico -------------------------------------------------------------------------------- /apps/website/public/email/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christianalares/seventy-seven/HEAD/apps/website/public/email/avatar.jpg -------------------------------------------------------------------------------- /apps/website/public/img/77-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christianalares/seventy-seven/HEAD/apps/website/public/img/77-dark.png -------------------------------------------------------------------------------- /apps/website/public/img/77-dark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christianalares/seventy-seven/HEAD/apps/website/public/img/77-dark.webp -------------------------------------------------------------------------------- /apps/website/public/img/77-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christianalares/seventy-seven/HEAD/apps/website/public/img/77-light.png -------------------------------------------------------------------------------- /packages/sdk/README.md: -------------------------------------------------------------------------------- 1 | # @seventy-seven/sdk 2 | 3 | TypeScript SDK for managing tickets with [Seventy Seven](https://seventy-seven.dev). -------------------------------------------------------------------------------- /packages/ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /apps/website/public/email/77-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christianalares/seventy-seven/HEAD/apps/website/public/email/77-logo.png -------------------------------------------------------------------------------- /apps/website/public/img/77-light.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christianalares/seventy-seven/HEAD/apps/website/public/img/77-light.webp -------------------------------------------------------------------------------- /apps/website/src/app/opengraph-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christianalares/seventy-seven/HEAD/apps/website/src/app/opengraph-image.jpg -------------------------------------------------------------------------------- /packages/integrations/README.md: -------------------------------------------------------------------------------- 1 | # @seventy-seven/sdk 2 | 3 | TypeScript SDK for managing tickets with [Seventy Seven](https://seventy-seven.dev). -------------------------------------------------------------------------------- /packages/orm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@seventy-seven/typescript-config/base.json", 3 | "include": ["src"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /apps/dashboard/src/utils/assertUnreachable.ts: -------------------------------------------------------------------------------- 1 | export const assertUnreachable = (x: never): never => { 2 | throw new Error("Didn't expect to get here", x) 3 | } 4 | -------------------------------------------------------------------------------- /apps/supabase/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@seventy-seven/typescript-config/base.json", 3 | "include": ["**/*.ts"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/supabase/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@seventy-seven/typescript-config/base.json", 3 | "include": ["src"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/account/teams/loading.tsx: -------------------------------------------------------------------------------- 1 | const TeamsLoadingPage = () => { 2 | return
Loading teams
3 | } 4 | 5 | export default TeamsLoadingPage 6 | -------------------------------------------------------------------------------- /apps/supabase/supabase/functions/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "editor.defaultFormatter": "denoland.vscode-deno" 5 | } 6 | -------------------------------------------------------------------------------- /apps/dashboard/src/utils/validation/common.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const imageTypeSchema = z.union([z.literal('image/jpeg'), z.literal('image/png'), z.literal('image/webp')]) 4 | -------------------------------------------------------------------------------- /packages/ui/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /packages/email/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@seventy-seven/typescript-config/react-library.json", 3 | "include": ["src", "index.ts", "emails/waitlist-confirmation.tsx"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /apps/dashboard/src/utils/pluralize.ts: -------------------------------------------------------------------------------- 1 | export const pluralize = (nr: number, singular: string, plural: string) => { 2 | if (nr === 1) { 3 | return `${nr} ${singular}` 4 | } 5 | 6 | return `${nr} ${plural}` 7 | } 8 | -------------------------------------------------------------------------------- /apps/dashboard/src/utils/shortId.ts: -------------------------------------------------------------------------------- 1 | import { customAlphabet } from 'nanoid' 2 | 3 | const nanoid = customAlphabet('abcdefghijklmnopqrstuwxyz0123456789', 10) 4 | 5 | export const shortId = () => { 6 | return nanoid() 7 | } 8 | -------------------------------------------------------------------------------- /packages/sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@seventy-seven/typescript-config/base.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src"], 7 | "exclude": ["dist", "node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/analytics/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@seventy-seven/typescript-config/nextjs.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src"], 7 | "exclude": ["dist", "node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/integrations/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@seventy-seven/typescript-config/base.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src"], 7 | "exclude": ["dist", "node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/typescript-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@seventy-seven/typescript-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "publishConfig": { 7 | "access": "public" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/dashboard/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(public)/all-done/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const searchParamsSchema = z.object({ 4 | event: z.literal('slack_oauth_completed'), 5 | }) 6 | 7 | export type WindowEvent = z.infer['event'] 8 | -------------------------------------------------------------------------------- /apps/website/src/utils/safe-action.ts: -------------------------------------------------------------------------------- 1 | import { createSafeActionClient } from 'next-safe-action' 2 | 3 | export const action = createSafeActionClient({ 4 | handleReturnedServerError: (e) => { 5 | return e.message || 'Oh no, something went wrong!' 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /apps/website/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import("next").NextConfig} */ 2 | const config = { 3 | transpilePackages: ['@seventy-seven/ui'], 4 | reactStrictMode: true, 5 | typescript: { 6 | ignoreBuildErrors: true, 7 | }, 8 | } 9 | 10 | export default config 11 | -------------------------------------------------------------------------------- /packages/supabase/src/session.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from './clients/server' 2 | 3 | export const getUser = async () => { 4 | const sb = createClient() 5 | 6 | const { 7 | data: { user }, 8 | } = await sb.auth.getUser() 9 | 10 | return user 11 | } 12 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/no-ticket-selected.tsx: -------------------------------------------------------------------------------- 1 | export const NoTicketSelected = () => { 2 | return ( 3 |
4 |

No ticket selected

5 |
6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@seventy-seven/typescript-config/react-library.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["./src/*"] 7 | } 8 | }, 9 | "include": ["src"], 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/ui/src/components/shadcn/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '../../utils' 2 | 3 | function Skeleton({ className, ...props }: React.HTMLAttributes) { 4 | return
5 | } 6 | 7 | export { Skeleton } 8 | -------------------------------------------------------------------------------- /packages/ui/src/components/shadcn/spinner.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '../../utils' 2 | import { Icon } from './icon' 3 | 4 | type Props = { 5 | className?: string 6 | } 7 | 8 | export const Spinner = ({ className }: Props) => { 9 | return 10 | } 11 | -------------------------------------------------------------------------------- /apps/website/src/store.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | 3 | type Store = { 4 | showConfetti: boolean 5 | setShowConfetti: (open: boolean) => void 6 | } 7 | 8 | export const useStore = create((set) => ({ 9 | showConfetti: false, 10 | setShowConfetti: (showConfetti) => set({ showConfetti }), 11 | })) 12 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | pre::-webkit-scrollbar { 7 | display: none; 8 | } 9 | 10 | * { 11 | @apply border-border; 12 | } 13 | 14 | body { 15 | @apply bg-background text-foreground font-sans; 16 | } 17 | } -------------------------------------------------------------------------------- /apps/dashboard/src/components/page-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@seventy-seven/ui/utils' 2 | 3 | type Props = { 4 | children: React.ReactNode 5 | className?: string 6 | } 7 | 8 | export const PageWrapper = ({ children, className }: Props) => { 9 | return
{children}
10 | } 11 | -------------------------------------------------------------------------------- /apps/dashboard/src/utils/sentencifyArray.tsx: -------------------------------------------------------------------------------- 1 | export const sentencifyArray = (items: string[]) => { 2 | if (items.length === 0) { 3 | return '' 4 | } 5 | 6 | if (items.length === 1) { 7 | return items[0] ?? '' 8 | } 9 | 10 | const lastItem = items.pop() 11 | return `${items.join(', ')} and ${lastItem}` 12 | } 13 | -------------------------------------------------------------------------------- /apps/website/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | import baseConfig from '@seventy-seven/ui/tailwind.config.ts' 4 | 5 | const config = { 6 | content: ['./src/**/*.{ts,tsx}', '../../packages/ui/src/**/*.{ts,tsx}'], 7 | presets: [baseConfig], 8 | } satisfies Config 9 | 10 | export default config 11 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | const NotFound = () => { 2 | return ( 3 |
4 |

404

5 |

This page could not be found 🥺

6 |
7 | ) 8 | } 9 | 10 | export default NotFound 11 | -------------------------------------------------------------------------------- /apps/dashboard/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | import baseConfig from '@seventy-seven/ui/tailwind.config.ts' 4 | 5 | const config = { 6 | content: ['./src/**/*.{ts,tsx}', '../../packages/ui/src/**/*.{ts,tsx}'], 7 | presets: [baseConfig], 8 | } satisfies Config 9 | 10 | export default config 11 | -------------------------------------------------------------------------------- /apps/website/src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | const NotFound = () => { 2 | return ( 3 |
4 |

404

5 |

This page could not be found 🥺

6 |
7 | ) 8 | } 9 | 10 | export default NotFound 11 | -------------------------------------------------------------------------------- /apps/dashboard/src/utils/insertIf.ts: -------------------------------------------------------------------------------- 1 | export const insertIf = { 2 | array: (...args: [condition: boolean, ...rest: T]) => { 3 | const [condition, ...rest] = args 4 | return condition ? rest : [] 5 | }, 6 | object: >(condition: boolean, obj: T) => { 7 | return condition ? obj : {} 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ThemeProvider as NextThemesProvider } from 'next-themes' 4 | import type { ThemeProviderProps } from 'next-themes/dist/types' 5 | 6 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 7 | return {children} 8 | } 9 | -------------------------------------------------------------------------------- /apps/supabase/supabase.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "project-root", 5 | "path": "./" 6 | }, 7 | { 8 | "name": "supabase-functions", 9 | "path": "supabase/functions" 10 | } 11 | ], 12 | "settings": { 13 | "files.exclude": { 14 | "supabase/functions/": true 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/website/src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ThemeProvider as NextThemesProvider } from 'next-themes' 4 | import type { ThemeProviderProps } from 'next-themes/dist/types' 5 | 6 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 7 | return {children} 8 | } 9 | -------------------------------------------------------------------------------- /apps/dashboard/src/store.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | 3 | interface SheetStore { 4 | isOpen: boolean 5 | open: () => void 6 | close: () => void 7 | } 8 | 9 | export const useMessagesListSheetStore = create()((set) => ({ 10 | isOpen: false, 11 | open: () => set({ isOpen: true }), 12 | close: () => set({ isOpen: false }), 13 | })) 14 | -------------------------------------------------------------------------------- /apps/dashboard/src/hooks/use-selected-ticket.ts: -------------------------------------------------------------------------------- 1 | import { ticketIdParsers } from '@/lib/search-params' 2 | import { useQueryStates } from 'nuqs' 3 | 4 | export const useSelectedTicket = () => { 5 | const [ticketId, setTicketId] = useQueryStates(ticketIdParsers, { 6 | shallow: false, 7 | history: 'push', 8 | }) 9 | 10 | return { ticketId, setTicketId } 11 | } 12 | -------------------------------------------------------------------------------- /apps/dashboard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@seventy-seven/typescript-config/nextjs.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["./src/*"] 7 | } 8 | }, 9 | "include": ["next-env.d.ts", "next.config.js", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "trigger.config.ts"], 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@seventy-seven/typescript-config/nextjs.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["./src/*"] 7 | } 8 | }, 9 | "include": ["next-env.d.ts", "next.config.js", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "tailwind.config.ts"], 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/typescript-config/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "plugins": [{ "name": "next" }], 7 | "module": "ESNext", 8 | "moduleResolution": "Bundler", 9 | "allowJs": true, 10 | "jsx": "preserve", 11 | "noEmit": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/page.tsx: -------------------------------------------------------------------------------- 1 | import { trpc } from '@/trpc/server' 2 | import { redirect } from 'next/navigation' 3 | 4 | export const dynamic = 'force-dynamic' 5 | 6 | const AuthorizedPage = async () => { 7 | const user = await trpc.users.maybeMe() 8 | 9 | if (user) { 10 | redirect('/inbox') 11 | } 12 | 13 | return null 14 | } 15 | 16 | export default AuthorizedPage 17 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/settings/security/page.tsx: -------------------------------------------------------------------------------- 1 | import { AuthToken } from '@/components/auth-token' 2 | import { HydrateClient, trpc } from '@/trpc/server' 3 | 4 | const SecurityPage = () => { 5 | trpc.users.myCurrentTeam.prefetch() 6 | 7 | return ( 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | export default SecurityPage 15 | -------------------------------------------------------------------------------- /apps/website/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --bg-shade-1: #404040; 8 | --bg-shade-2: #858585; 9 | --bg-shade-3: #d3d3d3; 10 | } 11 | 12 | * { 13 | @apply border-border; 14 | } 15 | 16 | body { 17 | @apply bg-background text-foreground font-sans; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(public)/invite/[inviteCode]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster } from '@seventy-seven/ui/sonner' 2 | 3 | type Props = { 4 | children: React.ReactNode 5 | } 6 | 7 | const InviteCodeLayout = ({ children }: Props) => { 8 | return ( 9 | <> 10 | 11 | {children} 12 | 13 | ) 14 | } 15 | 16 | export default InviteCodeLayout 17 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/sheets/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { createPushModal } from '@seventy-seven/ui/modal' 4 | import { TicketInfoSheet } from './ticket-info-sheet' 5 | 6 | export const { 7 | pushModal: pushSheet, 8 | popModal: popSheet, 9 | ModalProvider: SheetProvider, 10 | } = createPushModal({ 11 | modals: { 12 | ticketInfoSheet: TicketInfoSheet, 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /apps/website/src/components/container.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@seventy-seven/ui/utils' 2 | 3 | type Props = { 4 | children: React.ReactNode 5 | className?: string 6 | as?: keyof JSX.IntrinsicElements 7 | } 8 | 9 | export const Container = ({ children, as: As = 'div', className }: Props) => ( 10 | {children} 11 | ) 12 | -------------------------------------------------------------------------------- /apps/dashboard/src/utils/get-role-name.ts: -------------------------------------------------------------------------------- 1 | import type { TEAM_ROLE_ENUM } from '@seventy-seven/orm/enums' 2 | import { assertUnreachable } from './assertUnreachable' 3 | 4 | export const getRoleName = (role: TEAM_ROLE_ENUM) => { 5 | switch (role) { 6 | case 'MEMBER': 7 | return 'Member' 8 | case 'OWNER': 9 | return 'Owner' 10 | 11 | default: 12 | assertUnreachable(role) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/help/layout.tsx: -------------------------------------------------------------------------------- 1 | import { HydrateClient, trpc } from '@/trpc/server' 2 | 3 | type Props = { 4 | children: React.ReactNode 5 | } 6 | 7 | const HelpLayout = ({ children }: Props) => { 8 | trpc.users.me.prefetch() 9 | 10 | return ( 11 | 12 |
{children}
13 |
14 | ) 15 | } 16 | 17 | export default HelpLayout 18 | -------------------------------------------------------------------------------- /apps/dashboard/src/utils/teamRoleEnumToWord.ts: -------------------------------------------------------------------------------- 1 | import type { TEAM_ROLE_ENUM } from '@seventy-seven/orm/enums' 2 | import { assertUnreachable } from './assertUnreachable' 3 | 4 | export const teamRoleEnumToWord = (teamRole: TEAM_ROLE_ENUM) => { 5 | switch (teamRole) { 6 | case 'MEMBER': 7 | return 'Member' 8 | case 'OWNER': 9 | return 'Owner' 10 | default: 11 | assertUnreachable(teamRole) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/website/src/components/hero-heading.tsx: -------------------------------------------------------------------------------- 1 | import Balancer from 'react-wrap-balancer' 2 | 3 | export const HeroHeading = () => { 4 | return ( 5 |

6 | 7 | The open-source 8 |
9 | alternative to Zendesk 10 |
11 |

12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /packages/email/types.ts: -------------------------------------------------------------------------------- 1 | import type { Prisma } from '@seventy-seven/orm/prisma' 2 | 3 | export type Message = Prisma.MessageGetPayload<{ 4 | select: { 5 | id: true 6 | body: true 7 | created_at: true 8 | sent_from_full_name: true 9 | sent_from_email: true 10 | sent_from_avatar_url: true 11 | handler: { 12 | select: { 13 | full_name: true 14 | image_url: true 15 | } 16 | } 17 | } 18 | }> 19 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/settings/tags/layout.tsx: -------------------------------------------------------------------------------- 1 | import { PageWrapper } from '@/components/page-wrapper' 2 | import { HydrateClient, trpc } from '@/trpc/server' 3 | 4 | const TagsLayout = ({ children }: { children: React.ReactNode }) => { 5 | trpc.users.myCurrentTeam.prefetch() 6 | 7 | return ( 8 | 9 | {children} 10 | 11 | ) 12 | } 13 | 14 | export default TagsLayout 15 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/settings/members/page.tsx: -------------------------------------------------------------------------------- 1 | import { TeamMembers, TeamMembersSkeleton } from '@/components/team-members' 2 | import { trpc } from '@/trpc/server' 3 | import { Suspense } from 'react' 4 | 5 | const MembersPage = () => { 6 | trpc.users.myCurrentTeam.prefetch() 7 | 8 | return ( 9 | }> 10 | 11 | 12 | ) 13 | } 14 | 15 | export default MembersPage 16 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/create-seventy-seven-ticket-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { pushModal } from './modals' 4 | 5 | type Props = { 6 | children: React.ReactNode 7 | } 8 | 9 | export const CreateSeventySevenTicketButton = ({ children }: Props) => { 10 | return ( 11 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/email/index.ts: -------------------------------------------------------------------------------- 1 | import { render } from '@react-email/render' 2 | import { Resend } from 'resend' 3 | 4 | const RESEND_API_KEY = process.env.RESEND_API_KEY 5 | 6 | if (!RESEND_API_KEY) { 7 | throw new Error('RESEND_API_KEY is required') 8 | } 9 | 10 | export const createResendClient = () => new Resend(process.env.RESEND_API_KEY) 11 | export const componentToPlainText = (component: React.ReactElement) => 12 | render(component, { 13 | plainText: true, 14 | }) 15 | -------------------------------------------------------------------------------- /packages/ui/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": "src/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components/shadcn", 15 | "ui": "@/components/shadcn", 16 | "utils": "@/utils" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/ticket-info-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@seventy-seven/ui/button' 2 | import { Icon } from '@seventy-seven/ui/icon' 3 | import { pushSheet } from './sheets' 4 | 5 | export const TicketInfoButton = () => { 6 | return ( 7 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/account/layout.tsx: -------------------------------------------------------------------------------- 1 | import { AccountSubNav } from '@/components/account-sub-nav' 2 | 3 | type Props = { 4 | children: React.ReactNode 5 | } 6 | 7 | const AccountLayout = ({ children }: Props) => { 8 | return ( 9 |
10 |
11 | 12 |
{children}
13 |
14 |
15 | ) 16 | } 17 | 18 | export default AccountLayout 19 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/account/teams/layout.tsx: -------------------------------------------------------------------------------- 1 | import { PageWrapper } from '@/components/page-wrapper' 2 | import { HydrateClient, trpc } from '@/trpc/server' 3 | 4 | export const dynamic = 'force-dynamic' 5 | 6 | const TeamsLayout = ({ children }: { children: React.ReactNode }) => { 7 | trpc.teams.findMany.prefetch() 8 | 9 | return ( 10 | 11 | {children} 12 | 13 | ) 14 | } 15 | 16 | export default TeamsLayout 17 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(public)/all-done/event-emitter.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useEffect } from 'react' 4 | import type { WindowEvent } from './schema' 5 | 6 | type Props = { 7 | event: WindowEvent 8 | } 9 | 10 | export const EventEmitter = ({ event }: Props) => { 11 | useEffect(() => { 12 | if (!window?.opener) { 13 | return 14 | } 15 | 16 | if (event) { 17 | window.opener.postMessage(event, '*') 18 | } 19 | }, [event]) 20 | 21 | return null 22 | } 23 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { UpdateTeamAvatarForm } from '@/components/forms/update-team-avatar-form' 2 | import { UpdateTeamNameForm } from '@/components/forms/update-team-name-form' 3 | import { PageWrapper } from '@/components/page-wrapper' 4 | 5 | const SettingsPage = () => { 6 | return ( 7 | 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | export default SettingsPage 15 | -------------------------------------------------------------------------------- /apps/dashboard/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import("next").NextConfig} */ 2 | const config = { 3 | transpilePackages: ['@seventy-seven/ui'], 4 | reactStrictMode: true, 5 | typescript: { 6 | ignoreBuildErrors: true, 7 | }, 8 | images: { 9 | remotePatterns: [ 10 | { 11 | protocol: 'http', 12 | hostname: '127.0.0.1', 13 | }, 14 | { 15 | protocol: 'https', 16 | hostname: '**', 17 | }, 18 | ], 19 | }, 20 | } 21 | 22 | export default config 23 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCContext } from '@/trpc/init' 2 | import { appRouter } from '@/trpc/routers/_app' 3 | import { fetchRequestHandler } from '@trpc/server/adapters/fetch' 4 | 5 | export const dynamic = 'force-dynamic' 6 | 7 | const handler = (req: Request) => 8 | fetchRequestHandler({ 9 | endpoint: '/api/trpc', 10 | req, 11 | router: appRouter, 12 | createContext: createTRPCContext, 13 | }) 14 | 15 | export { handler as GET, handler as POST } 16 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/settings/integrations/layout.tsx: -------------------------------------------------------------------------------- 1 | import { PageWrapper } from '@/components/page-wrapper' 2 | import { HydrateClient, trpc } from '@/trpc/server' 3 | 4 | export const dynamic = 'force-dynamic' 5 | 6 | const IntegrationsLayout = ({ children }: { children: React.ReactNode }) => { 7 | trpc.integrations.getSlackIntegration.prefetch() 8 | 9 | return ( 10 | 11 | {children} 12 | 13 | ) 14 | } 15 | 16 | export default IntegrationsLayout 17 | -------------------------------------------------------------------------------- /apps/dashboard/src/utils/random.ts: -------------------------------------------------------------------------------- 1 | export function randomInt(...args: [number] | [number, number] | []) { 2 | switch (args.length) { 3 | // If one argument generate between 0 and args[0] 4 | case 1: 5 | return Math.floor(Math.random() * (args[0] + 1)) 6 | 7 | // If two arguments generate between args[0] and args[1] 8 | case 2: 9 | return Math.floor(Math.random() * (args[1] - args[0] + 1)) + args[0] 10 | 11 | // Otherwise generate between 0 and 1 12 | default: 13 | return Math.random() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/analytics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@seventy-seven/analytics", 3 | "version": "0.0.0", 4 | "private": true, 5 | "sideEffects": false, 6 | "main": "src/index.tsx", 7 | "scripts": { 8 | "lint": "biome check .", 9 | "check:types": "tsc --noEmit" 10 | }, 11 | "dependencies": { 12 | "@openpanel/nextjs": "0.0.10-beta", 13 | "@vercel/functions": "^1.0.2" 14 | }, 15 | "devDependencies": { 16 | "@seventy-seven/typescript-config": "workspace:*", 17 | "@types/node": "^20.14.2", 18 | "typescript": "^5.4.5" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "outputs": [".next/**", "!.next/cache/**", "dist/**"] 7 | }, 8 | "lint": { 9 | "dependsOn": ["^lint"] 10 | }, 11 | "dev": { 12 | "cache": false, 13 | "persistent": true 14 | }, 15 | "prisma:generate": { 16 | "cache": false 17 | }, 18 | "prisma:push": { 19 | "cache": false 20 | }, 21 | "check:types": { 22 | "persistent": true 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/typescript-config/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "esModuleInterop": true, 6 | "incremental": false, 7 | "isolatedModules": true, 8 | "lib": ["es2022", "DOM", "DOM.Iterable"], 9 | "module": "NodeNext", 10 | "moduleDetection": "force", 11 | "moduleResolution": "NodeNext", 12 | "noUncheckedIndexedAccess": true, 13 | "resolveJsonModule": true, 14 | "skipLibCheck": true, 15 | "strict": true, 16 | "target": "ES2022" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/settings/members/pending/page.tsx: -------------------------------------------------------------------------------- 1 | import { PendingTeamMembers, PendingTeamMembersSkeleton } from '@/components/pending-team-members' 2 | import { HydrateClient, trpc } from '@/trpc/server' 3 | import { Suspense } from 'react' 4 | 5 | const PendingMembersPage = () => { 6 | trpc.teams.invites.prefetch() 7 | 8 | return ( 9 | 10 | }> 11 | 12 | 13 | 14 | ) 15 | } 16 | 17 | export default PendingMembersPage 18 | -------------------------------------------------------------------------------- /packages/integrations/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@seventy-seven/integrations", 3 | "version": "1.0.0", 4 | "private": true, 5 | "sideEffects": false, 6 | "scripts": { 7 | "lint": "biome check .", 8 | "check:types": "tsc --noEmit" 9 | }, 10 | "dependencies": { 11 | "@slack/bolt": "^3.18.0", 12 | "@slack/oauth": "^3.0.0" 13 | }, 14 | "devDependencies": { 15 | "@seventy-seven/typescript-config": "workspace:*", 16 | "@types/node": "^20.14.2", 17 | "typescript": "^5.4.5" 18 | }, 19 | "exports": { 20 | "./slack": "./src/slack/index.ts" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/create-team-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Button } from '@seventy-seven/ui/button' 4 | import { Icon } from '@seventy-seven/ui/icon' 5 | import { cn } from '@seventy-seven/ui/utils' 6 | import { pushModal } from './modals' 7 | 8 | type Props = { 9 | className?: string 10 | } 11 | 12 | export const CreateTeamButton = ({ className }: Props) => { 13 | return ( 14 | 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 | # env 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # next.js 16 | .next/ 17 | out/ 18 | next-env.d.ts 19 | 20 | # Testing 21 | coverage 22 | 23 | # Turbo 24 | .turbo 25 | 26 | # Vercel 27 | .vercel 28 | 29 | # Build Outputs 30 | out/ 31 | build 32 | dist 33 | 34 | 35 | # Debug 36 | npm-debug.log* 37 | yarn-debug.log* 38 | yarn-error.log* 39 | 40 | # Misc 41 | .DS_Store 42 | *.pem 43 | -------------------------------------------------------------------------------- /apps/dashboard/src/utils/colors.ts: -------------------------------------------------------------------------------- 1 | import { randomInt } from './random' 2 | 3 | export const ticketTagColors = [ 4 | '#f9d4f4', 5 | '#eddee0', 6 | '#eaface', 7 | '#f5ebea', 8 | '#ddeff0', 9 | '#e9dbf9', 10 | '#ebface', 11 | '#f5ebea', 12 | '#d6f5fb', 13 | '#ebfce0', 14 | '#f4e9ea', 15 | '#d7faf3', 16 | '#f5e6dc', 17 | '#f8fcdf', 18 | '#eae0fa', 19 | '#e1dff4', 20 | '#f5eee9', 21 | '#f5e8dc', 22 | ] 23 | 24 | export const getRandomTagColor = () => { 25 | const index = randomInt(0, ticketTagColors.length - 1) 26 | const color = ticketTagColors[index]! 27 | 28 | return color 29 | } 30 | -------------------------------------------------------------------------------- /apps/supabase/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@seventy-seven/supabase-app", 3 | "private": true, 4 | "scripts": { 5 | "dev": "supabase start", 6 | "login": "supabase login", 7 | "db:reset": "tsx ./seed.ts" 8 | }, 9 | "devDependencies": { 10 | "@faker-js/faker": "^8.4.1", 11 | "@seventy-seven/orm": "workspace:*", 12 | "@seventy-seven/supabase": "workspace:*", 13 | "@seventy-seven/typescript-config": "workspace:*", 14 | "@supabase/supabase-js": "^2.42.5", 15 | "dotenv": "^16.4.5", 16 | "supabase": "^1.162.4", 17 | "tsx": "^4.19.1", 18 | "typescript": "^5.4.5" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/analytics-set-profile.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { trpc } from '@/trpc/client' 4 | import { setProfile } from '@seventy-seven/analytics' 5 | import { useEffect } from 'react' 6 | 7 | export const AnalyticsSetProfile = () => { 8 | const { data: user } = trpc.users.maybeMe.useQuery() 9 | 10 | useEffect(() => { 11 | if (!user) { 12 | return 13 | } 14 | 15 | setProfile({ 16 | profileId: user.id, 17 | firstName: user.full_name, 18 | email: user.email, 19 | avatar: user.image_url ?? undefined, 20 | }) 21 | }, [user]) 22 | 23 | return null 24 | } 25 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/invite-team-member-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { trpc } from '@/trpc/client' 4 | import { Button } from '@seventy-seven/ui/button' 5 | import { Icon } from '@seventy-seven/ui/icon' 6 | import { pushModal } from './modals' 7 | 8 | export const InviteTeamMemberButton = () => { 9 | const [team] = trpc.users.myCurrentTeam.useSuspenseQuery() 10 | 11 | return ( 12 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/clear-all-filters-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Button } from '@seventy-seven/ui/button' 4 | import { cn } from '@seventy-seven/ui/utils' 5 | import { useTicketFilters } from './ticket-filters/use-ticket-filters' 6 | 7 | type Props = { 8 | children: React.ReactNode 9 | className?: string 10 | } 11 | 12 | export const ClearAllFiltersButton = ({ children, className }: Props) => { 13 | const { clearFilters } = useTicketFilters() 14 | 15 | return ( 16 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /apps/dashboard/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/dashboard/src/trpc/query-client.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query' 2 | import superjson from 'superjson' 3 | 4 | export function makeQueryClient() { 5 | return new QueryClient({ 6 | defaultOptions: { 7 | queries: { 8 | staleTime: 30 * 1000, 9 | }, 10 | dehydrate: { 11 | serializeData: superjson.serialize, 12 | shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending', 13 | }, 14 | hydrate: { 15 | deserializeData: superjson.deserialize, 16 | }, 17 | }, 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/create-tag-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Button } from '@seventy-seven/ui/button' 4 | import { Icon } from '@seventy-seven/ui/icon' 5 | import { cn } from '@seventy-seven/ui/utils' 6 | import { pushModal } from './modals' 7 | 8 | type Props = { 9 | children: React.ReactNode 10 | className?: string 11 | } 12 | 13 | export const CreateTagButton = ({ children, className }: Props) => { 14 | return ( 15 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seventy-seven", 3 | "private": true, 4 | "scripts": { 5 | "postinstall": "turbo prisma:generate", 6 | "build": "turbo build", 7 | "dev": "dotenv -- turbo dev", 8 | "dev:trigger": "@trigger.dev/cli@latest dev", 9 | "lint": "turbo lint", 10 | "check:types": "turbo check:types --concurrency 20", 11 | "prisma:generate": "turbo prisma:generate" 12 | }, 13 | "devDependencies": { 14 | "@biomejs/biome": "^1.7.0", 15 | "@seventy-seven/typescript-config": "workspace:*", 16 | "dotenv-cli": "^7.4.1", 17 | "turbo": "^1.13.0", 18 | "typescript": "^5.4.5" 19 | }, 20 | "packageManager": "pnpm@8.6.12" 21 | } 22 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/modals/snooze-ticket-modal.tsx: -------------------------------------------------------------------------------- 1 | import { SnoozeTicketForm } from '@/components/forms/snooze-ticket-form' 2 | import { Modal, ModalDescription, ModalHeader, ModalTitle } from '@seventy-seven/ui/modal' 3 | 4 | type Props = { 5 | ticketId: string 6 | } 7 | 8 | export const SnoozeTicketModal = ({ ticketId }: Props) => { 9 | return ( 10 | 11 | 12 | Snooze Ticket 13 | When the time has expired this will automatically be put back in your inbox 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /apps/dashboard/src/utils/stripMarkupfromMessage.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'node-html-parser' 2 | 3 | export const stripMarkupfromMessage = (html: string) => { 4 | const parsedHtml = parse(html) 5 | 6 | const strippedTagNames = [ 7 | 'blockquote', 8 | 'style', 9 | 'script', 10 | 'noscript', 11 | 'iframe', 12 | 'object', 13 | 'embed', 14 | 'applet', 15 | 'base', 16 | 'link', 17 | 'meta', 18 | 'title', 19 | 'head', 20 | ] 21 | 22 | parsedHtml.querySelectorAll('*').forEach((el) => { 23 | if (strippedTagNames.includes(el.tagName.toLowerCase())) { 24 | el.remove() 25 | } 26 | }) 27 | 28 | return parsedHtml.toString() 29 | } 30 | -------------------------------------------------------------------------------- /packages/supabase/src/clients/client.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserClient } from '@supabase/ssr' 2 | import type { Database } from '../types/db' 3 | 4 | export const createClient = () => { 5 | const NEXT_PUBLIC_SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL 6 | const NEXT_PUBLIC_SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY 7 | 8 | if (!NEXT_PUBLIC_SUPABASE_URL) { 9 | throw new Error('Missing env.NEXT_PUBLIC_SUPABASE_URL') 10 | } 11 | 12 | if (!NEXT_PUBLIC_SUPABASE_ANON_KEY) { 13 | throw new Error('Missing env.NEXT_PUBLIC_SUPABASE_ANON_KEY') 14 | } 15 | 16 | return createBrowserClient(NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY) 17 | } 18 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/edit-ticket-tag-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import type { UsersRouter } from '@/trpc/routers/users-router' 4 | import { Button } from '@seventy-seven/ui/button' 5 | import { Icon } from '@seventy-seven/ui/icon' 6 | import { pushModal } from './modals' 7 | 8 | type Props = { 9 | tag: UsersRouter.MyCurrentTeam['current_team']['ticket_tags'][number] 10 | } 11 | 12 | export const EditTicketTagButton = ({ tag }: Props) => { 13 | return ( 14 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /packages/email/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import { Column, Img, Link, Row, Section } from '@react-email/components' 2 | 3 | const baseUrl = 4 | process.env.VERCEL_ENV === 'production' ? 'https://seventy-seven.dev/email/' : 'http://localhost:3001/email' 5 | 6 | export const Footer = () => { 7 | return ( 8 |
9 | 10 | 11 | 12 | Powered by 13 | 77 14 | 15 | 16 | 17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /apps/website/src/components/confetti-rain.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useStore } from '@/store' 4 | import dynamic from 'next/dynamic' 5 | 6 | const Confetti = dynamic(() => import('react-confetti'), { 7 | ssr: false, 8 | }) 9 | 10 | export const ConfettiRain = () => { 11 | const showConfetti = useStore((state) => state.showConfetti) 12 | const setShowConfetti = useStore((state) => state.setShowConfetti) 13 | 14 | return ( 15 | setShowConfetti(false)} 22 | /> 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /apps/website/src/utils/analytics.ts: -------------------------------------------------------------------------------- 1 | import { createAnalyticsClient } from '@seventy-seven/analytics' 2 | 3 | const NEXT_PUBLIC_OPENPANEL_WEBSITE_CLIENT_ID = process.env.NEXT_PUBLIC_OPENPANEL_WEBSITE_CLIENT_ID 4 | const OPENPANEL_WEBSITE_CLIENT_SECRET = process.env.OPENPANEL_WEBSITE_CLIENT_SECRET 5 | 6 | if (!NEXT_PUBLIC_OPENPANEL_WEBSITE_CLIENT_ID) { 7 | throw new Error('NEXT_PUBLIC_OPENPANEL_WEBSITE_CLIENT_ID is required') 8 | } 9 | 10 | if (!OPENPANEL_WEBSITE_CLIENT_SECRET) { 11 | throw new Error('OPENPANEL_WEBSITE_CLIENT_SECRET is required') 12 | } 13 | 14 | export const opServerClient = createAnalyticsClient({ 15 | clientId: NEXT_PUBLIC_OPENPANEL_WEBSITE_CLIENT_ID, 16 | clientSecret: OPENPANEL_WEBSITE_CLIENT_SECRET, 17 | }) 18 | -------------------------------------------------------------------------------- /apps/dashboard/src/trpc/server.ts: -------------------------------------------------------------------------------- 1 | import 'server-only' // <-- ensure this file cannot be imported from the client 2 | 3 | import { createHydrationHelpers } from '@trpc/react-query/rsc' 4 | import { cache } from 'react' 5 | import { createCallerFactory, createTRPCContext } from './init' 6 | import { makeQueryClient } from './query-client' 7 | import { appRouter } from './routers/_app' 8 | 9 | // IMPORTANT: Create a stable getter for the query client that 10 | // will return the same client during the same request. 11 | export const getQueryClient = cache(makeQueryClient) 12 | const caller = createCallerFactory(appRouter)(createTRPCContext) 13 | export const { trpc, HydrateClient } = createHydrationHelpers(caller, getQueryClient) 14 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/modals/assign-ticket-modal.tsx: -------------------------------------------------------------------------------- 1 | import type { TicketsRouter } from '@/trpc/routers/tickets-router' 2 | import { Modal, ModalDescription, ModalHeader, ModalTitle } from '@seventy-seven/ui/modal' 3 | import { AssignTeamMemberForm } from '../forms/assign-team-member-form' 4 | 5 | type Props = { 6 | ticket: TicketsRouter.FindById 7 | } 8 | 9 | export const AssignTicketModal = ({ ticket }: Props) => { 10 | return ( 11 | 12 | 13 | Assign ticket 14 | Choose which team member you want to assign this ticket to. 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /apps/dashboard/src/lib/analytics.tsx: -------------------------------------------------------------------------------- 1 | import { createAnalyticsClient } from '@seventy-seven/analytics' 2 | 3 | const NEXT_PUBLIC_OPENPANEL_DASHBOARD_CLIENT_ID = process.env.NEXT_PUBLIC_OPENPANEL_DASHBOARD_CLIENT_ID 4 | const OPENPANEL_DASHBOARD_CLIENT_SECRET = process.env.OPENPANEL_DASHBOARD_CLIENT_SECRET 5 | 6 | if (!NEXT_PUBLIC_OPENPANEL_DASHBOARD_CLIENT_ID) { 7 | throw new Error('NEXT_PUBLIC_OPENPANEL_DASHBOARD_CLIENT_ID is required') 8 | } 9 | 10 | if (!OPENPANEL_DASHBOARD_CLIENT_SECRET) { 11 | throw new Error('OPENPANEL_DASHBOARD_CLIENT_SECRET is required') 12 | } 13 | 14 | export const analyticsClient = createAnalyticsClient({ 15 | clientId: NEXT_PUBLIC_OPENPANEL_DASHBOARD_CLIENT_ID, 16 | clientSecret: OPENPANEL_DASHBOARD_CLIENT_SECRET, 17 | }) 18 | -------------------------------------------------------------------------------- /packages/orm/src/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | import { loggingExtension } from './prisma/extensions/loggingExtension' 3 | export * from '@prisma/client' 4 | 5 | const prismaClientSingleton = () => { 6 | return new PrismaClient({ 7 | // log: process.env.NODE_ENV === 'development' ? ['info', 'query'] : [], 8 | }).$extends(loggingExtension()) 9 | } 10 | 11 | type PrismaClientSingleton = ReturnType 12 | 13 | const globalForPrisma = globalThis as unknown as { 14 | prisma: PrismaClientSingleton | undefined 15 | } 16 | 17 | export const prisma = globalForPrisma.prisma ?? prismaClientSingleton() 18 | 19 | if (process.env.NODE_ENV !== 'production') { 20 | globalForPrisma.prisma = prisma 21 | } 22 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SettingsSubNav } from '@/components/settings-sub-nav' 2 | import { HydrateClient, trpc } from '@/trpc/server' 3 | 4 | export const dynamic = 'force-dynamic' 5 | 6 | type Props = { 7 | children: React.ReactNode 8 | } 9 | 10 | const SettingsLayout = ({ children }: Props) => { 11 | trpc.users.me.prefetch() 12 | 13 | return ( 14 | 15 |
16 |
17 |

Team settings

18 | 19 | 20 |
{children}
21 |
22 |
23 |
24 | ) 25 | } 26 | 27 | export default SettingsLayout 28 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(public)/all-done/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation' 2 | import { EventEmitter } from './event-emitter' 3 | import { searchParamsSchema } from './schema' 4 | 5 | type Props = { 6 | searchParams: Record 7 | } 8 | 9 | const AllDonePage = ({ searchParams }: Props) => { 10 | const parsedSearchParams = searchParamsSchema.safeParse(searchParams) 11 | 12 | if (!parsedSearchParams.success) { 13 | notFound() 14 | } 15 | 16 | return ( 17 | <> 18 | 19 |
20 |

All done, you can close this window!

21 |
22 | 23 | ) 24 | } 25 | 26 | export default AllDonePage 27 | -------------------------------------------------------------------------------- /apps/dashboard/src/lib/search-params.ts: -------------------------------------------------------------------------------- 1 | import { createSearchParamsCache, parseAsArrayOf, parseAsString, parseAsStringLiteral } from 'nuqs/server' 2 | 3 | // TICKET FILTERS 4 | export const statuses = ['unhandled', 'snoozed', 'starred', 'closed'] as const 5 | export type Status = (typeof statuses)[number] 6 | 7 | export const ticketFiltersParsers = { 8 | q: parseAsString, 9 | statuses: parseAsArrayOf(parseAsStringLiteral(statuses)), 10 | assignees: parseAsArrayOf(parseAsString), 11 | tags: parseAsArrayOf(parseAsString), 12 | } 13 | 14 | export const ticketFiltersCache = createSearchParamsCache(ticketFiltersParsers) 15 | 16 | // TICKET ID 17 | export const ticketIdParsers = { 18 | ticketId: parseAsString, 19 | } 20 | 21 | export const ticketIdCache = createSearchParamsCache(ticketIdParsers) 22 | -------------------------------------------------------------------------------- /packages/ui/src/components/shadcn/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as LabelPrimitive from '@radix-ui/react-label' 4 | import { type VariantProps, cva } from 'class-variance-authority' 5 | import * as React from 'react' 6 | 7 | import { cn } from '../../utils' 8 | 9 | const labelVariants = cva( 10 | 'text-sm leading-none cursor-pointer peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & VariantProps 16 | >(({ className, ...props }, ref) => ( 17 | 18 | )) 19 | Label.displayName = LabelPrimitive.Root.displayName 20 | 21 | export { Label } 22 | -------------------------------------------------------------------------------- /packages/orm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@seventy-seven/orm", 3 | "version": "0.0.0", 4 | "private": true, 5 | "exports": { 6 | "./prisma": "./src/prisma.ts", 7 | "./enums": "./src/prisma/enums.ts" 8 | }, 9 | "scripts": { 10 | "lint": "biome check .", 11 | "check:types": "tsc --noEmit", 12 | "prisma:generate": "prisma generate --schema=./src/prisma/schema.prisma", 13 | "_prisma:push": "cd ../../ && npx prisma db push --schema=./packages/orm/src/prisma/schema.prisma --skip-generate", 14 | "prisma:push": "prisma db push --schema=./src/prisma/schema.prisma --skip-generate" 15 | }, 16 | "dependencies": { 17 | "@prisma/client": "^6.2.1", 18 | "@prisma/extension-optimize": "^1.1.4" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^20.12.7", 22 | "prisma": "^6.2.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/account/teams/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { CreateTeamButton } from '@/components/create-team-button' 4 | import { TeamListItem } from '@/components/team-list-item' 5 | import { trpc } from '@/trpc/client' 6 | 7 | const AccountTeamsPage = () => { 8 | const [userTeams] = trpc.teams.findMany.useSuspenseQuery() 9 | 10 | return ( 11 | <> 12 |
13 | 14 |
15 | 16 | {userTeams.length === 0 ? ( 17 |

You don't belong to any team.

18 | ) : ( 19 | userTeams.map((userTeam) => ( 20 |
    21 | 22 |
23 | )) 24 | )} 25 | 26 | ) 27 | } 28 | 29 | export default AccountTeamsPage 30 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/ticket-filters/use-ticket-filters.ts: -------------------------------------------------------------------------------- 1 | import { ticketFiltersParsers } from '@/lib/search-params' 2 | import { useQueryStates } from 'nuqs' 3 | 4 | const statuses = ['unhandled', 'snoozed', 'starred', 'closed'] as const 5 | export type Status = (typeof statuses)[number] 6 | 7 | export const useTicketFilters = () => { 8 | const [filter, setFilter] = useQueryStates(ticketFiltersParsers, { 9 | shallow: false, 10 | }) 11 | 12 | const hasFilters = Object.entries(filter) 13 | .filter(([key]) => ['statuses', 'assignees', 'tags'].includes(key)) 14 | .some(([_key, value]) => value !== null) 15 | 16 | const clearFilters = () => { 17 | setFilter({ 18 | assignees: null, 19 | statuses: null, 20 | tags: null, 21 | }) 22 | } 23 | 24 | return { filter, setFilter, clearFilters, hasFilters } 25 | } 26 | -------------------------------------------------------------------------------- /packages/ui/src/components/shadcn/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '../../utils' 4 | 5 | export interface TextareaProps extends React.TextareaHTMLAttributes {} 6 | 7 | const Textarea = React.forwardRef(({ className, ...props }, ref) => { 8 | return ( 9 |