Skip to content

Commit e68fa92

Browse files
Create file upload component (#75)
* File uploader * Created uploader
1 parent 8402e49 commit e68fa92

File tree

8 files changed

+154
-24
lines changed

8 files changed

+154
-24
lines changed

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@trpc/client": "^11.0.0-rc.446",
3535
"@trpc/react-query": "^11.0.0-rc.446",
3636
"@trpc/server": "^11.0.0-rc.446",
37+
"browser-image-compression": "^2.0.2",
3738
"class-variance-authority": "^0.7.0",
3839
"clsx": "^2.1.0",
3940
"embla-carousel-react": "^8.0.0-rc22",
@@ -65,8 +66,8 @@
6566
"postcss": "^8.4.39",
6667
"prettier": "^3.3.2",
6768
"prettier-plugin-tailwindcss": "^0.6.5",
68-
"prisma-erd-generator": "^1.11.2",
6969
"prisma": "^5.19.1",
70+
"prisma-erd-generator": "^1.11.2",
7071
"tailwindcss": "^3.4.3",
7172
"tailwindcss-animate": "^1.0.7",
7273
"typescript": "^5.5.3",

pnpm-lock.yaml

+26
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/file-upload.tsx

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { env } from "../env";
5+
import { Input } from "./ui/input";
6+
import { Label } from "./ui/label";
7+
import compressImage from "browser-image-compression";
8+
import { useToast } from "../hooks/use-toast";
9+
import { useSession } from "next-auth/react";
10+
11+
export const blobToFile = (blob: Blob, name: string, type: string): File => {
12+
return new File([blob], name, { type });
13+
};
14+
15+
const uploadFile = async (file: File, tihldeAccessToken: string) => {
16+
const compressedImage = await compressImage(file, {
17+
maxSizeMB: 0.2,
18+
maxWidthOrHeight: 300,
19+
fileType: file.type,
20+
});
21+
22+
const newFile = blobToFile(compressedImage, file.name, file.type);
23+
24+
const data = new FormData();
25+
26+
data.append("file", newFile);
27+
28+
const response = await fetch(`${env.NEXT_PUBLIC_LEPTON_API_URL}/upload/`, {
29+
method: "POST",
30+
body: data,
31+
headers: {
32+
"X-CSRF-Token": tihldeAccessToken,
33+
Accept: "application/json",
34+
},
35+
});
36+
37+
if (!response.ok) {
38+
throw new Error(`Failed to upload file: ${response}`);
39+
}
40+
41+
const body = await response.json();
42+
43+
return body.url as string;
44+
};
45+
46+
interface FileUploaderProps {
47+
onSelect: (url: string) => void;
48+
}
49+
50+
export default function FileUploader({ onSelect }: FileUploaderProps) {
51+
const [imageFile, setImageFile] = useState<File | undefined>(undefined);
52+
const { toast } = useToast();
53+
const { data } = useSession();
54+
const [uploadedImageUrl, setUploadedImageUrl] = useState<undefined | string>(
55+
undefined,
56+
);
57+
58+
const onChanged = async (e: React.ChangeEvent<HTMLInputElement>) => {
59+
try {
60+
if (e.target.files && e.target.files.length > 0) {
61+
const file = e.target.files[0];
62+
setImageFile(file);
63+
if (file && data?.user.tihldeJWT) {
64+
const url = await uploadFile(file, data.user.tihldeJWT);
65+
setUploadedImageUrl(url!);
66+
67+
toast({
68+
title: "Filen din ble lastet opp",
69+
});
70+
onSelect(url);
71+
}
72+
}
73+
} catch (e) {
74+
toast({
75+
title: "Oops, det skjedde noe galt",
76+
description: "Vennligst prøv igjen",
77+
});
78+
}
79+
};
80+
81+
return (
82+
<div className="grid w-full max-w-sm items-center gap-1.5">
83+
<Label htmlFor="picture">Last opp bilde</Label>
84+
<Input id="picture" type="file" onChange={onChanged} />
85+
{uploadedImageUrl && (
86+
<img src={uploadedImageUrl} alt="Ditt opplastede bilde" />
87+
)}
88+
</div>
89+
);
90+
}

src/env.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@ export const env = createEnv({
2222
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
2323
process.env.VERCEL ? z.string() : z.string().url(),
2424
),
25-
LEPTON_API_URL: z.string({
26-
message:
27-
"Legg til en URL som peker til Lepton backenden vår. Hvis du kjører denne lokalt, bruk 'http://localhost:8000', eller den offisielle på 'https://api.tihlde.org'",
28-
}),
2925
ALLOWED_GROUP_SLUGS: z
3026
.string()
3127
.refine((i) => i.split(",").length > 0, {
@@ -43,6 +39,10 @@ export const env = createEnv({
4339
*/
4440
client: {
4541
// NEXT_PUBLIC_CLIENTVAR: z.string(),
42+
NEXT_PUBLIC_LEPTON_API_URL: z.string({
43+
message:
44+
"Legg til en URL som peker til Lepton backenden vår. Hvis du kjører denne lokalt, bruk 'http://localhost:8000', eller den offisielle på 'https://api.tihlde.org'",
45+
}),
4646
},
4747

4848
/**
@@ -54,7 +54,7 @@ export const env = createEnv({
5454
NODE_ENV: process.env.NODE_ENV,
5555
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
5656
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
57-
LEPTON_API_URL: process.env.LEPTON_API_URL,
57+
NEXT_PUBLIC_LEPTON_API_URL: process.env.NEXT_PUBLIC_LEPTON_API_URL,
5858
ALLOWED_GROUP_SLUGS: process.env.ALLOWED_GROUP_SLUGS,
5959
},
6060
/**

src/server/auth.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ declare module "next-auth" {
2121
id: string;
2222
nickname: string;
2323
role: UserRole;
24+
tihldeJWT?: string;
2425
}
2526

2627
// The session object only contains the user info
@@ -38,6 +39,7 @@ declare module "next-auth/jwt" {
3839
nickname: string;
3940
role: UserRole;
4041
id: string;
42+
tihldeJWT?: string;
4143
};
4244
}
4345
}
@@ -51,6 +53,7 @@ export const authOptions: NextAuthOptions = {
5153
id: token.user.id,
5254
nickname: token.user.nickname,
5355
role: token.user.role,
56+
tihldeJWT: token.user.tihldeJWT,
5457
},
5558
};
5659
},
@@ -62,6 +65,7 @@ export const authOptions: NextAuthOptions = {
6265
nickname: user.nickname,
6366
role: user.role,
6467
id: user.id,
68+
tihldeJWT: user.tihldeJWT,
6569
},
6670
};
6771
}
@@ -129,12 +133,12 @@ export const authOptions: NextAuthOptions = {
129133
});
130134
}
131135

132-
const session = {
136+
return {
133137
id: userId,
134138
nickname,
135139
role: getRoleForUser(memberships),
140+
tihldeJWT: token,
136141
};
137-
return session;
138142
},
139143
}),
140144
// Users can log in anonymously, using only a nickname,

src/server/service/lepton/get-memberships.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import { env } from "../../../env";
88
export const getTIHLDEMemberships = async (
99
token: string,
1010
): Promise<MembershipResponse> => {
11-
const response = await fetch(`${env.LEPTON_API_URL}/users/me/memberships/`, {
12-
headers: {
13-
"Content-Type": "application/json",
14-
"x-csrf-token": token,
11+
const response = await fetch(
12+
`${env.NEXT_PUBLIC_LEPTON_API_URL}/users/me/memberships/`,
13+
{
14+
headers: {
15+
"Content-Type": "application/json",
16+
"x-csrf-token": token,
17+
},
1518
},
16-
});
19+
);
1720

1821
if (!response.ok) {
1922
console.error(response.status, response.statusText, await response.json());

src/server/service/lepton/get-user.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ export const getTIHLDEUser = async (
1010
token: string,
1111
username: string,
1212
): Promise<TIHLDEUser> => {
13-
const response = await fetch(`${env.LEPTON_API_URL}/users/${username}/`, {
14-
headers: {
15-
"Content-Type": "application/json",
16-
"x-csrf-token": token,
13+
const response = await fetch(
14+
`${env.NEXT_PUBLIC_LEPTON_API_URL}/users/${username}/`,
15+
{
16+
headers: {
17+
"Content-Type": "application/json",
18+
"x-csrf-token": token,
19+
},
1720
},
18-
});
21+
);
1922

2023
if (!response.ok) {
2124
console.error(response.status, response.statusText, await response.json());

src/server/service/lepton/login.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ export const loginToTIHLDE = async (
44
username: string,
55
password: string,
66
): Promise<string | null> => {
7-
const response = await fetch(`${env.LEPTON_API_URL}/auth/login/`, {
8-
method: "POST",
9-
headers: {
10-
"Content-Type": "application/json",
7+
const response = await fetch(
8+
`${env.NEXT_PUBLIC_LEPTON_API_URL}/auth/login/`,
9+
{
10+
method: "POST",
11+
headers: {
12+
"Content-Type": "application/json",
13+
},
14+
body: JSON.stringify({ user_id: username, password }),
1115
},
12-
body: JSON.stringify({ user_id: username, password }),
13-
});
16+
);
1417

1518
if (response.status !== 200) {
1619
console.error(response.status, response.statusText, await response.json());

0 commit comments

Comments
 (0)