Skip to content

Commit

Permalink
Basic image uploads with UploadStuff
Browse files Browse the repository at this point in the history
  • Loading branch information
vakila committed Aug 28, 2024
1 parent c250048 commit a382d38
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 69 deletions.
9 changes: 2 additions & 7 deletions convex/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { v } from "convex/values";
import { mutation } from "./_generated/server";
import { viewer as getViewer } from "./users";
import { images } from "./schema";

export const generateUploadUrl = mutation({
args: {},
Expand All @@ -16,13 +17,7 @@ export const generateUploadUrl = mutation({
});

export const save = mutation({
args: {
storageId: v.id('_storage'),
authorId: v.id('users'),
name: v.string(),
type: v.string(),
size: v.number()
},
args: { ...images.withoutSystemFields, url: v.optional(v.string()) },
handler: async (ctx, args) => {
// Verify the user is still authenticated
const viewer = await getViewer(ctx, {});
Expand Down
4 changes: 3 additions & 1 deletion convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ export const users = Table('users', zodToConvexFields(usersZod));
export const imagesZod = {
name: zodOptionalString(),
storageId: zid('_storage'),
url: z.string().url()
url: z.string().url(),
type: zodOptionalString(),
size: z.optional(z.number()),
}
export const images = Table('images', zodToConvexFields(imagesZod))

Expand Down
32 changes: 15 additions & 17 deletions src/components/Blog/Edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,23 +188,21 @@ export function EditablePost({ version }: { version: Doc<'versions'> | null }) {
</div>
</Toolbar >

{
previewing
? (<div className="my-8" >
<DisplayPost post={{ ...version, ...form.getValues() } as PostOrVersion} />
</div>)
: <Form {...form}>
<form>
<div className="container">
<TextField name="title" form={form} />
<TextField name="slug" form={form} />
<TextField name="imageUrl" form={form} hidden />
<ImageField name="image" form={form} userId={form.getValues('editorId')} />
<MarkdownField name="summary" rows={3} form={form} />
<MarkdownField name="content" rows={10} form={form} />
</div>
</form>
</Form>
{previewing
? (<div className="my-8" >
<DisplayPost post={{ ...version, ...form.getValues() } as PostOrVersion} />
</div>)
: <Form {...form}>
<form>
<div className="container">
<TextField name="title" form={form} />
<TextField name="slug" form={form} />
<ImageField name="image" form={form} />
<MarkdownField name="summary" rows={3} form={form} />
<MarkdownField name="content" rows={10} form={form} />
</div>
</form>
</Form>
}

</>)
Expand Down
93 changes: 49 additions & 44 deletions src/components/Inputs.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Input } from "./ui/input";
import type { FieldPath, FieldValues, UseFormReturn } from "react-hook-form";
import type { FieldPath, FieldValue, FieldValues, UseFormReturn } from "react-hook-form";
import { FormField, FormItem, FormLabel, FormControl, FormMessage, FormDescription } from "./ui/form";
import { Textarea } from "./ui/textarea";
import { useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
import { UploadDropzone, type UploadFileResponse } from "@xixixao/uploadstuff/react";
import { UploadButton, type UploadFileResponse } from "@xixixao/uploadstuff/react";
import "@xixixao/uploadstuff/react/styles.css";
import type { Id } from "../../convex/_generated/dataModel";
import { useToast } from "./ui/use-toast";

interface CommonProps<Schema extends FieldValues> {
name: FieldPath<Schema>;
Expand All @@ -16,24 +17,28 @@ interface CommonProps<Schema extends FieldValues> {
interface TextFieldProps<Schema extends FieldValues> extends CommonProps<Schema> {
hidden?: boolean;
required?: boolean;
itemClass?: string;
controlClass?: string;
}

export function TextField<Schema extends FieldValues>({ name, form, hidden, required }: TextFieldProps<Schema>) {
export function TextField<Schema extends FieldValues>({
name, form, hidden, required, itemClass, controlClass
}: TextFieldProps<Schema>) {
const ifRequired = (required
? { required: true, "aria-required": true }
: {})
return (<FormField
control={form.control}
name={name}
render={({ field }) => (
<FormItem className="grid grid-cols-4 gap-4 mb-4 items-center">
<FormItem className={itemClass || "grid grid-cols-4 gap-4 mb-4 items-center"}>
<div className="col-span-1 row-span-2 text-right">
<FormLabel className="text-primary">
{name}{required && '*'}
</FormLabel>
<FormMessage className="w-full text-convex-yellow italic" />
</div>
<FormControl className="col-span-3 row-span-2">
<FormControl className={controlClass || "col-span-3 row-span-2"}>
<Input
type={hidden ? 'hidden' : 'text'}
{...field}
Expand Down Expand Up @@ -82,56 +87,56 @@ export function MarkdownField<Schema extends FieldValues>(

}

interface ImageFieldProps<Schema extends FieldValues> extends CommonProps<Schema> {
userId: Id<'users'>
}
export function ImageField<Schema extends FieldValues>({ form }: CommonProps<Schema>) {
const { toast } = useToast();

export function ImageField<Schema extends FieldValues>({ name, form, userId }: ImageFieldProps<Schema>) {
const generateUploadUrl = useMutation(api.images.generateUploadUrl);
const save = useMutation(api.images.save);

const saveAfterUpload = async (uploaded: UploadFileResponse[]) => {
const { name, type, size, response } = uploaded[0];
const { storageId } = (response as { storageId: Id<'_storage'> });
await save({
const image = await save({
name,
type,
size,
storageId,
authorId: userId
});
};

if (!image) {
toast({
title: 'Error saving file',
description: `images:save returned null for storageId ${storageId}`
})
} else {
toast({
title: `Saved image file ${name}`,
description: `URL: ${image.url}`
});
form.setValue('imageUrl' as FieldPath<Schema>,
image.url as FieldValue<Schema>,
{ shouldDirty: true });
}

return (
<FormField
control={form.control}
name={name}
render={() => (
<FormItem className="grid grid-cols-4 gap-4 mb-4 items-center">
<div className="col-span-1 row-span-2 text-right">
<FormLabel className="text-primary">
{name}
</FormLabel>
<FormMessage className="w-full text-convex-yellow italic" />
</div>
<FormControl className="col-span-3 row-span-2">
<UploadDropzone
uploadUrl={generateUploadUrl}
fileTypes={{
"application/pdf": [".pdf"],
"image/*": [".png", ".gif", ".jpeg", ".jpg"],
}}
onUploadComplete={saveAfterUpload}
onUploadError={(error: unknown) => {
// Do something with the error.
alert(`ERROR! ${error}`);
}}
/>
</FormControl>
};

</FormItem>
)}
/>
return (<div className="grid grid-cols-4 gap-x-4 items-center">
<TextField name={"imageUrl" as FieldPath<Schema>} form={form}
itemClass="space-y-2 grid grid-cols-subgrid col-span-3 gap-4 mb-4 items-center" controlClass="col-span-2 row-span-2" />
<div className="col-start-4 col-span-1 pb-2">
<UploadButton
uploadUrl={generateUploadUrl}
fileTypes={[".png", ".gif", ".jpeg", ".jpg"]}
onUploadComplete={saveAfterUpload}
onUploadError={(error: unknown) => {
toast({
title: 'Error uploading image',
description: `Error: ${error}`
})
}}
content={(progress) => progress ? 'Uploading...' : 'Upload image'}
/>
</div>
</div>

);
}
)
};

0 comments on commit a382d38

Please sign in to comment.