A sign-in link has been sent to your email address.
31 |
38 | >
39 | )}
40 |
41 |
42 | );
43 | }
44 |
45 | export function SignInWithGitHub() {
46 | const { signIn } = useAuthActions();
47 | return (
48 |
56 | );
57 | }
58 |
59 | function SignInWithMagicLink({
60 | handleLinkSent,
61 | }: {
62 | handleLinkSent: () => void;
63 | }) {
64 | const { signIn } = useAuthActions();
65 | const { toast } = useToast();
66 | return (
67 |
88 | );
89 | }
90 |
--------------------------------------------------------------------------------
/convex/messages.ts:
--------------------------------------------------------------------------------
1 | import { v } from "convex/values";
2 | import { mutation, query } from "./_generated/server";
3 | import { VALID_ROLES } from "./lib/permissions";
4 | import { checkPermission } from "./lib/permissions";
5 | import { getAuthUserId } from "@convex-dev/auth/server";
6 |
7 | /**
8 | * Query to list the most recent messages with author information.
9 | * Requires READ permission or higher.
10 | *
11 | * @returns Array of messages with author details (name or email)
12 | * @throws Error if user is not signed in or has insufficient permissions
13 | *
14 | * @example
15 | * // In your React component:
16 | * const messages = useQuery(api.messages.list);
17 | * return messages.map(msg => );
18 | */
19 | export const list = query({
20 | args: {},
21 | handler: async (ctx) => {
22 | // Verify user is authenticated
23 | const userId = await getAuthUserId(ctx);
24 | if (!userId) throw new Error("Not signed in");
25 |
26 | // Check if user has read permissions
27 | const hasAccess = await checkPermission(ctx, userId, VALID_ROLES.READ);
28 | if (!hasAccess) throw new Error("Insufficient permissions");
29 |
30 | // Fetch the 100 most recent messages
31 | const messages = await ctx.db.query("messages").order("desc").take(100);
32 |
33 | // Enrich messages with author information
34 | return Promise.all(
35 | messages.map(async (message) => {
36 | const { name, email } = (await ctx.db.get(message.userId))!;
37 | return { ...message, author: name ?? email! };
38 | }),
39 | );
40 | },
41 | });
42 |
43 | /**
44 | * Mutation to send a new message.
45 | * Requires WRITE permission or higher.
46 | *
47 | * @param body - The text content of the message
48 | * @throws Error if user is not signed in or has insufficient permissions
49 | *
50 | * @example
51 | * // In your React component:
52 | * const sendMessage = useMutation(api.messages.send);
53 | * await sendMessage({ body: "Hello, world!" });
54 | */
55 | export const send = mutation({
56 | args: { body: v.string() },
57 | handler: async (ctx, { body }) => {
58 | // Verify user is authenticated
59 | const userId = await getAuthUserId(ctx);
60 | if (!userId) throw new Error("Not signed in");
61 |
62 | // Check if user has write permissions
63 | const hasAccess = await checkPermission(ctx, userId, VALID_ROLES.WRITE);
64 | if (!hasAccess) throw new Error("Insufficient permissions");
65 |
66 | // Create the new message
67 | await ctx.db.insert("messages", { body, userId });
68 | },
69 | });
70 |
71 | /**
72 | * Mutation to delete a message.
73 | * Requires ADMIN permission.
74 | *
75 | * @param messageId - The ID of the message to delete
76 | * @throws Error if user is not signed in or has insufficient permissions
77 | *
78 | * @example
79 | * // In your React component:
80 | * const deleteMsg = useMutation(api.messages.deleteMessage);
81 | * await deleteMsg({ messageId: message._id });
82 | */
83 | export const deleteMessage = mutation({
84 | args: { messageId: v.id("messages") },
85 | handler: async (ctx, { messageId }) => {
86 | // Verify user is authenticated
87 | const userId = await getAuthUserId(ctx);
88 | if (!userId) throw new Error("Not signed in");
89 |
90 | // Check if user has admin permissions
91 | const hasAccess = await checkPermission(ctx, userId, VALID_ROLES.ADMIN);
92 | if (!hasAccess) throw new Error("Insufficient permissions");
93 |
94 | // Delete the message
95 | await ctx.db.delete(messageId);
96 | },
97 | });
98 |
--------------------------------------------------------------------------------
/convex/_generated/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * Generated utilities for implementing server-side Convex query and mutation functions.
4 | *
5 | * THIS CODE IS AUTOMATICALLY GENERATED.
6 | *
7 | * To regenerate, run `npx convex dev`.
8 | * @module
9 | */
10 |
11 | import {
12 | actionGeneric,
13 | httpActionGeneric,
14 | queryGeneric,
15 | mutationGeneric,
16 | internalActionGeneric,
17 | internalMutationGeneric,
18 | internalQueryGeneric,
19 | } from "convex/server";
20 |
21 | /**
22 | * Define a query in this Convex app's public API.
23 | *
24 | * This function will be allowed to read your Convex database and will be accessible from the client.
25 | *
26 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
27 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
28 | */
29 | export const query = queryGeneric;
30 |
31 | /**
32 | * Define a query that is only accessible from other Convex functions (but not from the client).
33 | *
34 | * This function will be allowed to read from your Convex database. It will not be accessible from the client.
35 | *
36 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
37 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
38 | */
39 | export const internalQuery = internalQueryGeneric;
40 |
41 | /**
42 | * Define a mutation in this Convex app's public API.
43 | *
44 | * This function will be allowed to modify your Convex database and will be accessible from the client.
45 | *
46 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
47 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
48 | */
49 | export const mutation = mutationGeneric;
50 |
51 | /**
52 | * Define a mutation that is only accessible from other Convex functions (but not from the client).
53 | *
54 | * This function will be allowed to modify your Convex database. It will not be accessible from the client.
55 | *
56 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
57 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
58 | */
59 | export const internalMutation = internalMutationGeneric;
60 |
61 | /**
62 | * Define an action in this Convex app's public API.
63 | *
64 | * An action is a function which can execute any JavaScript code, including non-deterministic
65 | * code and code with side-effects, like calling third-party services.
66 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
67 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
68 | *
69 | * @param func - The action. It receives an {@link ActionCtx} as its first argument.
70 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible.
71 | */
72 | export const action = actionGeneric;
73 |
74 | /**
75 | * Define an action that is only accessible from other Convex functions (but not from the client).
76 | *
77 | * @param func - The function. It receives an {@link ActionCtx} as its first argument.
78 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible.
79 | */
80 | export const internalAction = internalActionGeneric;
81 |
82 | /**
83 | * Define a Convex HTTP action.
84 | *
85 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object
86 | * as its second.
87 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
88 | */
89 | export const httpAction = httpActionGeneric;
90 |
--------------------------------------------------------------------------------
/src/Chat/Chat.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Input } from "@/components/ui/input";
5 | import { useMutation, useQuery } from "convex/react";
6 | import { FormEvent, useState } from "react";
7 | import { api } from "../../convex/_generated/api";
8 | import { MessageList } from "@/Chat/MessageList";
9 | import { Message } from "@/Chat/Message";
10 | import { Id } from "../../convex/_generated/dataModel";
11 | import {
12 | Select,
13 | SelectContent,
14 | SelectItem,
15 | SelectTrigger,
16 | SelectValue,
17 | } from "@/components/ui/select";
18 |
19 | export function Chat({ viewer }: { viewer: Id<"users"> }) {
20 | const [newMessageText, setNewMessageText] = useState("");
21 | const messages = useQuery(api.messages.list);
22 | const sendMessage = useMutation(api.messages.send);
23 | const deleteMessage = useMutation(api.messages.deleteMessage);
24 | const me = useQuery(api.auth.getMe);
25 | const updateRole = useMutation(api.auth.updateRole);
26 | const [error, setError] = useState(null);
27 |
28 | const handleSubmit = (event: FormEvent) => {
29 | event.preventDefault();
30 | setError(null);
31 | setNewMessageText("");
32 | sendMessage({ body: newMessageText }).catch((error) => {
33 | console.error("Failed to send message:", error);
34 | setError("You have the right to party, but not to post.");
35 | });
36 | };
37 |
38 | const handleDeleteMessage = (messageId: Id<"messages">) => {
39 | deleteMessage({ messageId }).catch((error) => {
40 | console.error("Failed to delete message:", error);
41 | setError("You have the right to party, but not to delete messages.");
42 | });
43 | };
44 |
45 | const handleRoleChange = (newRole: "read" | "write" | "admin") => {
46 | updateRole({ role: newRole }).catch((error) => {
47 | console.error("Failed to update role:", error);
48 | setError("Failed to update role");
49 | });
50 | };
51 |
52 | /**
53 | * Just used for the UI to show the delete button to admins.
54 | * The server-side check is done with checkPermission.
55 | */
56 | const isAdmin = me?.role === "admin";
57 |
58 | return (
59 | <>
60 |
61 | {messages?.map((message) => (
62 |
71 | {message.body}
72 |
73 | ))}
74 |
75 |
76 |
86 |
{error || " "}
87 |
88 |
89 | Test different roles:
90 |
105 |
106 |
107 | >
108 | );
109 | }
110 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as DialogPrimitive from "@radix-ui/react-dialog";
3 | import { Cross2Icon } from "@radix-ui/react-icons";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const Dialog = DialogPrimitive.Root;
8 |
9 | const DialogTrigger = DialogPrimitive.Trigger;
10 |
11 | const DialogPortal = DialogPrimitive.Portal;
12 |
13 | const DialogClose = DialogPrimitive.Close;
14 |
15 | const DialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ));
28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
29 |
30 | const DialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, children, ...props }, ref) => (
34 |
35 |
36 |
44 | {children}
45 |
46 |
47 | Close
48 |
49 |
50 |
51 | ));
52 | DialogContent.displayName = DialogPrimitive.Content.displayName;
53 |
54 | const DialogHeader = ({
55 | className,
56 | ...props
57 | }: React.HTMLAttributes) => (
58 |
65 | );
66 | DialogHeader.displayName = "DialogHeader";
67 |
68 | const DialogFooter = ({
69 | className,
70 | ...props
71 | }: React.HTMLAttributes) => (
72 |
79 | );
80 | DialogFooter.displayName = "DialogFooter";
81 |
82 | const DialogTitle = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
94 | ));
95 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
96 |
97 | const DialogDescription = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ));
107 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
108 |
109 | export {
110 | Dialog,
111 | DialogPortal,
112 | DialogOverlay,
113 | DialogTrigger,
114 | DialogClose,
115 | DialogContent,
116 | DialogHeader,
117 | DialogFooter,
118 | DialogTitle,
119 | DialogDescription,
120 | };
121 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to your Convex + React (Vite) + Convex Auth app with role-based permissions
2 |
3 | This is a [Convex](https://convex.dev/) project created with [`npm create convex`](https://www.npmjs.com/package/create-convex).
4 |
5 | After the initial setup (<2 minutes) you'll have a working full-stack app using:
6 |
7 | - Convex as your backend (database, server logic)
8 | - [Convex Auth](https://labs.convex.dev/auth) for your authentication implementation
9 | - [React](https://react.dev/) as your frontend (web page interactivity)
10 | - [Vite](https://vitest.dev/) for optimized web hosting
11 | - [Tailwind](https://tailwindcss.com/) and [shadcn/ui](https://ui.shadcn.com/) for building great looking accessible UI fast
12 |
13 | ## Role Based Permissions
14 |
15 | This project implements a hierarchical role-based permission system that controls access to different features of the application. The system is built using Convex and consists of three permission levels:
16 |
17 | - **READ**: Basic access level - can view messages
18 | - **WRITE**: Intermediate access - can view and send messages
19 | - **ADMIN**: Full access - can view, send, and delete messages
20 |
21 | ### Implementation Details
22 |
23 | The permission system is implemented across several files:
24 |
25 | #### 1. Schema Definition (`schema.ts`)
26 |
27 | ```typescript
28 | users: defineTable({
29 | // ... other fields ...
30 | role: v.optional(
31 | v.union(v.literal("read"), v.literal("write"), v.literal("admin")),
32 | ),
33 | });
34 | ```
35 |
36 | #### 2. Permission Management (`lib/permissions.ts`)
37 |
38 | The permissions system uses a numeric hierarchy to determine access levels:
39 |
40 | ```typescript
41 | const roleHierarchy = {
42 | read: 0,
43 | write: 1,
44 | admin: 2,
45 | };
46 | ```
47 |
48 | #### 3. Authentication Integration (`auth.ts`)
49 |
50 | New users are automatically assigned the READ role upon registration:
51 |
52 | ```typescript
53 | async afterUserCreatedOrUpdated(ctx, args) {
54 | if (args.existingUserId) return;
55 | await ctx.db.patch(args.userId, {
56 | role: VALID_ROLES.READ,
57 | });
58 | }
59 | ```
60 |
61 | #### 4. Usage Example (`messages.ts`)
62 |
63 | The permission system controls access to different operations:
64 |
65 | ```typescript
66 | // Reading messages requires READ access
67 | const hasAccess = await checkPermission(ctx, userId, VALID_ROLES.READ);
68 |
69 | // Sending messages requires WRITE access
70 | const hasAccess = await checkPermission(ctx, userId, VALID_ROLES.WRITE);
71 |
72 | // Deleting messages requires ADMIN access
73 | const hasAccess = await checkPermission(ctx, userId, VALID_ROLES.ADMIN);
74 | ```
75 |
76 | ### Security Considerations
77 |
78 | - Role checks are performed server-side in Convex functions
79 | - Role updates should be restricted to administrators in production
80 | - The permission system is integrated with the authentication system
81 | - Invalid or missing roles default to no access
82 |
83 | ## Get started
84 |
85 | If you just cloned this codebase and didn't use `npm create convex`, run:
86 |
87 | ```
88 | npm install
89 | npm run dev
90 | ```
91 |
92 | If you're reading this README on GitHub and want to use this template, run:
93 |
94 | ```
95 | npm create convex@latest -- -t react-vite-convexauth-shadcn
96 | ```
97 |
98 | ## The app
99 |
100 | The app is a basic multi-user chat. Walkthrough of the source code:
101 |
102 | - [convex/auth.ts](./convex/auth.ts) configures the available authentication methods
103 | - [convex/messages.ts](./convex/messages.ts) is the chat backend implementation
104 | - [src/main.tsx](./src/main.tsx) is the frontend entry-point
105 | - [src/App.tsx](./src/App.tsx) determines which UI to show based on the authentication state
106 | - [src/SignInForm.tsx](./src/SignInForm.tsx) implements the sign-in UI
107 | - [src/Chat/Chat.tsx](./src/Chat/Chat.tsx) is the chat frontend
108 |
109 | ## Configuring other authentication methods
110 |
111 | To configure different authentication methods, see [Configuration](https://labs.convex.dev/auth/config) in the Convex Auth docs.
112 |
113 | ## Learn more
114 |
115 | To learn more about developing your project with Convex, check out:
116 |
117 | - The [Tour of Convex](https://docs.convex.dev/get-started) for a thorough introduction to Convex principles.
118 | - The rest of [Convex docs](https://docs.convex.dev/) to learn about all Convex features.
119 | - [Stack](https://stack.convex.dev/) for in-depth articles on advanced topics.
120 |
121 | ## Join the community
122 |
123 | Join thousands of developers building full-stack apps with Convex:
124 |
125 | - Join the [Convex Discord community](https://convex.dev/community) to get help in real-time.
126 | - Follow [Convex on GitHub](https://github.com/get-convex/), star and contribute to the open-source implementation of Convex.
127 |
--------------------------------------------------------------------------------
/src/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from "react"
3 |
4 | import type {
5 | ToastActionElement,
6 | ToastProps,
7 | } from "@/components/ui/toast"
8 |
9 | const TOAST_LIMIT = 1
10 | const TOAST_REMOVE_DELAY = 1000000
11 |
12 | type ToasterToast = ToastProps & {
13 | id: string
14 | title?: React.ReactNode
15 | description?: React.ReactNode
16 | action?: ToastActionElement
17 | }
18 |
19 | const actionTypes = {
20 | ADD_TOAST: "ADD_TOAST",
21 | UPDATE_TOAST: "UPDATE_TOAST",
22 | DISMISS_TOAST: "DISMISS_TOAST",
23 | REMOVE_TOAST: "REMOVE_TOAST",
24 | } as const
25 |
26 | let count = 0
27 |
28 | function genId() {
29 | count = (count + 1) % Number.MAX_SAFE_INTEGER
30 | return count.toString()
31 | }
32 |
33 | type ActionType = typeof actionTypes
34 |
35 | type Action =
36 | | {
37 | type: ActionType["ADD_TOAST"]
38 | toast: ToasterToast
39 | }
40 | | {
41 | type: ActionType["UPDATE_TOAST"]
42 | toast: Partial
43 | }
44 | | {
45 | type: ActionType["DISMISS_TOAST"]
46 | toastId?: ToasterToast["id"]
47 | }
48 | | {
49 | type: ActionType["REMOVE_TOAST"]
50 | toastId?: ToasterToast["id"]
51 | }
52 |
53 | interface State {
54 | toasts: ToasterToast[]
55 | }
56 |
57 | const toastTimeouts = new Map>()
58 |
59 | const addToRemoveQueue = (toastId: string) => {
60 | if (toastTimeouts.has(toastId)) {
61 | return
62 | }
63 |
64 | const timeout = setTimeout(() => {
65 | toastTimeouts.delete(toastId)
66 | dispatch({
67 | type: "REMOVE_TOAST",
68 | toastId: toastId,
69 | })
70 | }, TOAST_REMOVE_DELAY)
71 |
72 | toastTimeouts.set(toastId, timeout)
73 | }
74 |
75 | export const reducer = (state: State, action: Action): State => {
76 | switch (action.type) {
77 | case "ADD_TOAST":
78 | return {
79 | ...state,
80 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
81 | }
82 |
83 | case "UPDATE_TOAST":
84 | return {
85 | ...state,
86 | toasts: state.toasts.map((t) =>
87 | t.id === action.toast.id ? { ...t, ...action.toast } : t
88 | ),
89 | }
90 |
91 | case "DISMISS_TOAST": {
92 | const { toastId } = action
93 |
94 | // ! Side effects ! - This could be extracted into a dismissToast() action,
95 | // but I'll keep it here for simplicity
96 | if (toastId) {
97 | addToRemoveQueue(toastId)
98 | } else {
99 | state.toasts.forEach((toast) => {
100 | addToRemoveQueue(toast.id)
101 | })
102 | }
103 |
104 | return {
105 | ...state,
106 | toasts: state.toasts.map((t) =>
107 | t.id === toastId || toastId === undefined
108 | ? {
109 | ...t,
110 | open: false,
111 | }
112 | : t
113 | ),
114 | }
115 | }
116 | case "REMOVE_TOAST":
117 | if (action.toastId === undefined) {
118 | return {
119 | ...state,
120 | toasts: [],
121 | }
122 | }
123 | return {
124 | ...state,
125 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
126 | }
127 | }
128 | }
129 |
130 | const listeners: Array<(state: State) => void> = []
131 |
132 | let memoryState: State = { toasts: [] }
133 |
134 | function dispatch(action: Action) {
135 | memoryState = reducer(memoryState, action)
136 | listeners.forEach((listener) => {
137 | listener(memoryState)
138 | })
139 | }
140 |
141 | type Toast = Omit
142 |
143 | function toast({ ...props }: Toast) {
144 | const id = genId()
145 |
146 | const update = (props: ToasterToast) =>
147 | dispatch({
148 | type: "UPDATE_TOAST",
149 | toast: { ...props, id },
150 | })
151 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
152 |
153 | dispatch({
154 | type: "ADD_TOAST",
155 | toast: {
156 | ...props,
157 | id,
158 | open: true,
159 | onOpenChange: (open) => {
160 | if (!open) dismiss()
161 | },
162 | },
163 | })
164 |
165 | return {
166 | id: id,
167 | dismiss,
168 | update,
169 | }
170 | }
171 |
172 | function useToast() {
173 | const [state, setState] = React.useState(memoryState)
174 |
175 | React.useEffect(() => {
176 | listeners.push(setState)
177 | return () => {
178 | const index = listeners.indexOf(setState)
179 | if (index > -1) {
180 | listeners.splice(index, 1)
181 | }
182 | }
183 | }, [state])
184 |
185 | return {
186 | ...state,
187 | toast,
188 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
189 | }
190 | }
191 |
192 | export { useToast, toast }
193 |
--------------------------------------------------------------------------------
/src/GetStarted/GetStartedDialog.tsx:
--------------------------------------------------------------------------------
1 | import { ConvexLogo } from "@/GetStarted/ConvexLogo";
2 | import { Code } from "@/components/Code";
3 | import { Button } from "@/components/ui/button";
4 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5 | import {
6 | Dialog,
7 | DialogClose,
8 | DialogContent,
9 | DialogFooter,
10 | DialogHeader,
11 | DialogTitle,
12 | DialogTrigger,
13 | } from "@/components/ui/dialog";
14 | import {
15 | CodeIcon,
16 | ExternalLinkIcon,
17 | MagicWandIcon,
18 | PlayIcon,
19 | StackIcon,
20 | } from "@radix-ui/react-icons";
21 | import { ReactNode } from "react";
22 |
23 | export function GetStartedDialog({ children }: { children: ReactNode }) {
24 | return (
25 |
41 | );
42 | }
43 |
44 | function GetStartedContent() {
45 | return (
46 |
47 |
48 | This template is a starting point for building your fullstack web
49 | application.
50 |
51 |
52 |
53 |
54 |
55 | Play with the app
56 |
57 |
58 | Close this dialog to see the app in action.
59 |
60 |
61 |
62 |
63 | Inspect your database
64 |
65 |
66 |
67 | The{" "}
68 |
73 | Convex dashboard
74 | {" "}
75 | is already open in another window.
76 |
77 |
78 |
79 |
80 |
81 |
82 | Change the backend
83 |
84 |
85 |
86 | Edit convex/messages.ts to change the backend
87 | functionality.
88 |
89 |
90 |
91 |
92 |
93 |
94 | Change the frontend
95 |
96 |
97 |
98 | Edit src/App.tsx to change your frontend.
99 |
100 |
101 |
102 |
103 |
Helpful resources
104 |
105 |
106 | Read comprehensive documentation for all Convex features.
107 |
108 |
109 | Learn about best practices, use cases, and more from a growing
110 | collection of articles, videos, and walkthroughs.
111 |
112 |
113 | Join our developer community to ask questions, trade tips & tricks,
114 | and show off your projects.
115 |
116 |
117 | Get unblocked quickly by searching across the docs, Stack, and
118 | Discord chats.
119 |
120 |
121 |
122 |
123 | );
124 | }
125 |
126 | function Resource({
127 | title,
128 | children,
129 | href,
130 | }: {
131 | title: string;
132 | children: ReactNode;
133 | href: string;
134 | }) {
135 | return (
136 |
149 | );
150 | }
151 |
--------------------------------------------------------------------------------
/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Cross2Icon } from "@radix-ui/react-icons"
3 | import * as ToastPrimitives from "@radix-ui/react-toast"
4 | import { cva, type VariantProps } from "class-variance-authority"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ToastProvider = ToastPrimitives.Provider
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
24 |
25 | const toastVariants = cva(
26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
27 | {
28 | variants: {
29 | variant: {
30 | default: "border bg-background text-foreground",
31 | destructive:
32 | "destructive group border-destructive bg-destructive text-destructive-foreground",
33 | },
34 | },
35 | defaultVariants: {
36 | variant: "default",
37 | },
38 | }
39 | )
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | )
53 | })
54 | Toast.displayName = ToastPrimitives.Root.displayName
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ))
69 | ToastAction.displayName = ToastPrimitives.Action.displayName
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ))
87 | ToastClose.displayName = ToastPrimitives.Close.displayName
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ))
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef
114 |
115 | type ToastActionElement = React.ReactElement
116 |
117 | export {
118 | type ToastProps,
119 | type ToastActionElement,
120 | ToastProvider,
121 | ToastViewport,
122 | Toast,
123 | ToastTitle,
124 | ToastDescription,
125 | ToastClose,
126 | ToastAction,
127 | }
128 |
--------------------------------------------------------------------------------
/convex/_generated/server.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * Generated utilities for implementing server-side Convex query and mutation functions.
4 | *
5 | * THIS CODE IS AUTOMATICALLY GENERATED.
6 | *
7 | * To regenerate, run `npx convex dev`.
8 | * @module
9 | */
10 |
11 | import {
12 | ActionBuilder,
13 | HttpActionBuilder,
14 | MutationBuilder,
15 | QueryBuilder,
16 | GenericActionCtx,
17 | GenericMutationCtx,
18 | GenericQueryCtx,
19 | GenericDatabaseReader,
20 | GenericDatabaseWriter,
21 | } from "convex/server";
22 | import type { DataModel } from "./dataModel.js";
23 |
24 | /**
25 | * Define a query in this Convex app's public API.
26 | *
27 | * This function will be allowed to read your Convex database and will be accessible from the client.
28 | *
29 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
30 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
31 | */
32 | export declare const query: QueryBuilder;
33 |
34 | /**
35 | * Define a query that is only accessible from other Convex functions (but not from the client).
36 | *
37 | * This function will be allowed to read from your Convex database. It will not be accessible from the client.
38 | *
39 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
40 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
41 | */
42 | export declare const internalQuery: QueryBuilder;
43 |
44 | /**
45 | * Define a mutation in this Convex app's public API.
46 | *
47 | * This function will be allowed to modify your Convex database and will be accessible from the client.
48 | *
49 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
50 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
51 | */
52 | export declare const mutation: MutationBuilder;
53 |
54 | /**
55 | * Define a mutation that is only accessible from other Convex functions (but not from the client).
56 | *
57 | * This function will be allowed to modify your Convex database. It will not be accessible from the client.
58 | *
59 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
60 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
61 | */
62 | export declare const internalMutation: MutationBuilder;
63 |
64 | /**
65 | * Define an action in this Convex app's public API.
66 | *
67 | * An action is a function which can execute any JavaScript code, including non-deterministic
68 | * code and code with side-effects, like calling third-party services.
69 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
70 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
71 | *
72 | * @param func - The action. It receives an {@link ActionCtx} as its first argument.
73 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible.
74 | */
75 | export declare const action: ActionBuilder;
76 |
77 | /**
78 | * Define an action that is only accessible from other Convex functions (but not from the client).
79 | *
80 | * @param func - The function. It receives an {@link ActionCtx} as its first argument.
81 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible.
82 | */
83 | export declare const internalAction: ActionBuilder;
84 |
85 | /**
86 | * Define an HTTP action.
87 | *
88 | * This function will be used to respond to HTTP requests received by a Convex
89 | * deployment if the requests matches the path and method where this action
90 | * is routed. Be sure to route your action in `convex/http.js`.
91 | *
92 | * @param func - The function. It receives an {@link ActionCtx} as its first argument.
93 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
94 | */
95 | export declare const httpAction: HttpActionBuilder;
96 |
97 | /**
98 | * A set of services for use within Convex query functions.
99 | *
100 | * The query context is passed as the first argument to any Convex query
101 | * function run on the server.
102 | *
103 | * This differs from the {@link MutationCtx} because all of the services are
104 | * read-only.
105 | */
106 | export type QueryCtx = GenericQueryCtx;
107 |
108 | /**
109 | * A set of services for use within Convex mutation functions.
110 | *
111 | * The mutation context is passed as the first argument to any Convex mutation
112 | * function run on the server.
113 | */
114 | export type MutationCtx = GenericMutationCtx;
115 |
116 | /**
117 | * A set of services for use within Convex action functions.
118 | *
119 | * The action context is passed as the first argument to any Convex action
120 | * function run on the server.
121 | */
122 | export type ActionCtx = GenericActionCtx;
123 |
124 | /**
125 | * An interface to read from the database within Convex query functions.
126 | *
127 | * The two entry points are {@link DatabaseReader.get}, which fetches a single
128 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts
129 | * building a query.
130 | */
131 | export type DatabaseReader = GenericDatabaseReader;
132 |
133 | /**
134 | * An interface to read from and write to the database within Convex mutation
135 | * functions.
136 | *
137 | * Convex guarantees that all writes within a single mutation are
138 | * executed atomically, so you never have to worry about partial writes leaving
139 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
140 | * for the guarantees Convex provides your functions.
141 | */
142 | export type DatabaseWriter = GenericDatabaseWriter;
143 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SelectPrimitive from "@radix-ui/react-select"
3 | import { cn } from "@/lib/utils"
4 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "@radix-ui/react-icons"
5 |
6 | const Select = SelectPrimitive.Root
7 |
8 | const SelectGroup = SelectPrimitive.Group
9 |
10 | const SelectValue = SelectPrimitive.Value
11 |
12 | const SelectTrigger = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, children, ...props }, ref) => (
16 | span]:line-clamp-1",
20 | className
21 | )}
22 | {...props}
23 | >
24 | {children}
25 |
26 |
27 |
28 |
29 | ))
30 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
31 |
32 | const SelectScrollUpButton = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, ...props }, ref) => (
36 |
44 |
45 |
46 | ))
47 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
48 |
49 | const SelectScrollDownButton = React.forwardRef<
50 | React.ElementRef,
51 | React.ComponentPropsWithoutRef
52 | >(({ className, ...props }, ref) => (
53 |
61 |
62 |
63 | ))
64 | SelectScrollDownButton.displayName =
65 | SelectPrimitive.ScrollDownButton.displayName
66 |
67 | const SelectContent = React.forwardRef<
68 | React.ElementRef,
69 | React.ComponentPropsWithoutRef
70 | >(({ className, children, position = "popper", ...props }, ref) => (
71 |
72 |
83 |
84 |
91 | {children}
92 |
93 |
94 |
95 |
96 | ))
97 | SelectContent.displayName = SelectPrimitive.Content.displayName
98 |
99 | const SelectLabel = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | SelectLabel.displayName = SelectPrimitive.Label.displayName
110 |
111 | const SelectItem = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, children, ...props }, ref) => (
115 |
123 |
124 |
125 |
126 |
127 |
128 | {children}
129 |
130 | ))
131 | SelectItem.displayName = SelectPrimitive.Item.displayName
132 |
133 | const SelectSeparator = React.forwardRef<
134 | React.ElementRef,
135 | React.ComponentPropsWithoutRef
136 | >(({ className, ...props }, ref) => (
137 |
142 | ))
143 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
144 |
145 | export {
146 | Select,
147 | SelectGroup,
148 | SelectValue,
149 | SelectTrigger,
150 | SelectContent,
151 | SelectLabel,
152 | SelectItem,
153 | SelectSeparator,
154 | SelectScrollUpButton,
155 | SelectScrollDownButton,
156 | }
157 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
3 | import {
4 | CheckIcon,
5 | ChevronRightIcon,
6 | DotFilledIcon,
7 | } from "@radix-ui/react-icons";
8 |
9 | import { cn } from "@/lib/utils";
10 |
11 | const DropdownMenu = DropdownMenuPrimitive.Root;
12 |
13 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
14 |
15 | const DropdownMenuGroup = DropdownMenuPrimitive.Group;
16 |
17 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
18 |
19 | const DropdownMenuSub = DropdownMenuPrimitive.Sub;
20 |
21 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
22 |
23 | const DropdownMenuSubTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef & {
26 | inset?: boolean;
27 | }
28 | >(({ className, inset, children, ...props }, ref) => (
29 |
38 | {children}
39 |
40 |
41 | ));
42 | DropdownMenuSubTrigger.displayName =
43 | DropdownMenuPrimitive.SubTrigger.displayName;
44 |
45 | const DropdownMenuSubContent = React.forwardRef<
46 | React.ElementRef,
47 | React.ComponentPropsWithoutRef
48 | >(({ className, ...props }, ref) => (
49 |
57 | ));
58 | DropdownMenuSubContent.displayName =
59 | DropdownMenuPrimitive.SubContent.displayName;
60 |
61 | const DropdownMenuContent = React.forwardRef<
62 | React.ElementRef,
63 | React.ComponentPropsWithoutRef
64 | >(({ className, sideOffset = 4, ...props }, ref) => (
65 |
66 |
76 |
77 | ));
78 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
79 |
80 | const DropdownMenuItem = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef & {
83 | inset?: boolean;
84 | }
85 | >(({ className, inset, ...props }, ref) => (
86 |
95 | ));
96 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
97 |
98 | const DropdownMenuCheckboxItem = React.forwardRef<
99 | React.ElementRef,
100 | React.ComponentPropsWithoutRef
101 | >(({ className, children, checked, ...props }, ref) => (
102 |
111 |
112 |
113 |
114 |
115 |
116 | {children}
117 |
118 | ));
119 | DropdownMenuCheckboxItem.displayName =
120 | DropdownMenuPrimitive.CheckboxItem.displayName;
121 |
122 | const DropdownMenuRadioItem = React.forwardRef<
123 | React.ElementRef,
124 | React.ComponentPropsWithoutRef
125 | >(({ className, children, ...props }, ref) => (
126 |
134 |
135 |
136 |
137 |
138 |
139 | {children}
140 |
141 | ));
142 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
143 |
144 | const DropdownMenuLabel = React.forwardRef<
145 | React.ElementRef,
146 | React.ComponentPropsWithoutRef & {
147 | inset?: boolean;
148 | }
149 | >(({ className, inset, ...props }, ref) => (
150 |
159 | ));
160 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
161 |
162 | const DropdownMenuSeparator = React.forwardRef<
163 | React.ElementRef,
164 | React.ComponentPropsWithoutRef
165 | >(({ className, ...props }, ref) => (
166 |
171 | ));
172 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
173 |
174 | const DropdownMenuShortcut = ({
175 | className,
176 | ...props
177 | }: React.HTMLAttributes) => {
178 | return (
179 |
183 | );
184 | };
185 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
186 |
187 | export {
188 | DropdownMenu,
189 | DropdownMenuTrigger,
190 | DropdownMenuContent,
191 | DropdownMenuItem,
192 | DropdownMenuCheckboxItem,
193 | DropdownMenuRadioItem,
194 | DropdownMenuLabel,
195 | DropdownMenuSeparator,
196 | DropdownMenuShortcut,
197 | DropdownMenuGroup,
198 | DropdownMenuPortal,
199 | DropdownMenuSub,
200 | DropdownMenuSubContent,
201 | DropdownMenuSubTrigger,
202 | DropdownMenuRadioGroup,
203 | };
204 |
--------------------------------------------------------------------------------