Skip to content

Commit

Permalink
Merge pull request #13 from miljan-code/feat/uploadthing
Browse files Browse the repository at this point in the history
🚧 feat: uploadthing
  • Loading branch information
miljan-code authored Feb 25, 2024
2 parents c3b33ab + 4faa2bc commit 58994e2
Show file tree
Hide file tree
Showing 19 changed files with 206 additions and 66 deletions.
10 changes: 8 additions & 2 deletions packages/cli/src/commands/add/helpers/check-argument.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { getUserPkgExec } from "@/utils/get-user-pkg-manager.js";
import { logger } from "@/utils/logger.js";

const availablePackages = ["drizzle", "nextauth", "trpc", "shadcn"] as const;
const availablePackages = [
"drizzle",
"nextauth",
"trpc",
"shadcn",
"uploadthing",
] as const;

export type AvailablePackage = (typeof availablePackages)[number];

Expand All @@ -18,6 +24,6 @@ const printArgError = () => {
const pkgExec = getUserPkgExec();

logger.warn("Please, select one of the available packages:");
logger.warn("> nextauth, drizzle, trpc, shadcn");
logger.warn(`> ${availablePackages.join(", ")}`);
logger.info(`\n${pkgExec} next-kickstart add <package>`);
};
2 changes: 2 additions & 0 deletions packages/cli/src/commands/add/installers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { drizzleInstaller } from "./drizzle.js";
import { nextauthInstaller } from "./nextauth.js";
import { shadcnInstaller } from "./shadcn.js";
import { trpcInstaller } from "./trpc.js";
import { uploadthingInstaller } from "./uploadthing.js";

export const mapPackages = () => ({
drizzle: drizzleInstaller,
nextauth: nextauthInstaller,
trpc: trpcInstaller,
shadcn: shadcnInstaller,
uploadthing: uploadthingInstaller,
});

export type MappedPackages = ReturnType<typeof mapPackages>;
33 changes: 33 additions & 0 deletions packages/cli/src/commands/add/installers/uploadthing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import ora from "ora";
import chalk from "chalk";

import { InstallPackagesOpts } from "@/commands/init/helpers/install-packages.js";
import { depInstaller } from "../helpers/dep-installer.js";
import { logger } from "@/utils/logger.js";
import { fsUploadthing } from "@/commands/common/fs-helpers.js";
import { updateKickstartConfig } from "@/commands/common/update-kickstart-config.js";

export const uploadthingInstaller = async ({
packages,
projectDir,
}: InstallPackagesOpts) => {
const loader = ora("Installing package dependencies").start();
await depInstaller({
projectDir,
deps: ["uploadthing", "@uploadthing/react"],
isDev: false,
});
loader.stop();
logger.success(`Dependencies has been installed successfully.`);

// Copy configuration files
fsUploadthing({ projectDir });
logger.success("Package setup files are successfully scaffolded.\n");

// Update next-kickstarter config
updateKickstartConfig(projectDir, "uploadthing");

// Next steps
logger.info("Find out more about Uploadthing:");
logger.info(` ${chalk.white("https://docs.kickstart.miljan.xyz")}`);
};
4 changes: 4 additions & 0 deletions packages/cli/src/commands/common/dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export const dependencies = {
"lucide-react": "^0.316.0",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",

// Uploadthing
uploadthing: "^6.4.1",
"@uploadthing/react": "^6.2.4",
};

export type Dependency = keyof typeof dependencies;
23 changes: 23 additions & 0 deletions packages/cli/src/commands/common/fs-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,26 @@ export const fsTRPC = ({ projectDir, packages }: FsTRPCOpts) => {
fs.copySync(trpcRoutersSrc, trpcRoutersDest);
fs.copySync(trpcApiSrc, trpcApiDest);
};

interface FsUploadthingOpts {
projectDir: string;
}

