├── apps ├── backend │ ├── api │ │ ├── app.js │ │ └── app.ts │ ├── env.d.ts │ ├── public │ │ └── stylesheets │ │ │ └── style.css │ ├── src │ │ ├── types │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ └── routes │ │ │ └── ai.ts │ ├── server.ts │ ├── .env.example │ ├── cjs-shim.ts │ ├── tsconfig.json │ ├── esbuild.config.js │ ├── vercel.json │ ├── env.ts │ ├── package.json │ ├── eslint.config.mjs │ └── app.ts └── ui │ ├── supabase │ └── seed.sql │ ├── .prettierignore │ ├── drizzle │ └── meta │ │ ├── 0000_snapshot.json │ │ └── _journal.json │ ├── constants │ ├── keys.ts │ └── theme.ts │ ├── app │ ├── favicon.ico │ ├── [locale] │ │ ├── (auth) │ │ │ ├── _constants │ │ │ │ └── constraints.ts │ │ │ ├── _components │ │ │ │ ├── ProtectedRoute.tsx │ │ │ │ └── GuestRoute.tsx │ │ │ ├── user │ │ │ │ └── layout.tsx │ │ │ ├── login │ │ │ │ ├── _api.ts │ │ │ │ └── layout.tsx │ │ │ ├── register │ │ │ │ └── layout.tsx │ │ │ ├── update-password │ │ │ │ └── layout.tsx │ │ │ └── reset-password │ │ │ │ └── layout.tsx │ │ ├── _components │ │ │ ├── devtools-styles.tsx │ │ │ ├── HydrateAtoms.tsx │ │ │ ├── BodyProvider.tsx │ │ │ ├── JotaiProvider.tsx │ │ │ └── app-sidebar-inset.tsx │ │ ├── [...not-found] │ │ │ └── page.tsx │ │ ├── subtitles │ │ │ ├── _utils.ts │ │ │ ├── layout.tsx │ │ │ ├── _api │ │ │ │ └── tmdb.ts │ │ │ ├── _components │ │ │ │ ├── detail.tsx │ │ │ │ ├── menu-items.tsx │ │ │ │ └── media-details.tsx │ │ │ └── _atoms.ts │ │ ├── (vocabulary) │ │ │ ├── mine │ │ │ │ ├── layout.tsx │ │ │ │ ├── chart │ │ │ │ │ └── layout.tsx │ │ │ │ └── vocabulary │ │ │ │ │ ├── layout.tsx │ │ │ │ │ └── page.tsx │ │ │ ├── _atoms.ts │ │ │ ├── (index) │ │ │ │ └── layout.tsx │ │ │ ├── _lib │ │ │ │ ├── filters.ts │ │ │ │ ├── vocab-utils.ts │ │ │ │ ├── vocab.ts │ │ │ │ └── LexiconTrie.test.ts │ │ │ ├── layout.tsx │ │ │ ├── _components │ │ │ │ ├── menu.tsx │ │ │ │ ├── cells.tsx │ │ │ │ ├── acquaint-all-dialog.tsx │ │ │ │ └── toggle-button.tsx │ │ │ └── _prompts │ │ │ │ └── getCategory.ts │ │ ├── _hooks │ │ │ └── useAppEffects.ts │ │ ├── global-error.tsx │ │ └── about │ │ │ └── page.tsx │ └── api │ │ ├── categories │ │ ├── schema.ts │ │ └── route.ts │ │ └── trpc │ │ └── [trpc] │ │ └── route.ts │ ├── postcss.config.mjs │ ├── public │ ├── robots.txt │ ├── vercel.svg │ ├── window.svg │ ├── file.svg │ ├── globe.svg │ └── next.svg │ ├── .npmrc │ ├── .browserslistrc │ ├── .stylelintrc.json │ ├── locales │ ├── server.ts │ ├── client.ts │ ├── zh.ts │ └── en.ts │ ├── .env.example │ ├── components │ ├── nav-actions.tsx │ ├── NavPathname.tsx │ ├── ui │ │ ├── skeleton.tsx │ │ ├── html-elements.tsx │ │ ├── label.tsx │ │ ├── toggle.tsx │ │ ├── sidebar.var.tsx │ │ ├── button.tsx │ │ ├── separator.tsx │ │ ├── collapsible.tsx │ │ ├── textarea.tsx │ │ ├── drawer.var.tsx │ │ ├── input.tsx │ │ ├── sonner.tsx │ │ ├── toggle.var.tsx │ │ ├── checkbox.tsx │ │ ├── textarea-input.tsx │ │ ├── popover.tsx │ │ ├── button.var.tsx │ │ ├── hover-card.tsx │ │ ├── alert.tsx │ │ ├── badge.tsx │ │ ├── spinner.tsx │ │ ├── accordion.tsx │ │ ├── resizable.tsx │ │ ├── card.tsx │ │ └── tooltip.tsx │ ├── NoSsr.tsx │ ├── Navigate.tsx │ ├── content-root.tsx │ ├── themes.tsx │ ├── icons │ │ └── arrow.tsx │ ├── login-toast.tsx │ ├── my-table │ │ ├── go-to-last-page.tsx │ │ └── pagination-size-select.tsx │ ├── nav-main.tsx │ ├── my-icon │ │ └── sort-icon.tsx │ ├── side-nav.tsx │ ├── nav-secondary.tsx │ ├── file-input.tsx │ ├── team-switcher.tsx │ ├── search-widget.tsx │ ├── nav-workspaces.tsx │ └── nav-favorites.tsx │ ├── trpc │ ├── client.var.tsx │ ├── routers │ │ ├── _app.ts │ │ └── auth.ts │ ├── server.HydrateClient.tsx │ ├── init.ts │ ├── query-client.ts │ ├── server.tsx │ └── client.tsx │ ├── renovate.json │ ├── atoms │ ├── vocabulary.ts │ ├── file-types.ts │ ├── auth.ts │ ├── store.ts │ └── index.ts │ ├── types │ ├── supabase-js.d.ts │ ├── tanstack.d.ts │ ├── utils.ts │ └── index.ts │ ├── lib │ ├── utils.ts │ ├── supabase │ │ ├── client.ts │ │ ├── middleware.ts │ │ └── server.ts │ ├── db.ts │ └── table-utils.ts │ ├── drizzle.config.ts │ ├── instrumentation.ts │ ├── vitest.config.js │ ├── components.json │ ├── .gitignore │ ├── sentry.server.config.ts │ ├── proxy.ts │ ├── tsconfig.json │ ├── hooks │ └── use-mobile.ts │ ├── sentry.edge.config.ts │ ├── env.ts │ ├── instrumentation-client.ts │ ├── .prettierrc │ └── next.config.ts ├── .gitattributes ├── packages ├── shared-types │ ├── types │ │ ├── index.ts │ │ └── types.ts │ ├── browser.d.ts │ ├── package.json │ └── index.d.ts └── utils │ ├── src │ ├── index.ts │ ├── atoms │ │ ├── index.ts │ │ ├── mediaQueryFamily.ts │ │ └── utilities.ts │ ├── lib │ │ ├── regex.ts │ │ ├── index.ts │ │ ├── merge-refs.ts │ │ ├── union-optional.ts │ │ ├── downloadFile.ts │ │ ├── utils.ts │ │ ├── date-utils.ts │ │ └── bindApply.ts │ ├── types │ │ ├── index.ts │ │ ├── narrow.ts │ │ └── shared-properties.ts │ ├── hooks │ │ ├── index.ts │ │ ├── useStickyValue.ts │ │ ├── useRetimer.ts │ │ ├── useIsEllipsisActive.ts │ │ ├── useAtomEffect.ts │ │ ├── utils.ts │ │ └── useStyleObserver.ts │ └── global.d.ts │ ├── tsconfig.json │ └── package.json ├── pnpm-workspace.yaml ├── .github ├── ISSUE_TEMPLATE │ ├── custom.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── create-diagram.yml ├── taze.config.ts ├── .editorconfig ├── LICENSE ├── tsconfig.json ├── .gitignore ├── README.md ├── patches └── http-proxy@1.18.1.patch ├── public └── tmdb.svg ├── eslint.config.js └── .vscode └── settings.json /apps/backend/api/app.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/backend/env.d.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/ui/supabase/seed.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /apps/backend/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/ui/.prettierignore: -------------------------------------------------------------------------------- 1 | /**/*.* 2 | !/**/*.html 3 | -------------------------------------------------------------------------------- /apps/ui/drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /packages/shared-types/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | -------------------------------------------------------------------------------- /packages/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/union-optional' 2 | -------------------------------------------------------------------------------- /apps/backend/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import '@total-typescript/ts-reset' 2 | -------------------------------------------------------------------------------- /apps/ui/constants/keys.ts: -------------------------------------------------------------------------------- 1 | export const COLOR_MODE_SETTING_KEY = 'color_mode_setting' 2 | -------------------------------------------------------------------------------- /apps/ui/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyle1an/sub-vocab/HEAD/apps/ui/app/favicon.ico -------------------------------------------------------------------------------- /apps/ui/drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [] 5 | } 6 | -------------------------------------------------------------------------------- /apps/ui/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ['@tailwindcss/postcss'], 3 | } 4 | 5 | export default config 6 | -------------------------------------------------------------------------------- /apps/ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Allow: / 3 | Disallow: /private/ 4 | 5 | Sitemap: https://acme.com/sitemap.xml 6 | -------------------------------------------------------------------------------- /packages/utils/src/atoms/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mediaQueryFamily' 2 | export * from './utilities' 3 | export * from './utils' 4 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/(auth)/_constants/constraints.ts: -------------------------------------------------------------------------------- 1 | export const USERNAME_MIN_LENGTH = 3 2 | 3 | export const PASSWORD_MIN_LENGTH = 6 4 | -------------------------------------------------------------------------------- /apps/ui/constants/theme.ts: -------------------------------------------------------------------------------- 1 | export const DARK__BACKGROUND = 'oklch(0.145 0 0)' 2 | export const LIGHT_THEME_COLOR = 'rgb(255, 255, 255)' 3 | -------------------------------------------------------------------------------- /apps/ui/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - apps/* 4 | 5 | catalog: 6 | jotai: ^2.15.2 7 | jotai-effect: ^2.1.5 8 | react: ^19.2.1 9 | -------------------------------------------------------------------------------- /apps/backend/server.ts: -------------------------------------------------------------------------------- 1 | import { fastify } from './app' 2 | 3 | fastify.listen({ port: 5001 }, (error, address) => { 4 | console.log(error, address) 5 | }) 6 | -------------------------------------------------------------------------------- /apps/backend/src/types/utils.ts: -------------------------------------------------------------------------------- 1 | import type { z } from 'zod' 2 | 3 | export type ZodObj> = { 4 | [K in keyof T]: z.ZodType 5 | } 6 | -------------------------------------------------------------------------------- /packages/utils/src/lib/regex.ts: -------------------------------------------------------------------------------- 1 | export function tryGetRegex(pattern: string) { 2 | try { 3 | return new RegExp(pattern) 4 | } catch (e) { 5 | return null 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /apps/ui/.npmrc: -------------------------------------------------------------------------------- 1 | # https://github.com/vercel/next.js/discussions/76247#discussioncomment-12256230 2 | public-hoist-pattern[]=*import-in-the-middle* 3 | public-hoist-pattern[]=*require-in-the-middle* 4 | -------------------------------------------------------------------------------- /apps/ui/app/api/categories/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod/v4-mini' 2 | 3 | export const categoriesSchema = z.object({ 4 | properName: z.array(z.string()), 5 | acronym: z.array(z.string()), 6 | }) 7 | -------------------------------------------------------------------------------- /apps/ui/.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 Chrome versions, last 2 Safari major versions, last 2 iOS major versions, last 2 ChromeAndroid versions, last 2 edge versions, last 2 Opera versions, last 2 Samsung versions, not dead 2 | -------------------------------------------------------------------------------- /packages/utils/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './narrow' 2 | export type * as os from './schema/opensubtitles.d.ts' 3 | export type * as tmdb from './schema/themoviedb.d.ts' 4 | export * from './shared-properties' 5 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/_components/devtools-styles.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react' 2 | import 'jotai-devtools/styles.css' 3 | 4 | const StylingComponent = () => 5 | export default StylingComponent 6 | -------------------------------------------------------------------------------- /packages/shared-types/browser.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | function setTimeout(...parameters: Parameters): ReturnType 3 | } 4 | 5 | export {} 6 | -------------------------------------------------------------------------------- /apps/backend/src/routes/ai.ts: -------------------------------------------------------------------------------- 1 | import { createOpenRouter } from '@openrouter/ai-sdk-provider' 2 | 3 | import { env } from '@backend/env.ts' 4 | 5 | const openrouter = createOpenRouter({ 6 | apiKey: env.OPENROUTER_API_KEY, 7 | }) 8 | -------------------------------------------------------------------------------- /packages/utils/src/types/narrow.ts: -------------------------------------------------------------------------------- 1 | import type { NarrowRaw, NarrowShallow } from '@sub-vocab/shared-types/types' 2 | 3 | export const narrow = (t: NarrowRaw) => t 4 | 5 | export const narrowShallow = (t: NarrowShallow) => t 6 | -------------------------------------------------------------------------------- /taze.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'taze' 2 | 3 | export default defineConfig({ 4 | write: true, 5 | ignorePaths: [ 6 | '**/node_modules/**', 7 | '**/test/**', 8 | ], 9 | packageMode: { 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /apps/ui/.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-recommended", 3 | "rules": { 4 | "at-rule-no-unknown": [ 5 | true, 6 | { 7 | "ignoreAtRules": ["tailwind"] 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/ui/locales/server.ts: -------------------------------------------------------------------------------- 1 | import { createI18nServer } from 'next-international/server' 2 | 3 | export const { getI18n, getScopedI18n, getStaticParams } = createI18nServer({ 4 | en: () => import('./en'), 5 | zh: () => import('./zh'), 6 | }) 7 | -------------------------------------------------------------------------------- /packages/utils/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useAtomEffect' 2 | export * from './useIsEllipsisActive' 3 | export * from './useRetimer' 4 | export * from './useStickyValue' 5 | export * from './useStyleObserver' 6 | export * from './utils' 7 | -------------------------------------------------------------------------------- /apps/ui/.env.example: -------------------------------------------------------------------------------- 1 | COREPACK_ENABLE_STRICT= 2 | NEXT_PUBLIC_SUB_API_URL= 3 | NEXT_PUBLIC_SUPABASE_ANON_KEY= 4 | NEXT_PUBLIC_SUPABASE_URL= 5 | NEXT_PUBLIC_LEGACY_USER_EMAIL_SUFFIX= 6 | PROJECT_REF= 7 | GOOGLE_GENERATIVE_AI_API_KEY= 8 | SENTRY_AUTH_TOKEN= 9 | -------------------------------------------------------------------------------- /apps/ui/components/nav-actions.tsx: -------------------------------------------------------------------------------- 1 | import { TopBar } from '@/components/top-bar' 2 | 3 | export function NavActions() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /apps/ui/trpc/client.var.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { createTRPCContext } from '@trpc/tanstack-react-query' 4 | 5 | import type { AppRouter } from './routers/_app' 6 | 7 | export const { TRPCProvider, useTRPC } = createTRPCContext() 8 | -------------------------------------------------------------------------------- /apps/backend/.env.example: -------------------------------------------------------------------------------- 1 | ENABLE_EXPERIMENTAL_COREPACK= 2 | PUBLIC_SUPABASE_URL= 3 | SUPABASE_ANON_KEY= 4 | POSTGRES_URL= 5 | TMDB_TOKEN= 6 | OPENSUBTITLES_API_KEY= 7 | APP_NAME__V_APP_VERSION= 8 | OPENROUTER_API_KEY= 9 | GOOGLE_GENERATIVE_AI_API_KEY= 10 | -------------------------------------------------------------------------------- /apps/ui/components/NavPathname.tsx: -------------------------------------------------------------------------------- 1 | import { usePathname } from 'next/navigation' 2 | 3 | export function NavPathname({ 4 | children, 5 | }: { 6 | children: (pathname: string) => React.ReactNode 7 | }) { 8 | return children(usePathname()) 9 | } 10 | -------------------------------------------------------------------------------- /apps/ui/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "packageRules": [ 5 | { 6 | "matchPackagePatterns": ["*"], 7 | "rangeStrategy": "replace" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /apps/ui/atoms/vocabulary.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai' 2 | 3 | export const fileInfoAtom = atom('') 4 | fileInfoAtom.debugLabel = 'fileInfoAtom' 5 | export const sourceTextAtom = atom({ 6 | value: '', 7 | epoch: 0, 8 | }) 9 | sourceTextAtom.debugLabel = 'sourceTextAtom' 10 | -------------------------------------------------------------------------------- /apps/ui/trpc/routers/_app.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCRouter } from '@/trpc/init' 2 | import { userRouter } from '@/trpc/routers/auth' 3 | 4 | export const appRouter = createTRPCRouter({ 5 | user: userRouter, 6 | }) 7 | // export type definition of API 8 | export type AppRouter = typeof appRouter 9 | -------------------------------------------------------------------------------- /apps/ui/locales/client.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { createI18nClient } from 'next-international/client' 3 | 4 | export const { useI18n, useChangeLocale, useCurrentLocale, useScopedI18n, I18nProviderClient } = createI18nClient({ 5 | en: () => import('./en'), 6 | zh: () => import('./zh'), 7 | }) 8 | -------------------------------------------------------------------------------- /apps/backend/cjs-shim.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module' 2 | import path from 'node:path' 3 | import url from 'node:url' 4 | 5 | globalThis.require = createRequire(import.meta.url) 6 | globalThis.__filename = url.fileURLToPath(import.meta.url) 7 | globalThis.__dirname = path.dirname(__filename) 8 | -------------------------------------------------------------------------------- /apps/ui/types/supabase-js.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | declare module '@supabase/supabase-js' { 3 | interface UserMetadata { 4 | email: string 5 | email_verified: boolean 6 | phone_verified: boolean 7 | sub: string 8 | username?: string 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/ui/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ClassArray, ClassDictionary, ClassValue } from 'clsx/lite' 2 | 3 | import { clsx } from 'clsx/lite' 4 | import { twMerge } from 'tailwind-merge' 5 | 6 | export function cn(...inputs: Exclude[]) { 7 | return twMerge(clsx(...inputs)) 8 | } 9 | -------------------------------------------------------------------------------- /packages/utils/src/global.d.ts: -------------------------------------------------------------------------------- 1 | import type { MergeDeep } from 'type-fest' 2 | 3 | import '@sub-vocab/shared-types' 4 | 5 | declare module 'es-toolkit' { 6 | export function merge, S extends Record>(target: T, source: S): MergeDeep 7 | } 8 | 9 | export {} 10 | -------------------------------------------------------------------------------- /packages/utils/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bindApply' 2 | export * from './date-utils' 3 | export * from './downloadFile' 4 | export * from './formatDistance' 5 | export * from './merge-refs' 6 | export * from './regex' 7 | export * from './union-optional' 8 | export * from './utilities' 9 | export * from './utils' 10 | -------------------------------------------------------------------------------- /apps/ui/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'drizzle-kit' 2 | 3 | import { env } from './env' 4 | 5 | export default defineConfig({ 6 | schemaFilter: ['auth', 'public'], 7 | dialect: 'postgresql', 8 | out: './drizzle', 9 | schema: './drizzle/schema.ts', 10 | dbCredentials: { 11 | url: env.POSTGRES_URL, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /apps/ui/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<'div'>) { 4 | return ( 5 |
10 | ) 11 | } 12 | 13 | export { Skeleton } 14 | -------------------------------------------------------------------------------- /apps/ui/lib/supabase/client.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserClient } from '@supabase/ssr' 2 | 3 | import type { Database } from '@/types/database.types' 4 | 5 | import { env } from '@/env' 6 | 7 | export function createClient() { 8 | return createBrowserClient( 9 | env.NEXT_PUBLIC_SUPABASE_URL, 10 | env.NEXT_PUBLIC_SUPABASE_ANON_KEY, 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/[...not-found]/page.tsx: -------------------------------------------------------------------------------- 1 | // https://github.com/vercel/next.js/discussions/61823#discussioncomment-8410582 2 | export default function NotFound() { 3 | return ( 4 |
5 |

404

6 |

This page could not be found.

7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/subtitles/_utils.ts: -------------------------------------------------------------------------------- 1 | import type { SubtitleData } from '@/app/[locale]/subtitles/_components/columns' 2 | 3 | export type RowId = { 4 | // https://standardschema.dev/#why-did-you-prefix-the-standard-property-with- 5 | '~rowId'?: string 6 | } 7 | 8 | export const getFileId = (row: SubtitleData & RowId) => row['~rowId'] ?? String(row.subtitle.attributes.files[0]?.file_id ?? '') 9 | -------------------------------------------------------------------------------- /apps/ui/components/NoSsr.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import dynamic from 'next/dynamic' 4 | import { Fragment } from 'react' 5 | 6 | // https://stackoverflow.com/a/57173209 7 | const Component = ({ children }: { children: React.ReactNode }) => ( 8 | {children} 9 | ) 10 | 11 | export const NoSSR = dynamic(() => Promise.resolve(Component), { 12 | ssr: false, 13 | }) 14 | -------------------------------------------------------------------------------- /apps/ui/types/tanstack.d.ts: -------------------------------------------------------------------------------- 1 | import type { RowData } from '@tanstack/react-table' 2 | 3 | import type { RowSelectionChangeFn } from './utils' 4 | 5 | declare module '@tanstack/react-table' { 6 | interface CellContext { 7 | onExpandedChange?: (expanded: boolean) => void 8 | onRowSelectionChange?: RowSelectionChangeFn | undefined 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/ui/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | # https://github.com/microsoft/vscode/issues/6447#issuecomment-221075881 9 | # indent_size = 2 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | insert_final_newline = false 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /apps/backend/api/app.ts: -------------------------------------------------------------------------------- 1 | import type { VercelRequest, VercelResponse } from '@vercel/node' 2 | 3 | import { fastify } from '@backend/app.ts' 4 | 5 | // https://github.com/vercel/examples/blob/main/solutions/node-hello-world/api/hello.ts 6 | export default async function handler(req: VercelRequest, res: VercelResponse) { 7 | await fastify.ready() 8 | fastify.server.emit('request', req, res) 9 | } 10 | -------------------------------------------------------------------------------- /apps/ui/instrumentation.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/nextjs' 2 | 3 | export async function register() { 4 | if (process.env.NEXT_RUNTIME === 'nodejs') { 5 | await import('./sentry.server.config') 6 | } 7 | 8 | if (process.env.NEXT_RUNTIME === 'edge') { 9 | await import('./sentry.edge.config') 10 | } 11 | } 12 | 13 | export const onRequestError = Sentry.captureRequestError 14 | -------------------------------------------------------------------------------- /apps/ui/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/(vocabulary)/mine/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ContentRoot } from '@/components/content-root' 2 | 3 | export default function MineLayout({ children }: LayoutProps<'/[locale]/mine'>) { 4 | return ( 5 | 6 |
7 | {children} 8 |
9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /packages/utils/src/hooks/useStickyValue.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | export function useStickyValue( 4 | value: T, 5 | predicate: (v: T) => boolean = Boolean, 6 | ): T { 7 | const [sticky, setSticky] = useState(value) 8 | const isValid = predicate(value) 9 | if (sticky !== value && isValid) { 10 | setSticky(value) 11 | } 12 | return isValid ? value : sticky 13 | } 14 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/subtitles/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ContentRoot } from '@/components/content-root' 2 | 3 | export default function Layout({ children }: LayoutProps<'/[locale]/subtitles'>) { 4 | return ( 5 | 6 |
7 | {children} 8 |
9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 5 | "module": "es2022", 6 | "moduleResolution": "bundler", 7 | "noEmit": true, 8 | "outDir": "./dist" 9 | }, 10 | "include": [".eslintrc.js", ".eslintrc.cjs", "**/*.config.js", "**/*.config.cjs", "**/*.config.ts", "**/*.js", "**/*.ts", "**/*.d.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/(vocabulary)/_atoms.ts: -------------------------------------------------------------------------------- 1 | import type { REALTIME_SUBSCRIBE_STATES } from '@supabase/supabase-js' 2 | 3 | import { atom } from 'jotai' 4 | 5 | export const vocabSubscriptionAtom = atom(null) 6 | vocabSubscriptionAtom.debugLabel = 'vocabSubscriptionAtom' 7 | 8 | export const isSourceTextStaleAtom = atom(false) 9 | isSourceTextStaleAtom.debugLabel = 'isSourceTextStaleAtom' 10 | -------------------------------------------------------------------------------- /apps/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "module": "es2022", 6 | "moduleResolution": "bundler", 7 | "outDir": "./dist", 8 | "lib": ["es2024"] 9 | }, 10 | "include": [".eslintrc.js", ".eslintrc.cjs", "**/*.config.js", "**/*.config.cjs", "**/*.config.ts", "**/*.js", "**/*.ts", "src/**/*.ts", "**/*.d.ts", "src/**/*.d.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/ui/trpc/server.HydrateClient.tsx: -------------------------------------------------------------------------------- 1 | import { dehydrate, HydrationBoundary } from '@tanstack/react-query' 2 | 3 | import { getQueryClient } from '@/trpc/server' 4 | 5 | export function HydrateClient(props: { children: React.ReactNode }) { 6 | const queryClient = getQueryClient() 7 | return ( 8 | 9 | {props.children} 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /packages/utils/src/lib/merge-refs.ts: -------------------------------------------------------------------------------- 1 | import type * as React from 'react' 2 | 3 | export function mergeRefs( 4 | ...inputRefs: (React.Ref | undefined)[] 5 | ): React.RefCallback { 6 | return (ref) => { 7 | for (const inputRef of inputRefs) { 8 | if (typeof inputRef === 'function') { 9 | inputRef(ref) 10 | } else if (inputRef) { 11 | inputRef.current = ref 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/subtitles/_api/tmdb.ts: -------------------------------------------------------------------------------- 1 | import createFetchClient from 'openapi-fetch' 2 | import createClient from 'openapi-react-query' 3 | 4 | import type { tmdb } from '@sub-vocab/utils/types' 5 | 6 | import { env } from '@/env' 7 | 8 | const baseUrl = env.NEXT_PUBLIC_SUB_API_URL 9 | 10 | const fetchClient = createFetchClient({ 11 | baseUrl: `${baseUrl}/tmdb-proxy`, 12 | }) 13 | export const $api = createClient(fetchClient) 14 | -------------------------------------------------------------------------------- /apps/ui/vitest.config.js: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import tsconfigPaths from 'vite-tsconfig-paths' 3 | import { configDefaults, defineConfig } from 'vitest/config' 4 | 5 | export default defineConfig({ 6 | plugins: [tsconfigPaths(), react()], 7 | test: { 8 | environment: 'jsdom', 9 | exclude: [...configDefaults.exclude, 'packages/template/*'], 10 | coverage: { 11 | enabled: true, 12 | }, 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /packages/shared-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sub-vocab/shared-types", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "description": "description", 6 | "license": "MIT", 7 | "exports": { 8 | ".": { 9 | "types": "./index.d.ts" 10 | }, 11 | "./browser": { 12 | "types": "./browser.d.ts" 13 | }, 14 | "./types": "./types/index.ts" 15 | }, 16 | "main": "index.js", 17 | "types": "index.d.ts" 18 | } 19 | -------------------------------------------------------------------------------- /apps/ui/app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import { fetchRequestHandler } from '@trpc/server/adapters/fetch' 2 | 3 | import { createTRPCContext } from '@/trpc/init' 4 | import { appRouter } from '@/trpc/routers/_app' 5 | 6 | const handler = (req: Request) => 7 | fetchRequestHandler({ 8 | endpoint: '/api/trpc', 9 | req, 10 | router: appRouter, 11 | createContext: createTRPCContext, 12 | }) 13 | export { handler as GET, handler as POST } 14 | -------------------------------------------------------------------------------- /packages/utils/src/atoms/mediaQueryFamily.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from 'effect' 2 | import { atomFamily } from 'jotai/utils' 3 | 4 | import { atomWithMediaQuery, withUnmountCallbackAtom, withUseA } from './utils' 5 | 6 | export const mediaQueryFamily = withUseA(atomFamily((query: string) => { 7 | return pipe( 8 | atomWithMediaQuery(query), 9 | (x) => withUnmountCallbackAtom(x, () => { 10 | mediaQueryFamily.remove(query) 11 | }), 12 | ) 13 | })) 14 | -------------------------------------------------------------------------------- /packages/shared-types/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { Entries } from './types' 2 | 3 | declare global { 4 | interface ObjectConstructor { 5 | // assign(target: T, source: U): T & Merge 6 | // https://github.com/microsoft/TypeScript/issues/35101#issue-522767105 7 | // eslint-disable-next-line ts/method-signature-style 8 | entries(o: T): T extends ArrayLike ? [string, U][] : Entries 9 | } 10 | } 11 | 12 | export {} 13 | -------------------------------------------------------------------------------- /apps/backend/esbuild.config.js: -------------------------------------------------------------------------------- 1 | import { build } from 'esbuild' 2 | 3 | build({ 4 | entryPoints: ['api/app.ts'], 5 | outdir: 'api', 6 | allowOverwrite: true, 7 | bundle: true, 8 | platform: 'node', 9 | target: 'node22', 10 | format: 'esm', 11 | // sourcemap: true, 12 | minify: true, 13 | treeShaking: true, 14 | // https://github.com/evanw/esbuild/issues/1921#issuecomment-1898197331 15 | inject: ['cjs-shim.ts'], 16 | legalComments: 'external', 17 | }) 18 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/(vocabulary)/(index)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ContentRoot } from '@/components/content-root' 2 | 3 | export default function Layout({ children }: LayoutProps<'/[locale]'>) { 4 | return ( 5 | 6 |
7 |
8 | {children} 9 |
10 |
11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/create-diagram.yml: -------------------------------------------------------------------------------- 1 | name: Create diagram 2 | on: 3 | workflow_dispatch: { } 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | get_data: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@master 13 | - name: Update diagram 14 | uses: githubocto/repo-visualizer@main 15 | with: 16 | excluded_paths: "ignore,.github" 17 | excluded_globs: "**/*.gitignore;**/package-lock.json;**/pnpm-lock.yaml" 18 | -------------------------------------------------------------------------------- /apps/ui/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /apps/ui/atoms/file-types.ts: -------------------------------------------------------------------------------- 1 | import type { ExtractAtomValue } from 'jotai' 2 | 3 | import { atomWithStorage } from 'jotai/utils' 4 | 5 | import { SUPPORTED_FILE_EXTENSIONS } from '@/lib/filesHandler' 6 | 7 | export const fileTypesAtom = atomWithStorage( 8 | 'fileTypesAtom', 9 | SUPPORTED_FILE_EXTENSIONS.sort((a, b) => a.localeCompare(b)).map((type) => ({ 10 | type, 11 | checked: true, 12 | })), 13 | ) 14 | fileTypesAtom.debugLabel = 'fileTypesAtom' 15 | 16 | export type FileType = ExtractAtomValue[number] 17 | -------------------------------------------------------------------------------- /apps/ui/components/Navigate.tsx: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/a/76242460 2 | import { useRouter } from 'next/navigation' 3 | import { useEffect } from 'react' 4 | 5 | type NavigateProps = { to: string, replace?: boolean } 6 | 7 | export default function Navigate({ to, replace = false }: NavigateProps): null { 8 | const router = useRouter() 9 | 10 | useEffect(() => { 11 | if (replace) { 12 | router.replace(to) 13 | } else { 14 | router.push(to) 15 | } 16 | }, [replace, router, to]) 17 | 18 | return null 19 | } 20 | -------------------------------------------------------------------------------- /apps/ui/app/api/categories/route.ts: -------------------------------------------------------------------------------- 1 | import { google } from '@ai-sdk/google' 2 | import { streamObject } from 'ai' 3 | 4 | import { categoriesSchema } from '@/app/api/categories/schema' 5 | 6 | export const maxDuration = 60 7 | 8 | export async function POST(req: Request) { 9 | const context = await req.json() 10 | 11 | const result = streamObject({ 12 | model: google('gemini-2.5-flash-lite'), 13 | schema: categoriesSchema, 14 | prompt: 15 | `${context}`, 16 | }) 17 | 18 | return result.toTextStreamResponse() 19 | } 20 | -------------------------------------------------------------------------------- /apps/ui/components/ui/html-elements.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from '@radix-ui/react-slot' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | export interface DivProps extends 6 | React.ComponentProps<'div'> {} 7 | 8 | export function Div({ 9 | className, 10 | asChild = false, 11 | children, 12 | ...props 13 | }: DivProps & { 14 | asChild?: boolean 15 | }) { 16 | const Component = asChild ? Slot : 'div' 17 | return ( 18 | 22 | {children} 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/(auth)/_components/ProtectedRoute.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useAtom } from 'jotai' 4 | 5 | import { authChangeEventAtom, userAtom } from '@/atoms/auth' 6 | import Navigate from '@/components/Navigate' 7 | 8 | export function ProtectedRoute({ children }: { children: React.ReactNode }) { 9 | const [user] = useAtom(userAtom) 10 | const [authChangeEvent] = useAtom(authChangeEventAtom) 11 | if (!authChangeEvent) { 12 | return null 13 | } 14 | 15 | if (!user) { 16 | return 17 | } 18 | 19 | return children 20 | } 21 | -------------------------------------------------------------------------------- /packages/utils/src/hooks/useRetimer.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | 3 | export function useRetimer() { 4 | const timeoutIdRef = useRef(undefined) 5 | return (handler?: () => void, timeout?: number | null) => { 6 | if (typeof timeoutIdRef.current === 'number') { 7 | clearTimeout(timeoutIdRef.current) 8 | } 9 | 10 | if (handler) { 11 | if (typeof timeout === 'number' || typeof timeout === 'undefined') { 12 | timeoutIdRef.current = window.setTimeout(handler, timeout) 13 | } else { 14 | handler() 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/backend/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "git": { 4 | "deploymentEnabled": { 5 | "alpha": false 6 | } 7 | }, 8 | "builds": [ 9 | { 10 | "src": "api/app.js", 11 | "use": "@vercel/node" 12 | }, 13 | { 14 | "src": "public/**", 15 | "use": "@vercel/static" 16 | } 17 | ], 18 | "rewrites": [ 19 | { 20 | "source": "/(.*)", 21 | "destination": "api/app.js" 22 | }, 23 | { 24 | "source": "/(.+\\.[a-z]+)$", 25 | "destination": "/public/$1" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /packages/utils/src/hooks/useIsEllipsisActive.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | export function useIsEllipsisActive() { 4 | const [isEllipsisActive, setIsEllipsisActive] = useState(false) 5 | 6 | function handleOnMouseOver(ev: React.MouseEvent) { 7 | const el = ev.target as T 8 | // https://stackoverflow.com/a/10017343 9 | const isActive = el.offsetWidth < el.scrollWidth 10 | if (isEllipsisActive !== isActive) { 11 | setIsEllipsisActive(isActive) 12 | } 13 | } 14 | 15 | return [isEllipsisActive, handleOnMouseOver] as const 16 | } 17 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/(auth)/_components/GuestRoute.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import type React from 'react' 4 | 5 | import { useAtom } from 'jotai' 6 | 7 | import { authChangeEventAtom, userAtom } from '@/atoms/auth' 8 | import Navigate from '@/components/Navigate' 9 | 10 | export function GuestRoute({ children }: { children: React.ReactNode }) { 11 | const [user] = useAtom(userAtom) 12 | const [authChangeEvent] = useAtom(authChangeEventAtom) 13 | if (!authChangeEvent) { 14 | return null 15 | } 16 | 17 | if (user) { 18 | return 19 | } 20 | 21 | return children 22 | } 23 | -------------------------------------------------------------------------------- /packages/utils/src/hooks/useAtomEffect.ts: -------------------------------------------------------------------------------- 1 | import type { DependencyList } from 'react' 2 | 3 | import { atomEffect } from 'jotai-effect' 4 | import { useAtomValue } from 'jotai/react' 5 | import { useCallbackOne as useStableCallback, useMemoOne as useStableMemo } from 'use-memo-one' 6 | 7 | type EffectFn = Parameters[0] 8 | 9 | // https://jotai.org/docs/recipes/use-atom-effect 10 | export function useAtomEffect(effectFn: EffectFn, inputs: DependencyList | undefined) { 11 | const effect = useStableCallback(effectFn, [inputs]) 12 | useAtomValue(useStableMemo(() => atomEffect(effect), [effect])) 13 | } 14 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/(vocabulary)/mine/chart/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function Layout({ children }: LayoutProps<'/[locale]/mine/chart'>) { 2 | return ( 3 |
4 |
5 |
6 |
7 | Acquainted Vocabulary 8 |
9 |
10 |
11 | {children} 12 |
13 |
14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /apps/ui/components/content-root.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from '@radix-ui/react-slot' 2 | 3 | import type { DivProps } from '@/components/ui/html-elements' 4 | 5 | import { cn } from '@/lib/utils' 6 | 7 | export function ContentRoot({ 8 | className, 9 | asChild = false, 10 | children, 11 | ...props 12 | }: DivProps & { 13 | asChild?: boolean 14 | }) { 15 | const Component = asChild ? Slot : 'div' 16 | return ( 17 | 22 | {children} 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/subtitles/_components/detail.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | 3 | export function Detail({ 4 | className, 5 | children, 6 | ...props 7 | }: React.ComponentProps<'div'>) { 8 | return ( 9 |
13 |
16 |
19 | {children} 20 |
21 |
22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/(vocabulary)/mine/vocabulary/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function Layout({ children }: LayoutProps<'/[locale]/mine/vocabulary'>) { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {children} 12 |
13 |
14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /apps/ui/components/themes.tsx: -------------------------------------------------------------------------------- 1 | import type { ArrayValues } from 'type-fest' 2 | 3 | const DEFAULT_MODE = { 4 | value: 'auto', 5 | label: 'Auto', 6 | icon: , 7 | } as const 8 | 9 | export const COLOR_MODE = { 10 | DEFAULT: DEFAULT_MODE, 11 | ALL: [ 12 | { 13 | value: 'light', 14 | label: 'Light', 15 | icon: , 16 | }, 17 | { 18 | value: 'dark', 19 | label: 'Dark', 20 | icon: , 21 | }, 22 | DEFAULT_MODE, 23 | ] as const, 24 | } 25 | 26 | export type ColorModeValue = ArrayValues['value'] 27 | -------------------------------------------------------------------------------- /packages/utils/src/atoms/utilities.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from 'effect' 2 | import { atom } from 'jotai' 3 | 4 | import { withAbortableMount, withReadonly } from '../atoms/utils' 5 | import { isServer } from '../lib/utilities' 6 | 7 | export const documentVisibilityStateAtom = (() => { 8 | if (isServer) return atom(() => 'hidden') 9 | return pipe( 10 | atom(document.visibilityState), 11 | (x) => withAbortableMount(x, (setAtom, signal) => { 12 | const listener = () => { 13 | setAtom(document.visibilityState) 14 | } 15 | document.addEventListener('visibilitychange', listener, { signal }) 16 | listener() 17 | }), 18 | withReadonly, 19 | ) 20 | })() 21 | -------------------------------------------------------------------------------- /apps/ui/types/utils.ts: -------------------------------------------------------------------------------- 1 | import type { CheckedState } from '@radix-ui/react-checkbox' 2 | import type { Row, RowData } from '@tanstack/react-table' 3 | import type { PartialDeep } from 'type-fest' 4 | import type z from 'zod/v4-mini' 5 | 6 | export type RowSelectionChangeFn = (checked: CheckedState, row: Row, mode?: 'singleRow' | 'singleSubRow') => void 7 | 8 | // https://github.com/colinhacks/zod/issues/53#issuecomment-1386446580 9 | export type ZodObj> = { 10 | [key in keyof T]: z.ZodMiniType 11 | } 12 | 13 | export type PartialObjectZ = PartialDeep 14 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/_components/HydrateAtoms.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import type { User } from '@supabase/supabase-js' 4 | 5 | import { useHydrateAtoms } from 'jotai/utils' 6 | 7 | import type { ColorModeValue } from '@/components/themes' 8 | 9 | import { colorModeSettingAtom } from '@/atoms' 10 | import { userAtom } from '@/atoms/auth' 11 | 12 | export const HydrateAtoms = ({ 13 | colorModeSetting, 14 | user, 15 | children, 16 | }: { 17 | colorModeSetting: ColorModeValue 18 | user: User | undefined | null 19 | children?: React.ReactNode 20 | }) => { 21 | useHydrateAtoms([ 22 | [colorModeSettingAtom, colorModeSetting], 23 | [userAtom, user], 24 | ]) 25 | return children 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /apps/ui/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as LabelPrimitive from '@radix-ui/react-label' 2 | import * as React from 'react' 3 | 4 | import { cn } from '@/lib/utils' 5 | 6 | function Label({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ) 20 | } 21 | 22 | export { Label } 23 | -------------------------------------------------------------------------------- /apps/ui/components/ui/toggle.tsx: -------------------------------------------------------------------------------- 1 | import type { VariantProps } from 'class-variance-authority' 2 | 3 | import * as TogglePrimitive from '@radix-ui/react-toggle' 4 | import * as React from 'react' 5 | 6 | import { toggleVariants } from '@/components/ui/toggle.var' 7 | import { cn } from '@/lib/utils' 8 | 9 | function Toggle({ 10 | className, 11 | variant, 12 | size, 13 | ...props 14 | }: React.ComponentProps 15 | & VariantProps) { 16 | return ( 17 | 22 | ) 23 | } 24 | 25 | export { Toggle } 26 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/(vocabulary)/_lib/filters.ts: -------------------------------------------------------------------------------- 1 | import type { VocabularySourceState } from '@/app/[locale]/(vocabulary)/_lib/vocab' 2 | import type { ColumnFilterFn } from '@/lib/table-utils' 3 | 4 | import { tryGetRegex } from '@sub-vocab/utils/lib' 5 | 6 | export function searchFilterValue(search: string, usingRegex: boolean): ColumnFilterFn | undefined { 7 | if (usingRegex) { 8 | const newRegex = tryGetRegex(search) 9 | if (newRegex) { 10 | return (row) => newRegex.test(row.trackedWord.form) 11 | } 12 | } else { 13 | search = search.toLowerCase() 14 | return (row) => row.wordFamily.some((word) => word.pathe.toLowerCase().includes(search)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/ui/lib/db.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js' 2 | import { drizzle } from 'drizzle-orm/postgres-js' 3 | import postgres from 'postgres' 4 | 5 | import type { Database } from '@/types/database.types.ts' 6 | 7 | import * as schema from '@/drizzle/schema.ts' 8 | import { env } from '@/env.ts' 9 | 10 | const connectionString = env.POSTGRES_URL 11 | // https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pooler 12 | const client = postgres(connectionString, { prepare: false }) 13 | const db = drizzle({ client, schema }) 14 | 15 | const supabase = createClient(env.NEXT_PUBLIC_SUPABASE_URL, env.NEXT_PUBLIC_SUPABASE_ANON_KEY) 16 | 17 | export { 18 | db, 19 | supabase, 20 | } 21 | -------------------------------------------------------------------------------- /apps/ui/.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | # Sentry Config File 44 | .env.sentry-build-plugin 45 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/_hooks/useAppEffects.ts: -------------------------------------------------------------------------------- 1 | import { useAtom } from 'jotai' 2 | 3 | import { metaThemeColorAtom } from '@/atoms' 4 | import { authChangeEventAtom, userAtom } from '@/atoms/auth' 5 | import { createClient } from '@/lib/supabase/client' 6 | import { useAtomEffect } from '@sub-vocab/utils/hooks' 7 | 8 | export function useAppEffects() { 9 | useAtomEffect((_, set) => { 10 | const supabase = createClient() 11 | const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => { 12 | set(authChangeEventAtom, event) 13 | set(userAtom, session?.user) 14 | }) 15 | return () => { 16 | subscription.unsubscribe() 17 | } 18 | }, []) 19 | useAtom(metaThemeColorAtom) 20 | } 21 | -------------------------------------------------------------------------------- /apps/ui/components/ui/sidebar.var.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | export const SIDEBAR_COOKIE_NAME = 'sidebar_state' 6 | 7 | type SidebarContextProps = { 8 | state: 'expanded' | 'collapsed' 9 | open: boolean 10 | setOpen: (open: boolean) => void 11 | openMobile: boolean 12 | setOpenMobile: (open: boolean) => void 13 | isMobile: boolean 14 | toggleSidebar: () => void 15 | } 16 | 17 | export const SidebarContext = React.createContext(null) 18 | 19 | export function useSidebar() { 20 | const context = React.use(SidebarContext) 21 | if (!context) { 22 | throw new Error('useSidebar must be used within a SidebarProvider.') 23 | } 24 | 25 | return context 26 | } 27 | -------------------------------------------------------------------------------- /apps/ui/trpc/init.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC } from '@trpc/server' 2 | import { cache } from 'react' 3 | 4 | export const createTRPCContext = cache(async () => { 5 | /** 6 | * @see: https://trpc.io/docs/server/context 7 | */ 8 | return {} 9 | }) 10 | // Avoid exporting the entire t-object 11 | // since it's not very descriptive. 12 | // For instance, the use of a t variable 13 | // is common in i18n libraries. 14 | const t = initTRPC.create({ 15 | /** 16 | * @see https://trpc.io/docs/server/data-transformers 17 | */ 18 | // transformer: superjson, 19 | }) 20 | // Base router and procedure helpers 21 | export const createTRPCRouter = t.router 22 | export const createCallerFactory = t.createCallerFactory 23 | export const baseProcedure = t.procedure 24 | -------------------------------------------------------------------------------- /apps/backend/env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from '@t3-oss/env-core' 2 | import process from 'node:process' 3 | import { z } from 'zod' 4 | 5 | export const env = createEnv({ 6 | server: { 7 | PUBLIC_SUPABASE_URL: z.url(), 8 | SUPABASE_ANON_KEY: z.string().min(1), 9 | POSTGRES_URL: z.url(), 10 | TMDB_TOKEN: z.string().min(1), 11 | OPENSUBTITLES_TOKEN: z.string().optional(), 12 | OPENSUBTITLES_API_KEY: z.string().min(1), 13 | APP_NAME__V_APP_VERSION: z.string().min(1), 14 | OPENROUTER_API_KEY: z.string().min(1), 15 | // https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai#provider-instance 16 | GOOGLE_GENERATIVE_AI_API_KEY: z.string().min(1), 17 | }, 18 | runtimeEnv: process.env, 19 | emptyStringAsUndefined: true, 20 | }) 21 | -------------------------------------------------------------------------------- /packages/utils/src/lib/union-optional.ts: -------------------------------------------------------------------------------- 1 | import type { KeysOfUnion, Simplify } from 'type-fest' 2 | 3 | type UnionOptionalInner = KeysOfUnion> = Simplify< 4 | // 1. For each member of the union (Note: `T extends any` is distributive) 5 | BaseType extends object 6 | ? ( 7 | // 2. Preserve the original type 8 | & BaseType 9 | // 3. And map other keys to `{ key?: undefined }` 10 | & { [K in Exclude]?: undefined } 11 | ) 12 | : never 13 | > 14 | 15 | export type UnionOptional = UnionOptionalInner 16 | 17 | export function unionOptional(obj: T) { 18 | return obj as UnionOptional 19 | } 20 | -------------------------------------------------------------------------------- /apps/ui/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import type { VariantProps } from 'class-variance-authority' 2 | 3 | import { Slot } from '@radix-ui/react-slot' 4 | import * as React from 'react' 5 | 6 | import { buttonVariants } from '@/components/ui/button.var' 7 | import { cn } from '@/lib/utils' 8 | 9 | function Button({ 10 | className, 11 | variant, 12 | size, 13 | asChild = false, 14 | ...props 15 | }: React.ComponentProps<'button'> 16 | & VariantProps & { 17 | asChild?: boolean 18 | }) { 19 | const Comp = asChild ? Slot : 'button' 20 | 21 | return ( 22 | 27 | ) 28 | } 29 | 30 | export { Button } 31 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/global-error.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as Sentry from '@sentry/nextjs' 4 | import NextError from 'next/error' 5 | import { useEffect } from 'react' 6 | 7 | export default function GlobalError({ error }: { error: Error & { digest?: string } }) { 8 | useEffect(() => { 9 | Sentry.captureException(error) 10 | }, [error]) 11 | 12 | return ( 13 | 14 | 15 | {/* `NextError` is the default Next.js error page component. Its type 16 | definition requires a `statusCode` prop. However, since the App Router 17 | does not expose status codes for errors, we simply pass 0 to render a 18 | generic error message. */} 19 | 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /apps/ui/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as SeparatorPrimitive from '@radix-ui/react-separator' 4 | import * as React from 'react' 5 | 6 | import { cn } from '@/lib/utils' 7 | 8 | function Separator({ 9 | className, 10 | orientation = 'horizontal', 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 26 | ) 27 | } 28 | 29 | export { Separator } 30 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/_components/BodyProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useStore } from 'jotai' 4 | import { useRef } from 'react' 5 | 6 | import { bodyBgColorAtom } from '@/atoms' 7 | import { useStyleObserver } from '@sub-vocab/utils/hooks' 8 | 9 | export function Body({ 10 | children, 11 | }: { 12 | children?: React.ReactNode 13 | }) { 14 | const ref = useRef(null) 15 | const store = useStore() 16 | useStyleObserver(ref, ([{ value }]) => { 17 | store.set(bodyBgColorAtom, value) 18 | }, { 19 | properties: ['background-color'], 20 | }) 21 | return ( 22 | 26 | {children} 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /apps/ui/sentry.server.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from '@sentry/nextjs' 6 | 7 | Sentry.init({ 8 | dsn: 'https://c85c12d1ecc241558e8aa3bc55dea61f@o4505257329098752.ingest.us.sentry.io/4505257332178944', 9 | 10 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. 11 | tracesSampleRate: 1, 12 | 13 | // Enable logs to be sent to Sentry 14 | enableLogs: true, 15 | 16 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 17 | debug: false, 18 | }) 19 | -------------------------------------------------------------------------------- /apps/ui/lib/table-utils.ts: -------------------------------------------------------------------------------- 1 | import type { FilterFn, Row } from '@tanstack/react-table' 2 | import type { SetParameterType } from 'type-fest' 3 | 4 | export const sortBySelection = (rowA: Row, rowB: Row) => { 5 | const a = rowA.getIsSelected() ? 1 : 0 6 | const b = rowB.getIsSelected() ? 1 : 0 7 | return a - b 8 | } 9 | 10 | export const noFilter = () => true 11 | 12 | export type ColumnFilterFn = (rowValue: T) => boolean 13 | 14 | type MyFilterFn = Parameters, { 2: ColumnFilterFn }>> 15 | 16 | export const filterFn: (...args: MyFilterFn) => boolean = (row, columnId, filter) => filter(row.original) 17 | 18 | export const combineFilters: (filters: ColumnFilterFn[]) => ColumnFilterFn = (filters) => (rowValue) => filters.every((f) => f(rowValue)) 19 | -------------------------------------------------------------------------------- /apps/ui/trpc/query-client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultShouldDehydrateQuery, 3 | QueryClient, 4 | } from '@tanstack/react-query' 5 | import ms from 'ms' 6 | import superjson from 'superjson' 7 | 8 | export function makeQueryClient() { 9 | return new QueryClient({ 10 | defaultOptions: { 11 | queries: { 12 | gcTime: ms('60min'), 13 | staleTime: ms('45min'), 14 | retry: 2, 15 | retryDelay: (failureCount) => (failureCount - 1) * ms('1s'), 16 | }, 17 | dehydrate: { 18 | serializeData: superjson.serialize, 19 | shouldDehydrateQuery: (query) => 20 | defaultShouldDehydrateQuery(query) 21 | || query.state.status === 'pending', 22 | }, 23 | hydrate: { 24 | deserializeData: superjson.deserialize, 25 | }, 26 | }, 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/(vocabulary)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { use } from 'react' 2 | 3 | import { irregularWordsQueryOptions, sharedVocabularyOptions, userVocabularyOptions } from '@/app/[locale]/(vocabulary)/_api' 4 | import { createClient, getUser } from '@/lib/supabase/server' 5 | import { getQueryClient } from '@/trpc/server' 6 | import { HydrateClient } from '@/trpc/server.HydrateClient' 7 | 8 | export default function Layout({ children }: LayoutProps<'/[locale]'>) { 9 | const queryClient = getQueryClient() 10 | queryClient.prefetchQuery(sharedVocabularyOptions()) 11 | queryClient.prefetchQuery(irregularWordsQueryOptions()) 12 | const { data: { user } } = use(getUser()) 13 | if (user?.id) { 14 | queryClient.prefetchQuery(userVocabularyOptions(user?.id, use(createClient()))) 15 | } 16 | return ( 17 | 18 | {children} 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/subtitles/_components/menu-items.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | 3 | import { Button } from '@/components/ui/button' 4 | 5 | export function RefetchButton({ 6 | isFetching, 7 | refetch, 8 | }: { 9 | isFetching: boolean 10 | refetch: () => void 11 | }) { 12 | return ( 13 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /apps/ui/proxy.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest } from 'next/server' 2 | 3 | import { createI18nMiddleware } from 'next-international/middleware' 4 | 5 | import { updateSession } from '@/lib/supabase/middleware' 6 | 7 | const I18nMiddleware = createI18nMiddleware({ 8 | locales: ['en', 'zh'], 9 | defaultLocale: 'en', 10 | urlMappingStrategy: 'rewrite', 11 | }) 12 | 13 | export function proxy(request: NextRequest) { 14 | // https://next-intl.dev/docs/routing/middleware#example-integrating-with-supabase-authentication 15 | const response = I18nMiddleware(request) 16 | return updateSession(request, response) 17 | } 18 | 19 | export const config = { 20 | // Match all pathnames except for 21 | // - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel` 22 | // - … the ones containing a dot (e.g. `favicon.ico`) 23 | matcher: ['/((?!api|static|trpc|_next|_vercel|.*\\..*).*)'], 24 | } 25 | -------------------------------------------------------------------------------- /apps/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": [ 5 | ], 6 | "incremental": true, 7 | "jsx": "preserve", 8 | "module": "esnext", 9 | "moduleResolution": "bundler", 10 | "paths": { 11 | "@backend/*": ["./../backend/*"], 12 | "@/*": ["./*"] 13 | }, 14 | "resolveJsonModule": true, 15 | "allowJs": true, 16 | "strict": true, 17 | "noEmit": true, 18 | "esModuleInterop": true, 19 | "isolatedModules": true, 20 | "skipLibCheck": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ], 26 | "lib": [ 27 | "ESNext", 28 | "DOM", 29 | "DOM.Iterable" 30 | ] 31 | }, 32 | "include": [ 33 | "next-env.d.ts", 34 | "**/*.ts", 35 | "**/*.tsx", 36 | ".next/types/**/*.ts" 37 | ], 38 | "exclude": ["node_modules"] 39 | } 40 | -------------------------------------------------------------------------------- /apps/ui/app/[locale]/about/page.tsx: -------------------------------------------------------------------------------- 1 | import IconTMDB from '@/../../public/tmdb.svg' 2 | import { ContentRoot } from '@/components/content-root' 3 | 4 | export default function AboutPage() { 5 | return ( 6 | 7 |
8 |
9 | 15 | 19 | 20 |
21 | {/* https://developer.themoviedb.org/docs/faq#what-are-the-attribution-requirements */} 22 | This product uses the TMDB API but is not endorsed or certified by TMDB. 23 |
24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /apps/ui/components/icons/arrow.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | export function ArrowOutwardRounded({ className = '', ...props }: SVGProps) { 6 | return ( 7 | 8 | 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /apps/ui/components/login-toast.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import { Button } from '@/components/ui/button' 4 | 5 | export function LoginToast() { 6 | return ( 7 |
8 |

9 | Login required 10 |

11 |
12 |
13 | Please log in to mark words. 14 |
15 |
16 | 28 |
29 |
30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /apps/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sub-vocab/backend", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "packageManager": "pnpm@10.24.0", 7 | "description": "description", 8 | "scripts": { 9 | "build": "tsc && node esbuild.config.js", 10 | "dev": "tsx watch --trace-deprecation --env-file=.env server", 11 | "lint": "eslint . --report-unused-disable-directives", 12 | "lint:fix": "eslint . --fix", 13 | "start": "node --env-file=.env app.js", 14 | "vercel-build": "node --run build" 15 | }, 16 | "dependencies": { 17 | "@fastify/cors": "^11.1.0", 18 | "@fastify/http-proxy": "^11.4.1", 19 | "@openrouter/ai-sdk-provider": "^1.4.0", 20 | "@sub-vocab/utils": "workspace:*", 21 | "fastify": "^5.6.2", 22 | "ofetch": "^1.5.1" 23 | }, 24 | "devDependencies": { 25 | "@vercel/node": "^5.5.15", 26 | "esbuild": "^0.27.1", 27 | "tsx": "^4.21.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/ui/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from 'effect' 2 | import { atom, useAtomValue } from 'jotai' 3 | 4 | import { withAbortableMount, withReadonly } from '@sub-vocab/utils/atoms' 5 | import { isServer } from '@sub-vocab/utils/lib' 6 | 7 | const MOBILE_BREAKPOINT = 768 8 | 9 | const isMobileAtom = (() => { 10 | if (isServer) return atom(() => false) 11 | const getSnapshot = () => window.innerWidth < MOBILE_BREAKPOINT 12 | const mediaQueryList = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 13 | return pipe( 14 | atom(getSnapshot()), 15 | (x) => withAbortableMount(x, (setAtom, signal) => { 16 | const listener = () => { 17 | setAtom(getSnapshot()) 18 | } 19 | mediaQueryList.addEventListener('change', listener, { signal }) 20 | listener() 21 | }), 22 | withReadonly, 23 | ) 24 | })() 25 | 26 | export function useIsMobile() { 27 | return useAtomValue(isMobileAtom) 28 | } 29 | -------------------------------------------------------------------------------- /apps/ui/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from '@radix-ui/react-collapsible' 2 | 3 | function Collapsible({ 4 | ...props 5 | }: React.ComponentProps) { 6 | return 7 | } 8 | 9 | function CollapsibleTrigger({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 17 | ) 18 | } 19 | 20 | function CollapsibleContent({ 21 | ...props 22 | }: React.ComponentProps) { 23 | return ( 24 | 28 | ) 29 | } 30 | 31 | export { Collapsible, CollapsibleContent, CollapsibleTrigger } 32 | -------------------------------------------------------------------------------- /apps/ui/sentry.edge.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). 2 | // The config you add here will be used whenever one of the edge features is loaded. 3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. 4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 5 | 6 | import * as Sentry from '@sentry/nextjs' 7 | 8 | Sentry.init({ 9 | dsn: 'https://c85c12d1ecc241558e8aa3bc55dea61f@o4505257329098752.ingest.us.sentry.io/4505257332178944', 10 | 11 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. 12 | tracesSampleRate: 1, 13 | 14 | // Enable logs to be sent to Sentry 15 | enableLogs: true, 16 | 17 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 18 | debug: false, 19 | }) 20 | -------------------------------------------------------------------------------- /apps/ui/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) { 6 | return ( 7 |