Skip to content

Commit

Permalink
chore: add server uploads to pg (#1055)
Browse files Browse the repository at this point in the history
  • Loading branch information
juliusmarminge authored Nov 18, 2024
1 parent a7eab3e commit 8b41a25
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 36 deletions.
35 changes: 35 additions & 0 deletions playground-v6/components/fieldset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as React from "react";
import cx from "clsx";

export function Input({
className,
type,
...props
}: React.ComponentProps<"input">) {
return (
<input
type={type}
className={cx(
className,
"border-input focus-visible:border-ring focus-visible:ring-ring/30 flex w-full rounded-lg border px-3 py-2 text-sm shadow-sm shadow-black/5 transition-shadow focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
type === "search" &&
"[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none",
type === "file" &&
"file:border-input p-0 pr-3 italic file:me-3 file:h-full file:border-0 file:border-r file:border-solid file:bg-transparent file:px-3 file:text-sm file:font-medium file:not-italic",
)}
{...props}
/>
);
}

export function Label({ className, ...props }: React.ComponentProps<"label">) {
return (
<label
className={cx(
"text-sm font-medium leading-4 peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className,
)}
{...props}
/>
);
}
88 changes: 70 additions & 18 deletions playground-v6/components/uploader.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,87 @@
"use client";

import { useActionState, useEffect, useRef } from "react";
import { useRouter } from "next/navigation";

import { generateReactHelpers, generateUploadButton } from "@uploadthing/react";

import { UploadRouter } from "../app/api/uploadthing/route";
import { uploadFiles } from "../lib/actions";
import { Input, Label } from "./fieldset";

export const UTButton = generateUploadButton<UploadRouter>();
export const { useUploadThing } = generateReactHelpers<UploadRouter>();

function ServerUploader(props: { type: "file" | "url" }) {
const formRef = useRef<HTMLFormElement>(null);
const [state, dispatch, isUploading] = useActionState(uploadFiles, {
success: false,
error: "",
});

useEffect(() => {
if (state.success === false && state.error) {
window.alert(state.error);
}
}, [state.success]);

return (
<form ref={formRef} action={dispatch}>
<div className="space-y-1">
<Label>Upload (server {props.type})</Label>
<Input
name="files"
multiple
disabled={isUploading}
className="h-10 p-0 file:me-3 file:border-0 file:border-e"
type={props.type === "file" ? "file" : "text"}
onChange={() => {
if (props.type === "file") {
formRef.current?.requestSubmit();
return;
}
}}
/>
</div>

<noscript>
<button type="submit" disabled={isUploading}>
{isUploading ? "⏳" : `Upload (server ${props.type})`}
</button>
</noscript>
</form>
);
}

export function Uploader() {
const router = useRouter();

return (
<UTButton
endpoint="anything"
input={{}}
onUploadError={(error) => {
window.alert(error.message);
}}
onClientUploadComplete={() => {
router.refresh();
}}
content={{
allowedContent: <></>,
button: ({ isUploading }) => (isUploading ? null : "Upload"),
}}
appearance={{
button: "!text-sm/6",
allowedContent: "!h-0",
}}
/>
<div className="flex gap-4">
<div className="space-y-1">
<Label>Upload (client)</Label>
<UTButton
endpoint="anything"
input={{}}
onUploadError={(error) => {
window.alert(error.message);
}}
onClientUploadComplete={() => {
router.refresh();
}}
content={{
allowedContent: <></>,
button: ({ isUploading }) =>
isUploading ? null : "Upload (Client)",
}}
appearance={{
button: "!text-sm/6",
allowedContent: "!h-0",
}}
/>
</div>
<ServerUploader type="file" />
<ServerUploader type="url" />
</div>
);
}
37 changes: 37 additions & 0 deletions playground-v6/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { cookies } from "next/headers";
import { redirect } from "next/navigation";

import { UTApi } from "uploadthing/server";
import { UploadFileResult } from "uploadthing/types";

import { CACHE_TAGS, SESSION_COOKIE_NAME } from "./const";
import { getSession, Session } from "./data";
Expand Down Expand Up @@ -35,6 +36,42 @@ export async function signOut() {
redirect("/");
}

export async function uploadFiles(previousState: unknown, form: FormData) {
const session = await getSession();
if (!session) {
return {
success: false as const,
error: "Unauthorized",
};
}

const files = form.getAll("files") as File[] | string[];
let uploadResults: UploadFileResult[];
if (files.some((file) => typeof file === "string")) {
uploadResults = await utapi.uploadFilesFromUrl(files as string[]);
} else {
uploadResults = await utapi.uploadFiles(files as File[]);
}

if (uploadResults.every((result) => result.error !== null)) {
return {
success: false as const,
error: "Failed to upload some files",
};
}

revalidateTag(CACHE_TAGS.LIST_FILES);

const uploadedCount = uploadResults.filter(
(result) => result.data != null,
).length;

return {
success: uploadedCount === uploadResults.length,
uploadedCount,
};
}

export async function getFileUrl(key: string) {
const session = await getSession();
if (!session) {
Expand Down
35 changes: 35 additions & 0 deletions playground/components/fieldset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as React from "react";
import cx from "clsx";

export function Input({
className,
type,
...props
}: React.ComponentProps<"input">) {
return (
<input
type={type}
className={cx(
className,
"border-input focus-visible:border-ring focus-visible:ring-ring/30 flex w-full rounded-lg border px-3 py-2 text-sm shadow-sm shadow-black/5 transition-shadow focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
type === "search" &&
"[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none",
type === "file" &&
"file:border-input p-0 pr-3 italic file:me-3 file:h-full file:border-0 file:border-r file:border-solid file:bg-transparent file:px-3 file:text-sm file:font-medium file:not-italic",
)}
{...props}
/>
);
}

export function Label({ className, ...props }: React.ComponentProps<"label">) {
return (
<label
className={cx(
"text-sm font-medium leading-4 peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className,
)}
{...props}
/>
);
}
88 changes: 70 additions & 18 deletions playground/components/uploader.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,87 @@
"use client";

import { useActionState, useEffect, useRef } from "react";
import { useRouter } from "next/navigation";

import { generateReactHelpers, generateUploadButton } from "@uploadthing/react";

import { UploadRouter } from "../app/api/uploadthing/route";
import { uploadFiles } from "../lib/actions";
import { Input, Label } from "./fieldset";

export const UTButton = generateUploadButton<UploadRouter>();
export const { useUploadThing } = generateReactHelpers<UploadRouter>();

function ServerUploader(props: { type: "file" | "url" }) {
const formRef = useRef<HTMLFormElement>(null);
const [state, dispatch, isUploading] = useActionState(uploadFiles, {
success: false,
error: "",
});

useEffect(() => {
if (state.success === false && state.error) {
window.alert(state.error);
}
}, [state.success]);

return (
<form ref={formRef} action={dispatch}>
<div className="space-y-1">
<Label>Upload (server {props.type})</Label>
<Input
name="files"
multiple
disabled={isUploading}
className="h-10 p-0 file:me-3 file:border-0 file:border-e"
type={props.type === "file" ? "file" : "text"}
onChange={() => {
if (props.type === "file") {
formRef.current?.requestSubmit();
return;
}
}}
/>
</div>

<noscript>
<button type="submit" disabled={isUploading}>
{isUploading ? "⏳" : `Upload (server ${props.type})`}
</button>
</noscript>
</form>
);
}

export function Uploader() {
const router = useRouter();

return (
<UTButton
endpoint={(rr) => rr.anything}
input={{}}
onUploadError={(error) => {
window.alert(error.message);
}}
onClientUploadComplete={() => {
router.refresh();
}}
content={{
allowedContent: <></>,
button: ({ isUploading }) => (isUploading ? null : "Upload"),
}}
appearance={{
button: "!text-sm/6",
allowedContent: "!h-0",
}}
/>
<div className="flex gap-4">
<div className="space-y-1">
<Label>Upload (client)</Label>
<UTButton
endpoint={(rr) => rr.anything}
input={{}}
onUploadError={(error) => {
window.alert(error.message);
}}
onClientUploadComplete={() => {
router.refresh();
}}
content={{
allowedContent: <></>,
button: ({ isUploading }) =>
isUploading ? null : "Upload (Client)",
}}
appearance={{
button: "!text-sm/6",
allowedContent: "!h-0",
}}
/>
</div>
<ServerUploader type="file" />
<ServerUploader type="url" />
</div>
);
}
37 changes: 37 additions & 0 deletions playground/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { cookies } from "next/headers";
import { redirect } from "next/navigation";

import { UTApi } from "uploadthing/server";
import { UploadFileResult } from "uploadthing/types";

import { CACHE_TAGS, SESSION_COOKIE_NAME } from "./const";
import { getSession, Session } from "./data";
Expand Down Expand Up @@ -35,6 +36,42 @@ export async function signOut() {
redirect("/");
}

export async function uploadFiles(previousState: unknown, form: FormData) {
const session = await getSession();
if (!session) {
return {
success: false as const,
error: "Unauthorized",
};
}

const files = form.getAll("files") as File[] | string[];
let uploadResults: UploadFileResult[];
if (files.some((file) => typeof file === "string")) {
uploadResults = await utapi.uploadFilesFromUrl(files as string[]);
} else {
uploadResults = await utapi.uploadFiles(files as File[]);
}

if (uploadResults.every((result) => result.error !== null)) {
return {
success: false as const,
error: "Failed to upload some files",
};
}

revalidateTag(CACHE_TAGS.LIST_FILES);

const uploadedCount = uploadResults.filter(
(result) => result.data != null,
).length;

return {
success: uploadedCount === uploadResults.length,
uploadedCount,
};
}

export async function getFileUrl(key: string) {
const session = await getSession();
if (!session) {
Expand Down

0 comments on commit 8b41a25

Please sign in to comment.