Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 144 additions & 11 deletions bun.lock

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
import "./src/env.js";

/** @type {import("next").NextConfig} */
const config = {};

const config = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "mvfgovwbkpojurkplimu.supabase.co",
},
],
},
}
export default config;

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.4.7",
"@mui/material": "^6.4.7",
"@supabase/supabase-js": "^2.49.1",
"@t3-oss/env-nextjs": "^0.10.1",
"@tanstack/react-query": "^5.50.0",
"@trpc/client": "^11.0.0-rc.446",
Expand Down
Binary file added public/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 23 additions & 36 deletions src/app/_components/post.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,37 @@
"use client";

import { useState } from "react";
import { FormEvent, useState } from "react";

import { api } from "~/trpc/react";
import Image from "next/image";

export function LatestPost() {
const [latestPost] = api.post.getLatest.useSuspenseQuery();

const utils = api.useUtils();
const [name, setName] = useState("");
const createPost = api.post.create.useMutation({
onSuccess: async () => {
await utils.post.invalidate();
setName("");
},
});

return (
<div className="w-full max-w-xs">
{latestPost ? (
<p className="truncate">Your most recent post: {latestPost.name}</p>
) : (
<p>You have no posts yet.</p>
)}
<form
onSubmit={(e) => {
e.preventDefault();
createPost.mutate({ name });
}}
className="flex flex-col gap-2"
>
<input
type="text"
placeholder="Title"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full rounded-full px-4 py-2 text-black"
/>
<button
type="submit"
className="rounded-full bg-white/10 px-10 py-3 font-semibold transition hover:bg-white/20"
disabled={createPost.isPending}
<p className="truncate">Your most recent post: {latestPost?.name}</p>
{latestPost && (
<div
className={
"mb-2 flex h-[200px] w-[320px] max-w-xs flex-col gap-4 rounded-xl bg-white/30 p-4 hover:bg-white/40"
}
>
{createPost.isPending ? "Submitting..." : "Submit"}
</button>
</form>
<div>
<p className={"text-xl font-bold text-[#f08080]"}>
{latestPost.name}
</p>
{latestPost.imageUrl && (
<Image
src={latestPost.imageUrl}
width={"100"}
height={"100"}
alt={"image"}
></Image>
)}
</div>
</div>
)}
</div>
);
}
94 changes: 94 additions & 0 deletions src/app/_components/postForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"use client";
import Add from "@mui/icons-material/Add";

import { FormEvent, useState } from "react";

import { api } from "~/trpc/react";
import { uploadImage } from "~/supabase/image-service";
import { Dialog } from "@mui/material";

export function PostForm() {
const utils = api.useUtils();
const [name, setName] = useState("");
const [selectedFile, setSelectedFile] = useState<File | null>();
const [openModal, setOpenModal] = useState(false);

const createPost = api.post.create.useMutation({
onSuccess: async () => {
await utils.post.invalidate();
setName("");
setOpenModal(false);
setSelectedFile(null);
},
});

const submit = async (e: FormEvent) => {
e.preventDefault();
let url;
if (selectedFile) {
url = await uploadImage(selectedFile);
}
createPost.mutate({ name: name, imageUrl: url ?? undefined });
};

return (
<div className="flex w-full max-w-xs flex-col items-center py-2">
<button
onClick={() => setOpenModal(true)}
className="opacity my-3 flex items-center rounded-xl bg-white/40 px-4 py-3 transition hover:bg-white/30"
>
<Add />
</button>
{openModal && (
<Dialog
open={openModal}
onClose={() => setOpenModal(false)}
className="relative z-10"
>
<div
className="relative z-10"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
>
<div>
<p className={"px-2 py-2 text-lg font-bold text-[#f08080]"}>
Lag ny post
</p>
<form
onSubmit={submit}
className="flex flex-col items-center gap-2 p-4"
>
<input
type="text"
placeholder="Title"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full rounded-xl border-2 border-gray-200 p-2"
/>
<input
className={
"w-full rounded-xl border-2 border-gray-200 p-2 px-4 text-black"
}
type="file"
onChange={(event) =>
setSelectedFile(
event.target.files ? event.target.files[0] : null,
)
}
/>
<button
type="submit"
className="w-20 rounded-xl bg-[#f08080]/50 py-2 font-semibold transition hover:bg-[#f08080]/30"
disabled={createPost.isPending}
>
{createPost.isPending ? "Submitting..." : "Submit"}
</button>
</form>
</div>
</div>
</Dialog>
)}
</div>
);
}
2 changes: 1 addition & 1 deletion src/app/api/trpc/[trpc]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const handler = (req: NextRequest) =>
env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
);
}
: undefined,
Expand Down
52 changes: 13 additions & 39 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,25 @@
import Link from "next/link";

import { LatestPost } from "~/app/_components/post";
import { api, HydrateClient } from "~/trpc/server";
import Image from "next/image";
import { PostForm } from "~/app/_components/postForm";

