├── supabase ├── seed.sql ├── .gitignore └── migrations │ └── 20240930164856_remote_schema.sql ├── .nvmrc ├── .github ├── semantic.yml ├── images │ ├── app-keys.png │ ├── app-create.png │ └── supabase-keys.png ├── pull_request_template.md └── workflows │ └── code-check.yml ├── .eslintrc.json ├── .prettierrc ├── src ├── app │ ├── favicon.ico │ ├── dashboard │ │ ├── [projectId] │ │ │ ├── @modal │ │ │ │ ├── default.tsx │ │ │ │ └── (.)task │ │ │ │ │ └── [taskId] │ │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── task │ │ │ │ └── [taskId] │ │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ └── page.tsx │ ├── auth │ │ ├── signout │ │ │ └── route.ts │ │ ├── actions.ts │ │ └── callback │ │ │ └── route.ts │ ├── [userSlug] │ │ ├── notFound.tsx │ │ └── page.tsx │ ├── settings │ │ └── page.tsx │ ├── layout.tsx │ ├── page.tsx │ ├── providers.tsx │ ├── api │ │ └── github │ │ │ └── actions.ts │ └── globals.css ├── lib │ ├── supabase │ │ ├── cookies.ts │ │ ├── utils.ts │ │ ├── client.ts │ │ ├── serverAdmin.ts │ │ ├── types.ts │ │ ├── server.ts │ │ └── middleware.ts │ ├── github │ │ ├── server.ts │ │ ├── client.ts │ │ ├── api.ts │ │ └── middleware.ts │ ├── handle-error.ts │ ├── utils.ts │ └── dataTable.ts ├── components │ ├── ui │ │ ├── skeleton.tsx │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── separator.tsx │ │ ├── input.tsx │ │ ├── sonner.tsx │ │ ├── badge.tsx │ │ ├── checkbox.tsx │ │ ├── tooltip.tsx │ │ ├── hover-card.tsx │ │ ├── date-picker.tsx │ │ ├── popover.tsx │ │ ├── avatar.tsx │ │ ├── toggle.tsx │ │ ├── data-table │ │ │ ├── data-table-view-options.tsx │ │ │ ├── data-table.tsx │ │ │ ├── data-table-toolbar.tsx │ │ │ ├── data-table-pagination.tsx │ │ │ └── data-table-column-header.tsx │ │ ├── toggle-group.tsx │ │ ├── card.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── table.tsx │ │ ├── drawer.tsx │ │ ├── dialog.tsx │ │ └── sheet.tsx │ ├── RepoList │ │ ├── RepoCardGrid.tsx │ │ ├── DeleteRepoButton.tsx │ │ ├── FavouriteRepoForm.tsx │ │ ├── RepoCards.tsx │ │ └── RepoCard.tsx │ ├── HeroParticles.tsx │ ├── RemoveTaskIssue.tsx │ ├── icons.tsx │ ├── RemoveTaskPullRequest.tsx │ ├── SignInButton.tsx │ ├── RouteModal.tsx │ ├── WorkInProgressBanner.tsx │ ├── ProfileDeleteCard.tsx │ ├── TaskIssueSelector.tsx │ ├── TaskPullRequestSelector.tsx │ ├── ProfileDeleteButton.tsx │ ├── Task │ │ └── TaskNotes.tsx │ ├── Repo │ │ ├── Notes.tsx │ │ └── RepoPublicDisplayToggle.tsx │ ├── ActivityCalendar.tsx │ ├── PublicProjectCardSkeleton.tsx │ ├── TasksTable │ │ ├── TasksTableToolbarActions.tsx │ │ ├── QuickAssignDialog.tsx │ │ ├── TasksTable.tsx │ │ └── DeleteTaskDialog.tsx │ ├── ThemeToggle.tsx │ ├── magicui │ │ ├── marquee.tsx │ │ ├── number-ticker.tsx │ │ ├── border-beam.tsx │ │ ├── blur-fade.tsx │ │ ├── magic-card.tsx │ │ ├── confetti.tsx │ │ └── sparkles-text.tsx │ ├── Header.tsx │ ├── TasksList │ │ └── Tasks.tsx │ ├── IssueSelector.tsx │ ├── Hero.tsx │ ├── HeaderUser.tsx │ ├── TaskForm │ │ └── UpdateTaskSheet.tsx │ ├── PublicProjectCard.tsx │ ├── RepoSelector.tsx │ ├── TrustedBy.tsx │ ├── ProfileSettings.tsx │ ├── IssuePreview.tsx │ ├── ProfileDeleteDialog.tsx │ └── Autocomplete.tsx ├── types │ ├── schemas.ts │ └── index.ts ├── hooks │ ├── useDebounce.ts │ ├── useMediaQuery.ts │ ├── useDebouncedValue.ts │ ├── useGitHubIssues.ts │ └── useGitHubRepositories.ts ├── services │ ├── meta.ts │ ├── profile │ │ └── api.ts │ ├── settings │ │ └── api.ts │ ├── project │ │ └── api.ts │ └── tasks │ │ └── api.ts └── middleware.ts ├── vitest.config.ts ├── postcss.config.mjs ├── next.config.mjs ├── components.json ├── .gitignore ├── .env.example ├── tsconfig.json ├── README.md ├── package.json ├── tailwind.config.ts └── CONTRIBUTING.md /supabase/seed.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.15.1 2 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | titleOnly: true 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | .env 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "plugins": ["prettier-plugin-tailwindcss"] 4 | } 5 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Balastrong/myntenance/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /.github/images/app-keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Balastrong/myntenance/HEAD/.github/images/app-keys.png -------------------------------------------------------------------------------- /src/app/dashboard/[projectId]/@modal/default.tsx: -------------------------------------------------------------------------------- 1 | export default function Default() { 2 | return null 3 | } 4 | -------------------------------------------------------------------------------- /.github/images/app-create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Balastrong/myntenance/HEAD/.github/images/app-create.png -------------------------------------------------------------------------------- /.github/images/supabase-keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Balastrong/myntenance/HEAD/.github/images/supabase-keys.png -------------------------------------------------------------------------------- /src/lib/supabase/cookies.ts: -------------------------------------------------------------------------------- 1 | export const GITHUB_ACCESS_TOKEN_COOKIE = "github_access_token" 2 | export const GITHUB_REFRESH_TOKEN_COOKIE = "github_refresh_token" 3 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: "jsdom", 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | This PR closes #[issue_number] 4 | -------------------------------------------------------------------------------- /src/lib/supabase/utils.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseClient } from "./types" 2 | 3 | export async function getCurrentUserId(client: DatabaseClient) { 4 | return client.auth.getUser().then(({ data }) => data.user?.id) 5 | } 6 | -------------------------------------------------------------------------------- /supabase/migrations/20240930164856_remote_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TRIGGER create_profile_trigger AFTER INSERT ON auth.users FOR EACH ROW WHEN ((new.raw_user_meta_data IS NOT NULL)) EXECUTE FUNCTION create_profile(); 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/dashboard/[projectId]/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function Layout({ 2 | children, 3 | modal, 4 | }: Readonly<{ 5 | children: React.ReactNode 6 | modal: React.ReactNode 7 | }>) { 8 | return ( 9 | <> 10 | {children} 11 | {modal} 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/supabase/client.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserClient } from "@supabase/ssr" 2 | import { Database } from "./types.gen" 3 | 4 | export const createClient = () => 5 | createBrowserClient( 6 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 7 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 8 | ) 9 | -------------------------------------------------------------------------------- /src/app/auth/signout/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/lib/supabase/server" 2 | import { NextResponse } from "next/server" 3 | 4 | export async function GET(request: Request) { 5 | await createClient().auth.signOut() 6 | 7 | const { origin } = new URL(request.url) 8 | 9 | return NextResponse.redirect(origin) 10 | } 11 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /src/lib/github/server.ts: -------------------------------------------------------------------------------- 1 | import { cookies } from "next/headers" 2 | import { Octokit } from "octokit" 3 | import { GITHUB_ACCESS_TOKEN_COOKIE } from "../supabase/cookies" 4 | 5 | export const getServerOctokit = () => { 6 | const token = cookies().get(GITHUB_ACCESS_TOKEN_COOKIE) 7 | 8 | return new Octokit({ 9 | auth: token?.value, 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/components/RepoList/RepoCardGrid.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | title: string 3 | children: React.ReactNode 4 | } 5 | 6 | export function RepoCardGrid({ title, children }: Props) { 7 | return ( 8 |
9 |

