Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create upload endpoint #77

Merged
merged 8 commits into from
Apr 13, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ const getAssignmentsByUserId = async (request: Request, response: Response) => {

const { userId, includePast, isPublished } =
GetAssignmentsQueryValidator.parse(request.query);

const assignments = await GetHandler.getAssignmentsByUserId(
userId,
includePast,
isPublished
);
const assignments = await GetHandler.getAssignmentsByUserId(
userId,
includePast,
isPublished
);

if (!assignments) {
response.status(HttpStatusCode.NOT_FOUND).json({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const getAssignmentsByUserId = async (
},
});

// TODO: search user under courses, what assignments are there

const assignmentsDto: Assignment[] = assignments.map((assignment) => {
return {
id: assignment.id,
Expand Down
4 changes: 2 additions & 2 deletions frontend/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"warn",
{
"selector": "default",
"format": ["camelCase"],
"format": ["camelCase", "UPPER_CASE", "PascalCase"],
"leadingUnderscore": "allow"
},
{
Expand Down Expand Up @@ -62,7 +62,7 @@
"react/no-typos": "warn",
"react/display-name": "warn",
"react/self-closing-comp": "warn",
"react/jsx-max-depth": ["warn", { "max": 5 }]
"react/jsx-max-depth": ["warn", { "max": 7 }]
},
"overrides": [
{
Expand Down
Empty file added frontend/config.ts
Empty file.
13 changes: 12 additions & 1 deletion frontend/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "utfs.io",
port: "",
pathname: "/f/**",
},
],
},
};

export default nextConfig;
4 changes: 3 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@nextui-org/react": "^2.2.10",
"@radix-ui/react-toast": "^1.1.5",
"@tanstack/react-query": "^5.24.1",
"@uploadthing/react": "^6.4.2",
"add": "^2.0.6",
"axios": "^1.6.7",
"class-variance-authority": "^0.7.0",
Expand All @@ -41,7 +42,8 @@
"tailwind": "^4.0.0",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"yarn": "^1.22.22"
"yarn": "^1.22.22",
"uploadthing": "^6.8.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.4.2",
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/app/api/uploadthing/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { cookies } from "next/headers";
import { createUploadthing, type FileRouter } from "uploadthing/next";
import { UploadThingError } from "uploadthing/server";

const f = createUploadthing();

const auth = (_req: Request) => {
const userCookie = cookies().get("token");
return userCookie;
};

// 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(({ req }) => {
// Delete the comment below when we have actual auth
// eslint-disable-next-line @typescript-eslint/await-thenable
const user = auth(req);
if (!user) throw new UploadThingError("Unauthorized");
return { userCookie: user };
})
.onUploadComplete(({ metadata, file }) => {
// This code RUNS ON YOUR SERVER after upload
console.log("Upload complete for userId:", metadata.userCookie);

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

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

export type OurFileRouter = typeof ourFileRouter;
9 changes: 9 additions & 0 deletions frontend/src/app/api/uploadthing/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createRouteHandler } from "uploadthing/next";

import { ourFileRouter } from "./core";

// Export routes for Next App Router, POST is keyword
// eslint-disable-next-line @typescript-eslint/naming-convention
export const { POST } = createRouteHandler({
router: ourFileRouter,
});
2 changes: 2 additions & 0 deletions frontend/src/app/assignments/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ function Page({ params }: Props) {
return () => {
disableEditing();
};
// Run once on page load
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const handleAddQuestion = () => {
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ export default function DashBoard() {
{isLoading ? (
<LogoLoading />
) : (
<AssignmentList assignments={assignments} userRole={user?.role ?? ""} />
<AssignmentList
assignments={assignments}
userRole={user?.role ?? "student"}
/>
)}
</div>
);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,4 @@ export default function Home() {
</div>
);
}
/* eslint-enable @typescript-eslint/no-misused-promises */
2 changes: 2 additions & 0 deletions frontend/src/app/user/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export default function Page() {
setIsLoading(true);
router.push("/");
}
// only fetch once, we do not need to update
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export default function AssignmentEditor({ isEditing = false }: Props) {
setDescription(assignment.description ?? "");
setIsPublished(assignment.isPublished);
}
// Run once on page load
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { toast } = useToast();
const { user } = useUserContext();
Expand All @@ -69,6 +71,8 @@ export default function AssignmentEditor({ isEditing = false }: Props) {
break;
}
},
// Run once on page load
// eslint-disable-next-line react-hooks/exhaustive-deps
[title, deadline, description]
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ function DescriptionField({
placeholder,
className,
}: Props) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const ReactQuill = useMemo(
() => dynamic(() => import("react-quill"), { ssr: false }),
[]
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/assignment/create/QuestionEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ function QuestionEditor({
setLanguage(initialQuestion.referenceSolution?.language ?? "python");
setReferenceSolutionCode(initialQuestion.referenceSolution?.code ?? "");
setTestCases(initialQuestion.testCases ?? []);
// runs once when loaded, does not change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
Expand All @@ -71,6 +73,8 @@ function QuestionEditor({
) {
onQuestionChange(newQuestion);
}
// onQuestionChange and initialQuestion do not change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
title,
description,
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/common/ReadOnlyUserCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function ReadOnlyFullUserCard({ userInfo }: { userInfo: UserInfo }) {
<Card>
<Avatar
showFallback
src={userInfo.photo}
src={userInfo.avatarUrl}
className="w-20 h-20 text-large"
/>
<div>
Expand All @@ -28,7 +28,7 @@ export function ReadOnlyUserCard({ userInfo }: { userInfo: UserInfo }) {
<Card>
<Avatar
showFallback
src={userInfo.photo}
src={userInfo.avatarUrl}
className="w-20 h-20 text-large"
/>
<div>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/common/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export default function SideBar() {
return;
});
}
// router does not change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user]);

// obtain current path, if is login/sign up, don't render SideBar
Expand Down
38 changes: 26 additions & 12 deletions frontend/src/components/forms/ProfileEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import FileInput from "./FileInput";
import userService from "@/helpers/user-service/api-wrapper";
import { useUserContext } from "@/contexts/user-context";
import { uploadFiles } from "@/utils/uploadthing";

export default function ProfileEditor({ userInfo }: { userInfo: UserInfo }) {
const { user, setUserContext } = useUserContext();
Expand All @@ -24,14 +25,16 @@ export default function ProfileEditor({ userInfo }: { userInfo: UserInfo }) {
return name == "";
}, [name]);
const [bio, setBio] = useState<string>(info.bio);
const [photo, setPhoto] = useState<string | undefined>(info.photo);
const [photo, setPhoto] = useState<string | undefined>(info.avatarUrl);
const [newPhoto, setNewPhoto] = useState<File>();

// userInfo is constant, do not change for now
const hasChanged = useMemo(() => {
if (name != info.name) return true;
if (bio != info.bio) return true;
if (photo != info.photo && !(photo == "" && info.photo == undefined))
if (
photo != info.avatarUrl &&
!(photo == "" && info.avatarUrl == undefined)
)
return true;
return false;
}, [name, bio, photo, info]);
Expand All @@ -41,14 +44,14 @@ export default function ProfileEditor({ userInfo }: { userInfo: UserInfo }) {
setPhoto(URL.createObjectURL(newPhoto));
} else {
setNewPhoto(undefined);
setPhoto(info.photo);
setPhoto(info.avatarUrl);
}
}, [newPhoto, info.photo]);
}, [newPhoto, info.avatarUrl]);

const handleDiscard = () => {
setName(info.name);
setBio(info.bio);
setPhoto(info.photo);
setPhoto(info.avatarUrl);
setNewPhoto(undefined);
};

Expand All @@ -59,22 +62,33 @@ export default function ProfileEditor({ userInfo }: { userInfo: UserInfo }) {
return;
}

if (name == info.name && bio == info.bio && photo == info.photo) {
if (name == info.name && bio == info.bio && photo == info.avatarUrl) {
setMessage("Profile saved!");
return;
}

const dataUpdated: Record<string, string> = {
name: name,
bio: bio,
};

try {
await userService.updateUserInfo(user?.uid ?? 0, {
name: name,
bio: bio,
});
let photoUrl = photo;
if (newPhoto) {
const fileResponse = await uploadFiles("imageUploader", {
files: [newPhoto],
});
photoUrl = fileResponse[0].url;
dataUpdated['"avatarUrl"'] = photoUrl;
setPhoto(photoUrl);
}
await userService.updateUserInfo(user?.uid ?? 0, dataUpdated);
setMessage("Profile saved!");
setInfo({
name: name,
email: info.email,
bio: bio,
photo: photo!,
avatarUrl: photoUrl,
});

setUserContext({
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/contexts/user-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ interface UserContextType {
}
const initialUser: User | null = null;

// React component generated by createContext
// eslint-disable-next-line @typescript-eslint/naming-convention
const UserContext = createContext<UserContextType>({
user: null,
setUserContext: () => {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/helpers/user-service/api-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const getUserInfo = async (uid: number): Promise<UserInfo | null> => {
name: responseData.name,
email: responseData.email,
bio: responseData.bio || "This person doesn't have bio",
photo: responseData.photo,
avatarUrl: responseData.avatarUrl,
};
return userInfo;
} else {
Expand Down
14 changes: 11 additions & 3 deletions frontend/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ export const config = {
matchers: "/:path*",
};

export default function middleware(request: NextRequest) {
const publicRoutes = ["/_next", "/public", "/login", "/sign-up"];
const middleware = (request: NextRequest) => {
const publicRoutes = [
"/_next",
"/public",
"/login",
"/sign-up",
"/api/uploadthing",
];
const redirectRoutes = ["/"];

const path = request.nextUrl.pathname;
Expand All @@ -29,4 +35,6 @@ export default function middleware(request: NextRequest) {
}

return NextResponse.next();
}
};

export default middleware;
2 changes: 1 addition & 1 deletion frontend/src/types/user-service.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface UserInfo {
name: string;
email: string;
bio: string;
photo?: string;
avatarUrl?: string;
}

interface ErrorResponse {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/utils/classMergeUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
export const cn = (...inputs: ClassValue[]) => {
return twMerge(clsx(inputs));
}
};
5 changes: 5 additions & 0 deletions frontend/src/utils/uploadthing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { OurFileRouter } from "@/app/api/uploadthing/core";
import { generateReactHelpers } from "@uploadthing/react";

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