export const fsUploadthing = ({ projectDir }: FsUploadthingOpts) => {
const utDir = path.join(PKG_ROOT, "template/libs/uploadthing");

const apiRoute = "app/api/uploadthing/";
const apiCoreSrc = path.join(utDir, apiRoute, "core.ts");
const apiCoreDest = path.join(projectDir, apiRoute, "core.ts");
const apiRouteSrc = path.join(utDir, apiRoute, "route.ts");
const apiRouteDest = path.join(projectDir, apiRoute, "route.ts");
const uploaderSrc = path.join(utDir, "components/uploader.tsx");
const uploaderDest = path.join(projectDir, "components/uploader.tsx");
const helpersSrc = path.join(utDir, "lib/uploadthing.ts");
const helpersDest = path.join(projectDir, "lib/uploadthing.ts");

fs.copySync(apiCoreSrc, apiCoreDest);
fs.copySync(apiRouteSrc, apiRouteDest);
fs.copySync(uploaderSrc, uploaderDest);
fs.copySync(helpersSrc, helpersDest);
};
5 changes: 5 additions & 0 deletions packages/cli/src/commands/common/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export async function checkPackages() {
message: "Would you like to use ShadCN/ui?",
});
},
uploadthing: () => {
return confirm({
message: "Would you like to use Uploadthing?",
});
},
},
{
onCancel() {
Expand Down
54 changes: 32 additions & 22 deletions packages/cli/src/commands/common/update-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import fs from "fs-extra";
import { PKG_ROOT } from "@/constants.js";
import { type InstallPackagesOpts } from "../init/helpers/install-packages.js";
import { type AvailablePackage } from "@/commands/add/helpers/check-argument.js";
import { type Packages } from "./prompts.js";

const envVariables = {
drizzle: {
Expand All @@ -15,40 +16,49 @@ const envVariables = {
GOOGLE_CLIENT_ID: `""\n`,
GOOGLE_CLIENT_SECRET: `""\n`,
},
uploadthing: {
UPLOADTHING_SECRET: `""\n`,
UPLOADTHING_APP_ID: `""\n`,
},
};

export const createEnv = ({ packages, projectDir }: InstallPackagesOpts) => {
let envContent = "";

if (packages.drizzle) {
for (const [key, value] of Object.entries(envVariables.drizzle)) {
for (const pkg of getKeys(envVariables)) {
if (!packages[pkg]) continue;
for (const [key, value] of Object.entries(envVariables[pkg])) {
envContent += `${key}=${value}`;
}
}

if (packages.nextauth) {
for (const [key, value] of Object.entries(envVariables.nextauth)) {
envContent += `${key}=${value}`;
}
}

if (packages.drizzle || packages.nextauth) {
const envPath = path.join(projectDir, ".env.example");
fs.writeFileSync(envPath, envContent);
}

const t3EnvFile =
packages.drizzle && packages.nextauth
? "env-auth-db.mjs"
: packages.drizzle
? "env-db.mjs"
: packages.nextauth
? "env-auth.mjs"
: "env-base.mjs";
const t3EnvSrc = path.join(PKG_ROOT, "template/libs/providers", t3EnvFile);
const envPath = path.join(projectDir, ".env.example");
const t3EnvSrc = path.join(PKG_ROOT, "template/libs/providers/env.mjs");
const t3EnvDest = path.join(projectDir, "env.mjs");

fs.writeFileSync(envPath, envContent);
fs.copySync(t3EnvSrc, t3EnvDest);
removeUselessVars(t3EnvDest, packages);
};

export const updateEnv = (projectDir: string, pkg: AvailablePackage) => {};

function removeUselessVars(filePath: string, packages: Packages) {
let content = fs.readFileSync(filePath, "utf-8");

for (const pkg of getKeys(envVariables)) {
if (packages[pkg]) continue;
for (const envVar of getKeys(envVariables[pkg])) {
content = content
.split("\n")
.filter((line) => !line.includes(envVar))
.join("\n");
}
}

fs.writeFileSync(filePath, content, "utf-8");
}

function getKeys<T extends {}>(obj: T) {
return Object.keys(obj) as Array<keyof T>;
}
1 change: 1 addition & 0 deletions packages/cli/src/commands/init/helpers/map-package-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const ALL_PACKAGES = {
nextauth: true,
trpc: true,
shadcn: true,
uploadthing: true,
};

export const mapPackageList = async (opts: Options): Promise<Packages> => {
Expand Down
7 changes: 2 additions & 5 deletions packages/cli/src/commands/init/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { Command } from "commander";
import { z } from "zod";

import {
checkPackages,
checkInstalls,
getProjectName,
} from "../common/prompts.js";
import { checkInstalls, getProjectName } from "../common/prompts.js";
import { generateKickstartConfig } from "../common/update-kickstart-config.js";
import { generateStarter } from "./helpers/generate-starter.js";
import { parsePath } from "./helpers/parse-path.js";
Expand Down Expand Up @@ -69,4 +65,5 @@ export const init = new Command()
.option("--shadcn", "install with shadcn", false)
.option("--trpc", "install with trpc", false)
.option("--nextauth", "install with nextauth", false)
.option("--uploadthing", "install with uploadthing", false)
.action(initAction);
5 changes: 5 additions & 0 deletions packages/cli/src/commands/init/installers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { nextAuthInstaller } from "./next-auth.js";
import { trpcInstaller } from "./trpc.js";
import { shadcnInstaller } from "./shadcn.js";
import { type Packages } from "@/commands/common/prompts.js";
import { uploadthingInstaller } from "./uploadthing.js";

export const mapPackages = (packages: Packages) => ({
drizzle: {
Expand All @@ -21,6 +22,10 @@ export const mapPackages = (packages: Packages) => ({
added: packages.shadcn,
install: shadcnInstaller,
},
uploadthing: {
added: packages.uploadthing,
install: uploadthingInstaller,
},
});

export type MappedPackages = ReturnType<typeof mapPackages>;
16 changes: 16 additions & 0 deletions packages/cli/src/commands/init/installers/uploadthing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import path from "node:path";

import { fsUploadthing } from "@/commands/common/fs-helpers.js";
import { addPackageDeps } from "@/commands/common/add-package-deps.js";
import { type Dependency } from "@/commands/common/dependencies.js";
import { type InstallPackagesOpts } from "../helpers/install-packages.js";

export const uploadthingInstaller = ({
projectDir,
packages,
}: InstallPackagesOpts) => {
const pkgJsonPath = path.join(projectDir, "package.json");
const deps: Dependency[] = ["uploadthing", "@uploadthing/react"];
addPackageDeps({ deps, isDev: false, pkgJsonPath });
fsUploadthing({ projectDir });
};
18 changes: 0 additions & 18 deletions packages/cli/template/libs/providers/env-auth.mjs

This file was deleted.

7 changes: 0 additions & 7 deletions packages/cli/template/libs/providers/env-base.mjs

This file was deleted.

12 changes: 0 additions & 12 deletions packages/cli/template/libs/providers/env-db.mjs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const env = createEnv({
GOOGLE_CLIENT_ID: z.string().min(1),
GOOGLE_CLIENT_SECRET: z.string().min(1),
NEXTAUTH_SECRET: z.string().min(1),
UPLOADTHING_SECRET: z.string().min(1),
UPLOADTHING_APP_ID: z.string().min(1),
},
client: {},
runtimeEnv: {
Expand All @@ -16,5 +18,7 @@ export const env = createEnv({
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
UPLOADTHING_SECRET: process.env.UPLOADTHING_SECRET,
UPLOADTHING_APP_ID: process.env.UPLOADTHING_APP_ID,
},
});
31 changes: 31 additions & 0 deletions packages/cli/template/libs/uploadthing/app/api/uploadthing/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createUploadthing, type FileRouter } from "uploadthing/next";

import { getUserSession } from "@/lib/auth";

const f = createUploadthing();

// FileRouter for your app, can contain multiple FileRoutes
export const ourFileRouter = {
// Define as many FileRoutes as you like, each with a unique routeSlug
imageUploader: f({ image: { maxFileSize: "4MB" } })
// Set permissions and file types for this FileRoute
.middleware(async () => {
// This code runs on your server before upload
const { session } = await getUserSession();
if (!session?.user.id) throw new Error("Unauthorized");

// Whatever is returned here is accessible in onUploadComplete as `metadata`
return { userId: session.user.id };
})
.onUploadComplete(({ metadata, file }) => {
// This code RUNS ON YOUR SERVER after upload
console.log("Upload complete for userId:", metadata.userId);

console.log("file url", file.url);

// !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback
return { uploadedBy: metadata.userId };
}),
} satisfies FileRouter;

export type OurFileRouter = typeof ourFileRouter;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ourFileRouter } from "./core";
import { createRouteHandler } from "uploadthing/next";

// Export routes for Next App Router
export const { GET, POST } = createRouteHandler({
router: ourFileRouter,
});
27 changes: 27 additions & 0 deletions packages/cli/template/libs/uploadthing/components/uploader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";

import { useUploadThing } from "@/lib/uploadthing";

export const Uploader = () => {
const { startUpload, isUploading } = useUploadThing("imageUploader", {
onClientUploadComplete: (res) => {
if (!res) return;
const imageUrl = res[0]?.url || "";
console.log(imageUrl);
},
});

const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
await startUpload([file]);
};

return (
<input
type="file"
disabled={isUploading}
onChange={(e) => void handleImageUpload(e)}
/>
);
};
6 changes: 6 additions & 0 deletions packages/cli/template/libs/uploadthing/lib/uploadthing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { generateReactHelpers } from "@uploadthing/react/hooks";

import type { OurFileRouter } from "@/app/api/uploadthing/core";

export const { useUploadThing, uploadFiles } =
generateReactHelpers<OurFileRouter>();

0 comments on commit 58994e2

Please sign in to comment.