{title}

10 |
{children}
11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/types/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | 3 | export const TaskStatusSchema = z.union([ 4 | z.literal("todo"), 5 | z.literal("doing"), 6 | z.literal("done"), 7 | z.literal("rejected"), 8 | ]) 9 | 10 | export const TaskStatusValues = TaskStatusSchema.options.map( 11 | (option) => option.value, 12 | ) 13 | 14 | export type TaskStatus = z.infer 15 | -------------------------------------------------------------------------------- /src/app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import RepoCards from "@/components/RepoList/RepoCards" 2 | import { RepoSelector } from "@/components/RepoSelector" 3 | 4 | export default async function Dashboard() { 5 | return ( 6 |
7 |

Dashboard

8 | 9 | 10 |
11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "avatars.githubusercontent.com", 8 | pathname: "/u/**", 9 | }, 10 | { 11 | protocol: "https", 12 | hostname: "github.com", 13 | }, 14 | ], 15 | }, 16 | }; 17 | 18 | export default nextConfig; 19 | -------------------------------------------------------------------------------- /src/lib/github/client.ts: -------------------------------------------------------------------------------- 1 | import { getCookie } from "cookies-next" 2 | import { Octokit } from "octokit" 3 | import { GITHUB_ACCESS_TOKEN_COOKIE } from "../supabase/cookies" 4 | 5 | export const getClientOctokit = () => { 6 | const token = getCookie(GITHUB_ACCESS_TOKEN_COOKIE) 7 | 8 | if (!token) { 9 | throw new Error("No supabase session found") 10 | } 11 | 12 | return new Octokit({ 13 | auth: token, 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /src/components/HeroParticles.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import Particles from "./magicui/particles" 5 | 6 | export function HeroParticles() { 7 | const { resolvedTheme } = useTheme() 8 | 9 | return ( 10 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | // TODO replace with useDebouncedValue 4 | export function useDebounce(value: T, delay?: number): T { 5 | const [debouncedValue, setDebouncedValue] = React.useState(value) 6 | 7 | React.useEffect(() => { 8 | const timer = setTimeout(() => setDebouncedValue(value), delay ?? 500) 9 | 10 | return () => { 11 | clearTimeout(timer) 12 | } 13 | }, [value, delay]) 14 | 15 | return debouncedValue 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/supabase/serverAdmin.ts: -------------------------------------------------------------------------------- 1 | import { createServerClient } from "@supabase/ssr" 2 | import { Database } from "./types.gen" 3 | 4 | export function createAdminClient() { 5 | return createServerClient( 6 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 7 | process.env.SUPABASE_SERVICE_KEY ?? 8 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 9 | { 10 | cookies: { 11 | getAll() { 12 | return [] 13 | }, 14 | }, 15 | }, 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /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/app/globals.css", 9 | "baseColor": "slate", 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 | } 21 | -------------------------------------------------------------------------------- /src/app/[userSlug]/notFound.tsx: -------------------------------------------------------------------------------- 1 | import { SignInButton } from "@/components/SignInButton" 2 | 3 | export default async function NotFound({ slug }: { slug: string }) { 4 | return ( 5 |
6 |

7 | Looks like {slug} doesn't have a public profile 8 | here 9 |

10 | Want to create your own? 11 |
12 | 13 |
14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/RemoveTaskIssue.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { assignTaskIssue } from "@/services/tasks/api" 4 | import { Button } from "./ui/button" 5 | import { X } from "lucide-react" 6 | 7 | export function RemoveTaskIssue({ taskId }: { taskId: number }) { 8 | return ( 9 |
{ 11 | assignTaskIssue({ id: taskId, issueNumber: null }) 12 | }} 13 | > 14 | 17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/icons.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export type IconProps = React.HTMLAttributes 4 | 5 | export const Icons = { 6 | spinner: (props: IconProps) => ( 7 | 17 | 18 | 19 | ), 20 | } 21 | -------------------------------------------------------------------------------- /src/components/RemoveTaskPullRequest.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { assignTaskPullRequest } from "@/services/tasks/api" 4 | import { X } from "lucide-react" 5 | import { Button } from "./ui/button" 6 | 7 | export function RemoveTaskPullRequest({ taskId }: { taskId: number }) { 8 | return ( 9 |
{ 11 | assignTaskPullRequest({ id: taskId, prNumber: null }) 12 | }} 13 | > 14 | 17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/SignInButton.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { login } from "@/app/auth/actions" 3 | import { useFormStatus } from "react-dom" 4 | import { Button } from "./ui/button" 5 | import { BorderBeam } from "./magicui/border-beam" 6 | 7 | export const SignInButton = () => { 8 | const { pending } = useFormStatus() 9 | return ( 10 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/hooks/useMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | export function useMediaQuery(query: string) { 4 | const [value, setValue] = React.useState(false) 5 | 6 | React.useEffect(() => { 7 | function onChange(event: MediaQueryListEvent) { 8 | setValue(event.matches) 9 | } 10 | 11 | const result = matchMedia(query) 12 | result.addEventListener("change", onChange) 13 | setValue(result.matches) 14 | 15 | return () => result.removeEventListener("change", onChange) 16 | }, [query]) 17 | 18 | return value 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 | .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 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/components/RouteModal.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useRouter } from "next/navigation" 4 | import { Dialog, DialogContent } from "./ui/dialog" 5 | 6 | type Props = { 7 | children: React.ReactNode 8 | } 9 | export function RouteModal({ children }: Props) { 10 | const router = useRouter() 11 | 12 | const handleOpenChange = () => { 13 | router.back() 14 | } 15 | 16 | return ( 17 | 18 | {children} 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Production 2 | NEXT_PUBLIC_SUPABASE_URL=https://bqlqcmtccbsiocjopvas.supabase.co 3 | NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJxbHFjbXRjY2JzaW9jam9wdmFzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTk4NTEyMjksImV4cCI6MjAzNTQyNzIyOX0.WSOXFefddaZUWCdvzeDfLNm8hp152ufl5R47PQbBKJ0 4 | 5 | # Local (check CONTRIBUTING.md to learn where to get these values) 6 | # NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321 7 | # NEXT_PUBLIC_SUPABASE_ANON_KEY= 8 | # SUPABASE_SERVICE_KEY= 9 | # GITHUB_CLIENT_ID= 10 | # GITHUB_CLIENT_SECRET= 11 | -------------------------------------------------------------------------------- /src/components/WorkInProgressBanner.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | export function WorkInProgressBanner() { 4 | return ( 5 |
6 | 🚧 We're building this together, found a bug or have a feature idea? 7 | 12 | Create an issue on GitHub! 13 | 14 | 🚧 15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/supabase/types.ts: -------------------------------------------------------------------------------- 1 | import { SupabaseClient } from "@supabase/supabase-js" 2 | import { Database, Tables } from "./types.gen" 3 | 4 | export type DatabaseClient = SupabaseClient 5 | 6 | export type Task = Tables<"tasks"> 7 | export type TaskInsert = Database["public"]["Tables"]["tasks"]["Insert"] 8 | export type TaskUpdate = Database["public"]["Tables"]["tasks"]["Update"] 9 | 10 | export type SettingsUpdate = 11 | Database["public"]["Tables"]["user_settings"]["Update"] 12 | 13 | export type ProfileUpdate = 14 | Database["public"]["Tables"]["user_profiles"]["Update"] 15 | -------------------------------------------------------------------------------- /src/components/ProfileDeleteCard.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import ProfileDeleteButton from "./ProfileDeleteButton" 4 | import { Card, CardContent, CardFooter, CardHeader } from "./ui/card" 5 | 6 | export function ProfileDeleteCard() { 7 | return ( 8 | 9 | Delete account 10 | 11 | Once you delete your account, there is no going back. Please be certain. 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/components/TaskIssueSelector.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { assignTaskIssue } from "@/services/tasks/api" 4 | import { IssueSelector } from "./IssueSelector" 5 | 6 | type Props = { 7 | taskId: number 8 | repositoryFullName: string 9 | } 10 | 11 | export function TaskIssueSelector({ taskId, repositoryFullName }: Props) { 12 | const issueBaseQuery = `is:issue repo:${repositoryFullName}` 13 | 14 | return ( 15 | { 18 | assignTaskIssue({ id: taskId, issueNumber }) 19 | }} 20 | /> 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/components/TaskPullRequestSelector.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { assignTaskPullRequest } from "@/services/tasks/api" 4 | import { IssueSelector } from "./IssueSelector" 5 | 6 | type Props = { 7 | taskId: number 8 | repositoryFullName: string 9 | } 10 | 11 | export function TaskPullRequestSelector({ taskId, repositoryFullName }: Props) { 12 | const prBaseQuery = `is:pr repo:${repositoryFullName}` 13 | 14 | return ( 15 | { 18 | assignTaskPullRequest({ id: taskId, prNumber }) 19 | }} 20 | /> 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/services/meta.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { DatabaseClient } from "@/lib/supabase/types" 4 | import { unstable_cache } from "next/cache" 5 | 6 | export const getSiteMeta = unstable_cache( 7 | async (supabase: DatabaseClient) => { 8 | const { 9 | data: { users }, 10 | } = await supabase.auth.admin.listUsers() 11 | 12 | const { count: projects } = await supabase 13 | .from("projects") 14 | .select("*", { count: "exact" }) 15 | 16 | return { 17 | usersCount: users.length, 18 | projectsCount: projects ?? 0, 19 | } 20 | }, 21 | undefined, 22 | { revalidate: 3600 }, 23 | ) 24 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface SearchParams { 2 | [key: string]: string | string[] | undefined 3 | } 4 | 5 | export interface Option { 6 | label: string 7 | value: string 8 | icon?: React.ComponentType<{ className?: string }> 9 | } 10 | 11 | export interface DataTableFilterField { 12 | label: string 13 | value: keyof TData 14 | placeholder?: string 15 | options?: Option[] 16 | } 17 | 18 | export interface DataTableFilterOption { 19 | id: string 20 | label: string 21 | value: keyof TData 22 | options: Option[] 23 | filterValues?: string[] 24 | filterOperator?: string 25 | isMulti?: boolean 26 | } 27 | -------------------------------------------------------------------------------- /src/services/profile/api.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | import { createClient } from "@/lib/supabase/server" 3 | import { getCurrentUserId } from "@/lib/supabase/utils" 4 | 5 | export async function getOwnProfile() { 6 | const supabase = createClient() 7 | const userId = await getCurrentUserId(supabase) 8 | 9 | return await createClient() 10 | .from("user_profiles") 11 | .select("*") 12 | .eq("user", userId!) 13 | .single() 14 | } 15 | 16 | export async function getUserProfileBySlug(slug: string) { 17 | return await createClient() 18 | .from("user_profiles") 19 | .select("*") 20 | .ilike("slug", slug) 21 | .single() 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/code-check.yml: -------------------------------------------------------------------------------- 1 | name: Check code style 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | run-checks: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - name: Setup pnpm 16 | uses: pnpm/action-setup@v4.0.0 17 | - name: Setup Node 18 | uses: actions/setup-node@v4.0.3 19 | with: 20 | node-version-file: .nvmrc 21 | - run: pnpm i --frozen-lockfile 22 | 23 | - name: Checking format 24 | run: pnpm run format:check 25 | - name: Run lint 26 | run: pnpm run lint 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "target": "ES6", 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/supabase/server.ts: -------------------------------------------------------------------------------- 1 | import { createServerClient } from "@supabase/ssr" 2 | import { cookies } from "next/headers" 3 | import { Database } from "./types.gen" 4 | 5 | export function createClient() { 6 | const cookieStore = cookies() 7 | 8 | return createServerClient( 9 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 10 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 11 | { 12 | cookies: { 13 | getAll() { 14 | return cookieStore.getAll() 15 | }, 16 | setAll(cookiesToSet) { 17 | cookiesToSet.forEach(({ name, value, options }) => 18 | cookieStore.set(name, value, options), 19 | ) 20 | }, 21 | }, 22 | }, 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/components/ProfileDeleteButton.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ProfileDeleteDialog } from "./ProfileDeleteDialog" 5 | import { Button } from "./ui/button" 6 | 7 | export default function ProfileDeleteButton() { 8 | const [showDeleteDialog, setShowDeleteDialog] = React.useState(false) 9 | return ( 10 | <> 11 | setShowDeleteDialog(false)} 16 | /> 17 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/useDebouncedValue.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | export const useDebouncedValue = (value: string, delay: number = 300) => { 4 | const [debouncedValue, setDebouncedValue] = useState(value) 5 | const [isDebouncing, setIsDebouncing] = useState(false) 6 | 7 | useEffect(() => { 8 | if (value === debouncedValue) { 9 | return 10 | } 11 | 12 | setIsDebouncing(true) 13 | 14 | const timeout = setTimeout(() => { 15 | setDebouncedValue(value) 16 | setIsDebouncing(false) 17 | }, delay) 18 | 19 | return () => { 20 | clearTimeout(timeout) 21 | setIsDebouncing(false) 22 | } 23 | }, [value, delay, debouncedValue]) 24 | 25 | return { 26 | debouncedValue, 27 | isDebouncing, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Task/TaskNotes.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useDebouncedValue } from "@/hooks/useDebouncedValue" 4 | import { updateTaskNotes } from "@/services/tasks/api" 5 | import { useEffect, useState } from "react" 6 | import { Textarea } from "../ui/textarea" 7 | 8 | type Props = { 9 | taskId: number 10 | taskNotes: string 11 | } 12 | export function TaskNotes({ taskNotes, taskId }: Props) { 13 | const [title, setTitle] = useState(taskNotes) 14 | const { debouncedValue: debouncedTitle } = useDebouncedValue(title, 500) 15 | 16 | useEffect(() => { 17 | if (debouncedTitle !== taskNotes) { 18 | updateTaskNotes(taskId, debouncedTitle) 19 | } 20 | }, [debouncedTitle, taskId, taskNotes]) 21 | 22 | return