├── .prettierignore ├── src ├── vite-env.d.ts ├── lib │ ├── schemas.ts │ ├── constants.ts │ └── utils.ts ├── pages │ ├── quiz-detail │ │ ├── lib │ │ │ └── utils.ts │ │ ├── components │ │ │ └── quiz-skeleton.tsx │ │ └── index.tsx │ ├── quiz-home │ │ ├── lib │ │ │ ├── constants.ts │ │ │ └── atoms.ts │ │ ├── index.tsx │ │ └── components │ │ │ ├── quiz-list.tsx │ │ │ ├── delete-alert-dialog.tsx │ │ │ ├── quiz-list-section.tsx │ │ │ ├── quiz-item.tsx │ │ │ └── quiz-form.tsx │ ├── quiz-results │ │ ├── lib │ │ │ └── schemas.ts │ │ ├── components │ │ │ ├── question-review.tsx │ │ │ └── results-skeleton.tsx │ │ └── index.tsx │ └── login │ │ ├── components │ │ ├── login-form.tsx │ │ └── register-form.tsx │ │ └── index.tsx ├── components │ ├── ui │ │ ├── skeleton.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── textarea.tsx │ │ ├── progress.tsx │ │ ├── sonner.tsx │ │ ├── input.tsx │ │ ├── checkbox.tsx │ │ ├── radio-group.tsx │ │ ├── card.tsx │ │ ├── tabs.tsx │ │ ├── slider.tsx │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ └── alert-dialog.tsx │ ├── quiz-not-found.tsx │ └── input-with-feedback.tsx ├── hooks │ └── use-prefetch-query.tsx ├── App.tsx ├── layouts │ └── authenticated │ │ ├── index.tsx │ │ └── components │ │ ├── sidebar.tsx │ │ └── settings-dialog.tsx ├── main.tsx └── index.css ├── public ├── favicon.ico └── assets │ └── meta.webp ├── vercel.json ├── convex ├── auth.config.ts ├── http.ts ├── constants.ts ├── _generated │ ├── api.js │ ├── api.d.ts │ ├── dataModel.d.ts │ ├── server.js │ └── server.d.ts ├── auth.ts ├── tsconfig.json ├── eslint.config.js ├── utils.ts ├── users.ts ├── schema.ts ├── key.ts ├── README.md └── quizzes │ ├── queries.ts │ ├── mutations.ts │ └── actions.ts ├── tsconfig.json ├── .gitignore ├── vite.config.ts ├── components.json ├── .prettierrc ├── tsconfig.node.json ├── tsconfig.app.json ├── README.md ├── eslint.config.js ├── package.json └── index.html /.prettierignore: -------------------------------------------------------------------------------- 1 | *.css -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/lib/schemas.ts: -------------------------------------------------------------------------------- 1 | export type Status = 'idle' | 'loading' | 'success' | 'error' 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tigerabrodi/hawanji/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] 3 | } 4 | -------------------------------------------------------------------------------- /public/assets/meta.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tigerabrodi/hawanji/HEAD/public/assets/meta.webp -------------------------------------------------------------------------------- /src/pages/quiz-detail/lib/utils.ts: -------------------------------------------------------------------------------- 1 | export const getSkeletonOptionsPerQuestionKey = (quizId: string) => 2 | `skeleton-options-per-question-${quizId}` 3 | -------------------------------------------------------------------------------- /src/pages/quiz-home/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const NOTE_INPUT_KEY = 'quiz-home-note-input' 2 | 3 | export const CONTEXT_INPUT_KEY = 'quiz-home-context-input' 4 | -------------------------------------------------------------------------------- /convex/auth.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | providers: [ 3 | { 4 | domain: process.env.CONVEX_SITE_URL, 5 | applicationID: "convex", 6 | }, 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /convex/http.ts: -------------------------------------------------------------------------------- 1 | import { httpRouter } from "convex/server"; 2 | import { auth } from "./auth"; 3 | 4 | const http = httpRouter(); 5 | 6 | auth.addHttpRoutes(http); 7 | 8 | export default http; 9 | -------------------------------------------------------------------------------- /src/pages/quiz-home/lib/atoms.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai' 2 | 3 | export const isSelectModeAtom = atom(false) 4 | export const selectedQuizIdsAtom = atom>(new Set()) 5 | -------------------------------------------------------------------------------- /convex/constants.ts: -------------------------------------------------------------------------------- 1 | export const ALPHABET_MAP: Record = { 2 | 0: 'A', 3 | 1: 'B', 4 | 2: 'C', 5 | 3: 'D', 6 | 4: 'E', 7 | 5: 'F', 8 | 6: 'G', 9 | 7: 'H', 10 | 8: 'I', 11 | 9: 'J', 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"], 11 | "@convex/*": ["./convex/*"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/quiz-results/lib/schemas.ts: -------------------------------------------------------------------------------- 1 | import { api } from '@convex/_generated/api' 2 | import { FunctionReturnType } from 'convex/server' 3 | 4 | export type QuizResults = NonNullable< 5 | FunctionReturnType 6 | > 7 | 8 | export type QuestionFromQuizResult = QuizResults['questions'][number] 9 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const ROUTES = { 2 | login: '/', 3 | quizHome: '/quizzes', 4 | quizDetail: '/quizzes/:quizId', 5 | quizDetailResults: '/quizzes/:quizId/results', 6 | } as const 7 | 8 | export const TAB_VALUES = { 9 | LOGIN: 'login', 10 | REGISTER: 'register', 11 | } as const 12 | 13 | export const ERROR_TOAST_DURATION = 7000 14 | -------------------------------------------------------------------------------- /src/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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .env -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import tailwindcss from '@tailwindcss/vite' 2 | import react from '@vitejs/plugin-react' 3 | import path from 'path' 4 | import { defineConfig } from 'vite' 5 | 6 | // https://vite.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react(), tailwindcss()], 9 | resolve: { 10 | alias: { 11 | '@': path.resolve(__dirname, './src'), 12 | '@convex': path.resolve(__dirname, './convex'), 13 | }, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /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": "src/index.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 | -------------------------------------------------------------------------------- /convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import { anyApi } from "convex/server"; 12 | 13 | /** 14 | * A utility for referencing Convex functions in your app's API. 15 | * 16 | * Usage: 17 | * ```js 18 | * const myFunctionReference = api.myModule.myFunction; 19 | * ``` 20 | */ 21 | export const api = anyApi; 22 | export const internal = anyApi; 23 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "embeddedLanguageFormatting": "auto", 5 | "htmlWhitespaceSensitivity": "css", 6 | "insertPragma": false, 7 | "jsxBracketSameLine": false, 8 | "jsxSingleQuote": false, 9 | "printWidth": 80, 10 | "proseWrap": "preserve", 11 | "quoteProps": "as-needed", 12 | "requirePragma": false, 13 | "semi": false, 14 | "singleQuote": true, 15 | "tabWidth": 2, 16 | "trailingComma": "es5", 17 | "useTabs": false, 18 | "vueIndentScriptAndStyle": false, 19 | "plugins": ["prettier-plugin-tailwindcss"] 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: Array) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | 8 | export async function handlePromise( 9 | promise: Promise 10 | ): Promise<[PromiseResult, null] | [null, Error]> { 11 | try { 12 | const result = await promise 13 | return [result, null] 14 | } catch (error) { 15 | return [null, error instanceof Error ? error : new Error(String(error))] 16 | } 17 | } 18 | 19 | export async function sleep(ms: number) { 20 | return new Promise((resolve) => setTimeout(resolve, ms)) 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 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 | -------------------------------------------------------------------------------- /convex/auth.ts: -------------------------------------------------------------------------------- 1 | import { Password } from '@convex-dev/auth/providers/Password' 2 | import { convexAuth } from '@convex-dev/auth/server' 3 | import { DataModel } from './_generated/dataModel' 4 | import { MutationCtx } from './_generated/server' 5 | 6 | export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({ 7 | providers: [Password()], 8 | callbacks: { 9 | async createOrUpdateUser(ctx: MutationCtx, args) { 10 | if (args.existingUserId) { 11 | return args.existingUserId 12 | } 13 | 14 | // First create the user 15 | const userId = await ctx.db.insert('users', { 16 | email: args.profile.email!, 17 | updatedAt: Date.now(), 18 | }) 19 | 20 | return userId 21 | }, 22 | }, 23 | }) 24 | -------------------------------------------------------------------------------- /src/components/quiz-not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button' 2 | import { ROUTES } from '@/lib/constants' 3 | import { Link } from 'react-router' 4 | 5 | export function QuizNotFound() { 6 | return ( 7 |
8 |
9 |
10 |

Quiz Not Found

11 |

12 | The quiz you are looking for does not exist. 13 |

14 |
15 | 16 | 19 |
20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function Separator({ 7 | className, 8 | orientation = "horizontal", 9 | decorative = true, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 23 | ) 24 | } 25 | 26 | export { Separator } 27 | -------------------------------------------------------------------------------- /src/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 |