export default async function Home() {
const hello = await api.post.hello({ text: "from tRPC" });

void api.post.getLatest.prefetch();

return (
<HydrateClient>
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white">
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16">
<h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]">
Create <span className="text-[hsl(280,100%,70%)]">T3</span> App
</h1>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-8">
<Link
className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20"
href="https://create.t3.gg/en/usage/first-steps"
target="_blank"
>
<h3 className="text-2xl font-bold">First Steps →</h3>
<div className="text-lg">
Just the basics - Everything you need to know to set up your
database and authentication.
</div>
</Link>
<Link
className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20"
href="https://create.t3.gg/en/introduction"
target="_blank"
>
<h3 className="text-2xl font-bold">Documentation →</h3>
<div className="text-lg">
Learn more about Create T3 App, the libraries it uses, and how
to deploy it.
</div>
</Link>
<main className="flex min-h-screen justify-center bg-gradient-to-b from-[#f6c492] to-[#f6b092] text-white">
<div className="container flex flex-col items-center justify-between px-4">
<div className={"flex flex-col items-center"}>
<Image
src={"/logo.png"}
alt={"platepals"}
width={"150"}
height={"150"}
/>
<LatestPost />
</div>
<div className="flex flex-col items-center gap-2">
<p className="text-2xl text-white">
{hello ? hello.greeting : "Loading tRPC query..."}
</p>
</div>

<LatestPost />
<PostForm />
</div>
</main>
</HydrateClient>
Expand Down
2 changes: 2 additions & 0 deletions src/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const env = createEnv({
* isn't built with invalid env vars.
*/
server: {
SUPABASE_KEY: z.string(),
DATABASE_URL: z.string().url(),
NODE_ENV: z
.enum(["development", "test", "production"])
Expand All @@ -27,6 +28,7 @@ export const env = createEnv({
* middlewares) or client-side so we need to destruct manually.
*/
runtimeEnv: {
SUPABASE_KEY: process.env.SUPABASE_KEY,
DATABASE_URL: process.env.DATABASE_URL,
NODE_ENV: process.env.NODE_ENV,
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
Expand Down
12 changes: 9 additions & 3 deletions src/server/api/routers/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { z } from "zod";

import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
import { posts } from "~/server/db/schema";
import { createSignedUrl } from "~/supabase/image-service";

export const postRouter = createTRPCRouter({
hello: publicProcedure
Expand All @@ -13,18 +14,23 @@ export const postRouter = createTRPCRouter({
}),

create: publicProcedure
.input(z.object({ name: z.string().min(1) }))
.input(
z.object({ name: z.string().min(1), imageUrl: z.string().optional() }),
)
.mutation(async ({ ctx, input }) => {
await ctx.db.insert(posts).values({
name: input.name,
imageUrl: input.imageUrl,
});
}),

getLatest: publicProcedure.query(async ({ ctx }) => {
const post = await ctx.db.query.posts.findFirst({
orderBy: (posts, { desc }) => [desc(posts.createdAt)],
});

return post ?? null;
if (!post) return null;
if (!post?.imageUrl) return post;
const signedUrl = await createSignedUrl(post.imageUrl);
return { ...post, imageUrl: signedUrl };
}),
});
5 changes: 3 additions & 2 deletions src/server/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ export const posts = createTable(
{
id: integer("id").primaryKey().generatedByDefaultAsIdentity(),
name: varchar("name", { length: 256 }),
imageUrl: varchar("imageUrl"),
createdAt: timestamp("created_at", { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(
() => new Date()
() => new Date(),
),
},
(example) => ({
nameIndex: index("name_idx").on(example.name),
})
}),
);
20 changes: 20 additions & 0 deletions src/supabase/image-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { supabase } from "~/supabase/supabase";

export async function uploadImage(file: File) {
const uniqueName = `${Date.now()}-${file.name}`;
const { data, error } = await supabase.storage
.from("post-images")
.upload(`public/${uniqueName}`, file);
if (error) {
console.error("Upload failed", error);
return null;
}
return data.path;
}

export async function createSignedUrl(url: string) {
const { data } = await supabase.storage
.from("post-images")
.createSignedUrl(url, 3600);
return data?.signedUrl;
}
6 changes: 6 additions & 0 deletions src/supabase/supabase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createClient } from "@supabase/supabase-js";

const supabaseUrl = "https://mvfgovwbkpojurkplimu.supabase.co";
const supabaseKey =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im12Zmdvdndia3BvanVya3BsaW11Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDE0MzEzODYsImV4cCI6MjA1NzAwNzM4Nn0.NrKBQt1tWNh7seAZjbBmdcWzV-dVhwx6A1XVFOIXjgo";
export const supabase = createClient(supabaseUrl, supabaseKey);
2 changes: 1 addition & 1 deletion src/trpc/react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function TRPCReactProvider(props: { children: React.ReactNode }) {
},
}),
],
})
}),
);

return (
Expand Down
2 changes: 1 addition & 1 deletion src/trpc/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ const caller = createCaller(createContext);

export const { trpc: api, HydrateClient } = createHydrationHelpers<AppRouter>(
caller,
getQueryClient
getQueryClient,
);