diff --git a/backend/src/modules/content/controllers/content.controller.ts b/backend/src/modules/content/controllers/content.controller.ts index 7915431..95042e2 100644 --- a/backend/src/modules/content/controllers/content.controller.ts +++ b/backend/src/modules/content/controllers/content.controller.ts @@ -45,7 +45,7 @@ export class ContentController { const file = files.thumbnail[0]; const fileName = file.newFilename; const fileType = file.mimetype; - const filePath = file.filepath; // Use 'filepath' for formidable v2+ + const filePath = file.filepath; try { if (!filePath) { @@ -54,10 +54,10 @@ export class ContentController { // Pass the file object with the correct path to StorageService const response = await StorageService.uploadFile( - { ...file, path: filePath }, // Ensure 'path' is set if your StorageService expects it + { ...file, path: filePath }, "thumbnails", fileName, - fileType, + fileType ); res.status(201).json(response); } catch (error: any) { @@ -86,15 +86,16 @@ export class ContentController { static async editContent(req: Request, res: Response) { console.log("Fetching Content..."); - const { contentId, userId } = req.params; - const { data } = req.body; + const { contentId } = req.params; + const data = req.body; + const uid = req.user?.uid; try { // const confirmation = await axios.get(`${apiURL}/content/${contentId}`) const confirmation = await ContentService.getContent(contentId); const owner_id = confirmation?.creatorUID; - if (userId == owner_id) { + if (uid == owner_id) { //check whether they are allowed to edit the content const response = await ContentService.editContent(contentId, data); res.status(200).json(response); @@ -104,7 +105,7 @@ export class ContentController { } catch (error: any) { console.log(error); res - .status(401) + .status(500) .json({ error: error.message || "Failed to edit content" }); } } @@ -156,7 +157,7 @@ export class ContentController { file, "thumbnails", fileName, - fileType, + fileType ); const updateData = JSON.parse(fields.data); diff --git a/backend/src/modules/content/routes/content.routes.ts b/backend/src/modules/content/routes/content.routes.ts index 1105999..656c4d6 100644 --- a/backend/src/modules/content/routes/content.routes.ts +++ b/backend/src/modules/content/routes/content.routes.ts @@ -1,6 +1,7 @@ import { Router } from "express"; import { ContentController } from "../controllers/content.controller"; import commentRouter from "./comment.routes"; +import { authenticateToken } from "../../../shared/middleware/auth"; const contentRoutes = Router(); @@ -23,7 +24,11 @@ contentRoutes.put("/shares/:contentId", ContentController.incrementShareCount); contentRoutes.delete("/:contentId", ContentController.deleteContent); // Delete content by ID -contentRoutes.put("/:contentId/:userId", ContentController.editContent); // Edit content by ID +contentRoutes.put( + "/:contentId", + authenticateToken, + ContentController.editContent +); // Edit content by ID contentRoutes.put( "/editThumbnail/:contentId/:userId", ContentController.editContentAndThumbnail diff --git a/backend/src/modules/storage/services/storage.service.ts b/backend/src/modules/storage/services/storage.service.ts index 4971209..5d1a012 100644 --- a/backend/src/modules/storage/services/storage.service.ts +++ b/backend/src/modules/storage/services/storage.service.ts @@ -107,14 +107,18 @@ export class StorageService { throw new Error("No valid file data found (no filepath or buffer)."); } + // Saving File to direct path fileName = `${fileName}${fileType ? `.${fileType.split("/")[1]}` : ""}`; - const fullPath = path.join(dir, fileName); await fs.writeFile(fullPath, fileBuffer); logger.info(`File saved locally at ${fullPath}`); + // Returning URL for the file + const urlPath = `${filePath}/${fileName}`; + const url = `http://localhost:3000/local_uploads/${urlPath}`; + // Return the absolute file system path - return { url: fullPath }; + return { url }; } catch (error: any) { logger.error("Local upload error:", error); throw new Error(`Local upload failed: ${error.message}`); diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 092408a..2472754 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -1,28 +1,29 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; export default tseslint.config( - { ignores: ['dist'] }, + { ignores: ["dist"] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', + "react-refresh/only-export-components": [ + "warn", { allowConstantExport: true }, ], + "react-hooks/exhaustive-deps": "off", }, - }, -) + } +); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7dfb9b9..7baf440 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -100,7 +100,14 @@ export default function App() { } /> {/* CONTENT */} - } /> + } + /> + } + /> } /> {/* PRO */} diff --git a/frontend/src/components/content/CommentList.tsx b/frontend/src/components/content/CommentList.tsx index 198212b..11668c5 100644 --- a/frontend/src/components/content/CommentList.tsx +++ b/frontend/src/components/content/CommentList.tsx @@ -54,8 +54,11 @@ export default function CommentList({ setLoading(false); return; } - setComments(commentsResult); - setNumComments(commentsResult.length); + + if (commentsResult) { + setComments(commentsResult); + setNumComments(commentsResult.length); + } setLoading(false); }; diff --git a/frontend/src/pages/content/ContentEditor.tsx b/frontend/src/pages/content/ContentEditor.tsx index 40087e2..05409bb 100644 --- a/frontend/src/pages/content/ContentEditor.tsx +++ b/frontend/src/pages/content/ContentEditor.tsx @@ -16,6 +16,7 @@ import OrderedList from "@tiptap/extension-ordered-list"; import { apiURL } from "../../scripts/api"; import Toolbar from "../../components/content/toolbar"; import axios from "axios"; +import { ImageService } from "../../services/ImageService"; export default function ContentEditor({ isEditMode }: { isEditMode: boolean }) { const [title, setTitle] = useState(""); @@ -48,9 +49,10 @@ export default function ContentEditor({ isEditMode }: { isEditMode: boolean }) { }, }); - // --------------------------------------- - // ----------- Event Handlers ------------ - // --------------------------------------- + /* + * Load saved title and content from localStorage and cookies + * This will set the title and content in the editor if they exist + */ useEffect(() => { const savedTitle = localStorage.getItem("title"); const savedContent = Cookies.get("content"); @@ -65,7 +67,10 @@ export default function ContentEditor({ isEditMode }: { isEditMode: boolean }) { } }, [editor]); - // Fetch existing content data if in edit mode + /* + * Fetch existing content if in edit mode + * This will load the content from the server and set it in the editor + */ useEffect(() => { if (isEditMode) { const fetchContent = async () => { @@ -94,13 +99,83 @@ export default function ContentEditor({ isEditMode }: { isEditMode: boolean }) { fetchContent(); } }, [isEditMode, editor]); - // … - // --------------------------------------- - // -------------- Functions -------------- - // --------------------------------------- + /* + * Load thumbnail preview from URL if provided + * This will convert the URL to a File object and set it as the thumbnail + * This is useful for editing existing content with a thumbnail + */ + useEffect(() => { + let objectUrl: string | null = null; + if ( + thumbnailPreview && + typeof thumbnailPreview === "string" && + !thumbnail + ) { + (async () => { + try { + const file = await urlToFile( + thumbnailPreview, + "thumbnail.jpg", + "image/jpeg" + ); + setThumbnail(file); + objectUrl = URL.createObjectURL(file); + setThumbnailPreview(objectUrl); + } catch { + setThumbnail(null); + setThumbnailPreview(null); + setError("Failed to load thumbnail image."); + } + })(); + } else if (!thumbnailPreview) { + setThumbnail(null); + setThumbnailPreview(null); + } + return () => { + if (objectUrl) URL.revokeObjectURL(objectUrl); + }; + }, [thumbnailPreview]); + + /* + * Converts a URL to a File object + * This is used to convert the thumbnail preview URL to a File object + * so it can be uploaded to the server + * + * @param url - The URL of the image + * @param filename - The name of the file + * @param mimeType - The MIME type of the file + * @returns A Promise that resolves to a File object + */ + async function urlToFile( + url: string, + filename: string, + mimeType: string + ): Promise { + const res = await fetch(url); + const blob = await res.blob(); + return new File([blob], filename, { type: mimeType }); + } + + /* + * clearContent() -> void + * + * @description + * Clears the content editor, title, and thumbnail preview. + */ + const clearContent = () => { + localStorage.removeItem("title"); + Cookies.remove("content"); + setTitle(""); + setContent(""); + if (editor) { + editor.commands.setContent(""); + } + setThumbnail(null); + setThumbnailPreview(null); + }; - /** + /* * handleThumbnailChange() -> void * * @description @@ -123,22 +198,80 @@ export default function ContentEditor({ isEditMode }: { isEditMode: boolean }) { } }; - /** - * Handles the submission of the content, including thumbnail upload and notifications. - * Uses POST for create and PUT for update mode. + /* + * validateForm() -> boolean + * + * @description + * Validates the form to ensure that the title and content are provided. + * If either is missing, it sets an error message and returns false. + * @return {boolean} - Returns true if the form is valid, false otherwise. */ - const handleSubmit = async () => { - setError(""); - + const validateForm = () => { if (!title || !content) { setError( "Title and content are required, and were not provided. Please try again." ); - return; + return false; } + return true; + }; + + /* + * submitContent() -> Promise + * + * @description + * Submits the content to the server. If in edit mode, it updates existing content. + * If not in edit mode, it creates new content. + * + * @param payload - The content data to submit + */ + const submitContent = async (data: Record) => { + if (isEditMode) { + await axios.put(`${apiURL}/content/${id}`, data, { + headers: { "Content-Type": "application/json" }, + withCredentials: true, + }); + } else { + await axios.post(`${apiURL}/content`, data, { + headers: { "Content-Type": "application/json" }, + withCredentials: true, + }); + } + }; + + /* + * handlePostSubmit() -> void + * + * @description + * Handles the post submission logic, clearing local storage and cookies, + * resetting the form state, and navigating to the content page. + */ + const handlePostSubmit = () => { + localStorage.removeItem("title"); + Cookies.remove("content"); + setTitle(""); + setContent(""); + + if (id) { + navigate(`/content/${id}`); + } else { + setError("Content ID not found after submission. Please try again."); + } + }; + + /* + * handleSubmit() -> void + * + * @description + * Handles the form submission, validates the form, uploads the thumbnail if provided, + * and submits the content to the server. If successful, it redirects to the content page. + */ + const handleSubmit = async () => { + setError(""); + + if (!validateForm()) return; // Prepare content payload - // eslint-disable-next-line @typescript-eslint/no-explicit-any const newContent: Record = { creatorUID: auth.user!.uid, title, @@ -146,70 +279,40 @@ export default function ContentEditor({ isEditMode }: { isEditMode: boolean }) { }; try { - // Handle thumbnail upload if present + // TODO: Move the thumbnail logic to the CONTENT SERVICE when developing the CONTENT SERVICE. if (thumbnail) { - const formData = new FormData(); - formData.append("thumbnail", thumbnail); - const thumbRes = await axios.post( - `${apiURL}/content/uploadThumbnail`, - formData, - { - headers: { "Content-Type": "multipart/form-data" }, - } + const thumbnailUploadResponse = await ImageService.uploadThumbnail( + thumbnail ); - newContent.thumbnailUrl = thumbRes.data.url; - } - // Determine request method and URL - if (isEditMode) { - await axios.put( - `${apiURL}/content/${id}`, - newContent, - { - headers: { "Content-Type": "application/json" }, - withCredentials: true, - } - ); - } else { - await axios.post( - `${apiURL}/content`, - { - ...newContent, - }, - { - headers: { "Content-Type": "application/json" }, - withCredentials: true, - } - ); + if (thumbnailUploadResponse instanceof Error) { + setError( + thumbnailUploadResponse.message || + "Failed to upload thumbnail. Please try again." + ); + return; + } + newContent.thumbnail = thumbnailUploadResponse.url; } + await submitContent(newContent); } catch (error) { - console.error("Error submitting content:", error); setError( "An error occurred while submitting the content. Please try again." ); + return; } // If submission was successful, redirect to the content page if (!error) { - // Reset local storage and cookies - localStorage.removeItem("title"); - Cookies.remove("content"); - setTitle(""); - setContent(""); - - if (id) { - navigate(`/content/${id}`); - } else { - setError("Content ID not found after submission. Please try again."); - } - } - - // User must be authenticated to create content - if (!auth.isAuthenticated) { - navigate("/authentication/login"); + handlePostSubmit(); } }; + // User must be authenticated to create content + if (!auth.isAuthenticated) { + navigate("/authentication/login"); + } + // -------------------------------------- // -------------- Render ---------------- // -------------------------------------- @@ -265,8 +368,6 @@ export default function ContentEditor({ isEditMode }: { isEditMode: boolean }) { Thumbnail Preview )} @@ -286,23 +387,7 @@ export default function ContentEditor({ isEditMode }: { isEditMode: boolean }) { {error &&

{error}

}
- diff --git a/frontend/src/services/AuthenticationService.ts b/frontend/src/services/AuthenticationService.ts index b64f420..cb55992 100644 --- a/frontend/src/services/AuthenticationService.ts +++ b/frontend/src/services/AuthenticationService.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { apiURL } from "../scripts/api"; -/** +/* * AuthenticationService class * * @description @@ -9,7 +9,7 @@ import { apiURL } from "../scripts/api"; * login, and OAuth authentication with Google and GitHub. */ export class AuthenticationService { - /** + /* * register() -> Promise<{ userUID: string; } | Error> * * @description @@ -56,7 +56,7 @@ export class AuthenticationService { } } - /** + /* * login() -> Promise<{ userUID: string; } | Error> * * @description @@ -94,7 +94,7 @@ export class AuthenticationService { } } - /** + /* * signInWithGoogle() -> Promise<{ token: string; userUID: string } | Error> * * @description @@ -109,7 +109,7 @@ export class AuthenticationService { return AuthenticationService.signInWithProvider("google", useRedirect); } - /** + /* * signInWithGithub() -> Promise<{ token: string; userUID: string } | Error> * * @description @@ -124,7 +124,7 @@ export class AuthenticationService { return AuthenticationService.signInWithProvider("github", useRedirect); } - /** + /* * signInWithProvider(provider: string, useRedirect: boolean) -> Promise<{ token: string; userUID: string } | Error> * * @description @@ -213,7 +213,7 @@ export class AuthenticationService { } } - /** + /* * handleCallbackResult(token: string) -> Promise<{ token: string; userUID: string } | Error> * * @description @@ -246,7 +246,7 @@ export class AuthenticationService { } } - /** + /* * logout() -> Promise * * @description @@ -280,7 +280,7 @@ export class AuthenticationService { } } - /** + /* * refreshToken() -> Promise<{ message: string; userUID: string } | Error> * * @description @@ -312,7 +312,7 @@ export class AuthenticationService { } } - /** + /* * resetPassword() -> Promise<{ message: string } | Error> * * @description diff --git a/frontend/src/services/CommentService.ts b/frontend/src/services/CommentService.ts index ad80cad..cf694bb 100644 --- a/frontend/src/services/CommentService.ts +++ b/frontend/src/services/CommentService.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { apiURL } from "../scripts/api"; import { Comment } from "../models/Comment"; -/** +/* * CommentService class * * @description @@ -10,7 +10,7 @@ import { Comment } from "../models/Comment"; * fetching, updating, and deleting comments. */ export default class CommentService { - /** + /* * publishComment() -> Promise<{ message: string; comment: Comment } | Error> * * Publishes a comment for a specific content ID. @@ -49,7 +49,7 @@ export default class CommentService { } } - /** + /* * getPostComments() -> Promise * * Fetches all comments for a specific content ID. @@ -78,7 +78,7 @@ export default class CommentService { } } - /** + /* * getComment() -> Promise * * Fetches a specific comment by its ID and the content ID it belongs to. @@ -111,7 +111,7 @@ export default class CommentService { } } - /** + /* * updateComment() -> Promise<{ message: string } | Error> * * Updates a comment by its ID and the content ID it belongs to. @@ -147,7 +147,7 @@ export default class CommentService { } } - /** + /* * deleteComment() -> Promise<{ message: string } | Error> * * Deletes a comment by its ID and the content ID it belongs to. diff --git a/frontend/src/services/FollowService.ts b/frontend/src/services/FollowService.ts index a2e0296..d8c2bb5 100644 --- a/frontend/src/services/FollowService.ts +++ b/frontend/src/services/FollowService.ts @@ -2,14 +2,14 @@ import axios from "axios"; import { apiURL } from "../scripts/api"; import { User } from "../models/User"; -/** +/* * FollowService class * * @description * This class provides methods to follow, unfollow, and manage follow requests between users. */ export default class FollowService { - /** + /* * followUser(userID: string, userToFollowID: string) -> Promise<{ message: string } | Error> * * @description @@ -44,7 +44,7 @@ export default class FollowService { } } - /** + /* * unfollowUser(userID: string, userToUnfollowID: string) -> Promise<{ message: string } | Error> * * @description @@ -78,7 +78,7 @@ export default class FollowService { } } - /** + /* * getFollowers(userID: string) -> Promise<{ followers: User[] } | Error> * * @description @@ -107,7 +107,7 @@ export default class FollowService { } } - /** + /* * getFollowing(userID: string) -> Promise<{ following: User[] } | Error> * * @description @@ -136,8 +136,7 @@ export default class FollowService { } } - // TODO: IMPLEMENT GET FOLLOW REQUESTS IN BACKEND - /** + /* * getFollowRequests(userID: string) -> Promise<{ requests: User[] } | Error> * * @description @@ -167,7 +166,7 @@ export default class FollowService { } } - /** + /* * approveFollowRequest(userID: string, requesterID: string) -> Promise<{ message: string } | Error> * * @description @@ -202,7 +201,7 @@ export default class FollowService { } } - /** + /* * rejectFollowRequest(userID: string, requesterID: string) -> Promise<{ message: string } | Error> * * @description diff --git a/frontend/src/services/ImageService.ts b/frontend/src/services/ImageService.ts new file mode 100644 index 0000000..aff5b63 --- /dev/null +++ b/frontend/src/services/ImageService.ts @@ -0,0 +1,152 @@ +import axios from "axios"; +import { apiURL } from "../scripts/api"; + +/* + * ImageService.ts + * This service handles image preparation for upload, including resizing and compressing images. + * It ensures that images meet the application's requirements for file type and size. + */ +export class ImageService { + /* + * uploadThumbnail() -> Promise<{ url: string } | Error> + * + * Uploads a thumbnail image after preparing it (resizing and compressing). + * + * @param thumbnail - The image file to be uploaded as a thumbnail. + * @returns A Promise that resolves to the URL of the uploaded thumbnail or an Error. + */ + public static async uploadThumbnail( + thumbnail: File + ): Promise<{ url: string } | Error> { + try { + const preparedFile = await ImageService.prepareImageForUpload(thumbnail); + const formData = new FormData(); + formData.append("thumbnail", preparedFile); + const response = await axios.post( + `${apiURL}/content/uploadThumbnail`, + formData, + { + headers: { "Content-Type": "multipart/form-data" }, + } + ); + + return response.data; + } catch (error) { + const message = + axios.isAxiosError(error) && error.response?.data?.error + ? error.response.data.error + : "Failed to upload thumbnail"; + + return new Error(message); + } + } + + /* + * Prepares an image file for upload by resizing and compressing it. + * + * @param file - The image file to be prepared. + * @returns A Promise that resolves to the prepared File object. + * @throws Error if the file type is unsupported or exceeds size limits. + */ + private static async prepareImageForUpload(file: File): Promise { + // Resizing max dimensions to 1280x1280 pixels - Quality set to 0.8 by default + const resizedFile = await ImageService.resizeImage(file, 1280, 1280); + + // Confirm the file type and size within the application limits + if (!ImageService.isValidFileType(resizedFile)) { + throw new Error("Unsupported file type."); + } + + // 5MB limit + if (!ImageService.isValidFileSize(resizedFile, 5 * 1024 * 1024)) { + throw new Error("File size exceeds limit."); + } + + return resizedFile; + } + + /* + * Resizes an image file to fit within specified maximum dimensions. + * + * @param file - The image file to be resized. + * @param maxWidth - The maximum width of the resized image. + * @param maxHeight - The maximum height of the resized image. + * @returns A Promise that resolves to the resized File object. + */ + private static resizeImage( + file: File, + maxWidth: number, + maxHeight: number, + quality: number = 0.8 + ): Promise { + return new Promise((resolve, reject) => { + const img = new Image(); + const url = URL.createObjectURL(file); + + img.onload = () => { + // 1 - Calculate new dimensions while maintaining aspect ratio + let { width, height } = img; + const aspectRatio = width / height; + + if (width > maxWidth || height > maxHeight) { + if (width / maxWidth > height / maxHeight) { + width = maxWidth; + height = Math.round(maxWidth / aspectRatio); + } else { + height = maxHeight; + width = Math.round(maxHeight * aspectRatio); + } + } + + // 2 - Create a canvas to draw the resized image + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + if (!ctx) return reject(new Error("Canvas context not available")); + ctx.drawImage(img, 0, 0, width, height); + + // 3 - Convert canvas to Blob and create a new File object + canvas.toBlob( + (blob) => { + if (blob) { + const resizedFile = new File([blob], file.name, { + type: file.type, + }); + resolve(resizedFile); + } else { + reject(new Error("Image resizing failed")); + } + }, + file.type, + quality + ); + }; + + img.onerror = () => reject(new Error("Image loading failed")); + img.src = url; + }); + } + + /* + * Validates the file type against allowed image types. + * + * @param file - The image file to validate. + * @returns True if the file type is valid, false otherwise. + */ + private static isValidFileType(file: File): boolean { + const allowedTypes = ["image/jpeg", "image/png", "image/webp"]; + return allowedTypes.includes(file.type); + } + + /* + * Validates the file size against the maximum allowed size. + * + * @param file - The image file to validate. + * @param maxSize - The maximum allowed file size in bytes. + * @returns True if the file size is within limits, false otherwise. + */ + private static isValidFileSize(file: File, maxSize: number): boolean { + return file.size <= maxSize; + } +} diff --git a/frontend/src/services/NotificationService.ts b/frontend/src/services/NotificationService.ts index d05eddb..2d13f87 100644 --- a/frontend/src/services/NotificationService.ts +++ b/frontend/src/services/NotificationService.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { apiURL } from "../scripts/api"; import { Notification } from "../models/Notification"; -/** +/* * NotificationService class * * @description @@ -10,7 +10,7 @@ import { Notification } from "../models/Notification"; * marking as read, and pushing new notifications. */ export default class NotificationService { - /** + /* * getNotifications() -> Promise * * @description @@ -44,7 +44,7 @@ export default class NotificationService { } } - /** + /* * getUnreadNotifications() -> Promise * * @description @@ -72,7 +72,7 @@ export default class NotificationService { } } - /** + /* * markAsRead() -> Promise * * @description diff --git a/frontend/src/services/SubscriptionService.ts b/frontend/src/services/SubscriptionService.ts index 9948201..1d9343c 100644 --- a/frontend/src/services/SubscriptionService.ts +++ b/frontend/src/services/SubscriptionService.ts @@ -3,7 +3,7 @@ import { apiURL } from "../scripts/api"; import { SubscriptionStatus } from "../models/SubscriptionStatus"; export class SubscriptionService { - /** + /* * Creates a subscription session for the user. * * @returns A promise that resolves to the response containing the session URL. @@ -33,7 +33,7 @@ export class SubscriptionService { } } - /** + /* * Cancels the user's subscription. * * @returns A promise that resolves to a response containing the cancellation message and end date. @@ -69,7 +69,7 @@ export class SubscriptionService { } } - /** + /* * Retrieves the subscription status for the user. * * @param forceRefresh - Whether to force a refresh of the subscription status. diff --git a/frontend/src/services/UserService.ts b/frontend/src/services/UserService.ts index cd6c4c7..c833d82 100644 --- a/frontend/src/services/UserService.ts +++ b/frontend/src/services/UserService.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { apiURL } from "../scripts/api"; import { User } from "../models/User"; -/** +/* * UserService class * * @description @@ -31,7 +31,7 @@ export default class UserService { } } - /** + /* * updateUserWithID(user: User) -> Promise<{ message: string } | Error> * * @description @@ -54,7 +54,7 @@ export default class UserService { } } - /** + /* * deleteUserWithID(id: string, email: string, password: string) -> Promise<{ message: string } | Error> * * @description @@ -88,7 +88,7 @@ export default class UserService { } } - /** + /* * uploadProfileImage(profileImage: File, oldProfileImage: string) -> Promise<{ url: string } | Error> * * @description @@ -125,7 +125,7 @@ export default class UserService { } } - /** + /* * changePassword(userId: string, oldPassword: string, newPassword: string, confirmPassword: string) -> Promise<{ message: string } | Error> * * @description @@ -173,7 +173,7 @@ export default class UserService { } } - /** + /* * validatePassword(oldPassword: string, newPassword: string, confirmPassword: string, requireOldPassword: boolean) -> Promise * * @description @@ -227,7 +227,7 @@ export default class UserService { return null; } - /** + /* * changeEmail(userId: string, newEmail: string, currentPassword: string) -> Promise<{ message: string } | Error> * * @description @@ -261,7 +261,7 @@ export default class UserService { } } - /** + /* * changeUsername(userId: string, newUsername: string) -> Promise<{ message: string } | Error> * * @description @@ -292,7 +292,7 @@ export default class UserService { } } - /** + /* * getRelatedContentCreators(userId: string) -> Promise * * @description diff --git a/frontend/src/styles/content/createContent.scss b/frontend/src/styles/content/createContent.scss index 0db2aed..a6510f3 100644 --- a/frontend/src/styles/content/createContent.scss +++ b/frontend/src/styles/content/createContent.scss @@ -95,10 +95,12 @@ } .thumbnail-preview { - width: 100%; - height: auto; + max-width: 500px; + max-height: 500px; border-radius: 10px; margin-bottom: -1rem; + // Fill image without deforming it + object-fit: cover; } .summary-container {