diff --git a/src/app/store.js b/src/app/store.js
index d57bf82..348bddd 100644
--- a/src/app/store.js
+++ b/src/app/store.js
@@ -1,24 +1,22 @@
-import { configureStore } from '@reduxjs/toolkit'
-import userReducer from '../features/user/userSlice'
-import currentUserReducer from '../features/currentUser/currentUserSlice'
-import usersReducer from '../features/users/usersSlice'
-import postsReducer from '../features/posts/postsSlice'
-import incomingMessagesReducer from '../features/incomingMessages/incomingMessagesSlice'
-import outgoingMessagesReducer from '../features/outgoingMessages/outgoingMessagesSlice'
-import imagesReducer from '../features/images/imagesSlice'
-import linkReducer from '../features/link/linkSlice'
-import accountPageReducer from '../features/accountPage/accountPageSlice'
+import { configureStore } from "@reduxjs/toolkit";
+import userReducer from "../features/user/userSlice";
+import currentUserReducer from "../features/currentUser/currentUserSlice";
+import usersReducer from "../features/users/usersSlice";
+import postsReducer from "../features/posts/postsSlice";
+import incomingMessagesReducer from "../features/incomingMessages/incomingMessagesSlice";
+import outgoingMessagesReducer from "../features/outgoingMessages/outgoingMessagesSlice";
+import linkReducer from "../features/link/linkSlice";
+import accountPageReducer from "../features/accountPage/accountPageSlice";
export default configureStore({
- reducer: {
- user: userReducer,
- currentUser: currentUserReducer,
- users: usersReducer,
- posts: postsReducer,
- incomingMessages: incomingMessagesReducer,
- outgoingMessages: outgoingMessagesReducer,
- images: imagesReducer,
- link: linkReducer,
- accountPage: accountPageReducer,
- },
+ reducer: {
+ user: userReducer,
+ currentUser: currentUserReducer,
+ users: usersReducer,
+ posts: postsReducer,
+ incomingMessages: incomingMessagesReducer,
+ outgoingMessages: outgoingMessagesReducer,
+ link: linkReducer,
+ accountPage: accountPageReducer,
+ },
});
diff --git a/src/backend/backend.js b/src/backend/backend.js
index c0a469f..38db8b9 100644
--- a/src/backend/backend.js
+++ b/src/backend/backend.js
@@ -1,7 +1,9 @@
/* =====================================================================
+
Fakebook — back-end file (AUTH + mock social features)
+
===================================================================== */
import store from "../app/store";
@@ -38,6 +40,39 @@ const LS_TOKEN = "fakebook.jwt";
const LS_USER_ID = "fakebook.user_id";
+/* --------------------------- SESSION (NEW) ----------------------------- */
+
+/* In-memory copy – null while logged-out */
+
+let authToken = null;
+
+let authUser = null;
+
+/* Header helper – import this anywhere you need the bearer token */
+
+const authHeader = () =>
+ authToken ? { Authorization: `Bearer ${authToken}` } : {};
+
+/* Initialise / clear session */
+
+function setAuth(token, user_id) {
+ authToken = token;
+
+ authUser = user_id;
+
+ localStorage.setItem(LS_TOKEN, token);
+
+ localStorage.setItem(LS_USER_ID, user_id);
+}
+
+function clearAuth() {
+ authToken = authUser = null;
+
+ localStorage.removeItem(LS_TOKEN);
+
+ localStorage.removeItem(LS_USER_ID);
+}
+
/* --------------------------- Utilities -------------------------------- */
const genId = () => Math.random().toString(36).slice(2, 11);
@@ -45,8 +80,17 @@ const genId = () => Math.random().toString(36).slice(2, 11);
const delay = (ms = 200) => new Promise((r) => setTimeout(r, ms));
async function $fetch(url, opts = {}) {
+ /* inject bearer automatically */
+
const res = await fetch(url, {
- headers: { "Content-Type": "application/json", ...(opts.headers || {}) },
+ headers: {
+ "Content-Type": "application/json",
+
+ ...authHeader(),
+
+ ...(opts.headers || {}),
+ },
+
...opts,
});
@@ -57,9 +101,33 @@ async function $fetch(url, opts = {}) {
return data;
}
-/* ---------------------- REST → UI mapper (moved up) ------------------ */
+/* ---------------------- REST → UI mapper ------------------------------- */
+
+function addPath(u, fileName) {
+ if (typeof fileName !== "string" || !fileName.length) return fileName;
+
+ if (fileName.includes("/")) return fileName; /* already has folder */
+
+ return `${u.user_id}/${fileName}`; /* prepend owner id */
+}
function mapRestUser(u) {
+ const photosRaw = JSON.parse(u.photos || "[]");
+
+ const photos = photosRaw.map((item) => {
+ if (typeof item === "string") {
+ /* legacy: array of strings */
+
+ return { filename: addPath(u, item) };
+ }
+
+ if (item && typeof item.filename === "string") {
+ return { ...item, filename: addPath(u, item.filename) };
+ }
+
+ return item;
+ });
+
return {
userID: u.user_id,
@@ -67,11 +135,13 @@ function mapRestUser(u) {
lastname: u.lastname,
+ /* already stored as folder/filename → leave untouched */
+
profilePictureURL: u.profilePictureURL,
backgroundPictureURL: u.backgroundPictureURL,
- photos: JSON.parse(u.photos || "[]"),
+ photos,
posts: JSON.parse(u.posts || "[]"),
@@ -85,7 +155,9 @@ function mapRestUser(u) {
/* ===================================================================== *
- SECTION A — REAL AUTH WORKFLOW (FIXED) *
+
+ SECTION A — REAL AUTH WORKFLOW
+
===================================================================== */
@@ -100,6 +172,8 @@ export function subscribeAuth() {
const user_id = localStorage.getItem(LS_USER_ID);
if (!token || !user_id) {
+ clearAuth(); /* make sure RAM copy is empty */
+
store.dispatch(signOut());
store.dispatch(loadingFinished());
@@ -107,16 +181,16 @@ export function subscribeAuth() {
return;
}
+ /* restore session into RAM */
+
+ setAuth(token, user_id);
+
try {
- /* The users endpoint does NOT understand ?user_id=.
-
- Workaround: fetch all users (limit=-1) then find ours. */
+ /* users endpoint lacks ?user_id, so fetch all */
- const users = await $fetch(`${USERS_URL}?limit=-1`, {
- headers: { Authorization: `Bearer ${token}` },
- });
+ const users = await $fetch(`${USERS_URL}?limit=-1`);
- const u = users.find((u) => u.user_id === user_id);
+ const u = users.find((x) => x.user_id === user_id);
if (!u) throw new Error("User not found");
@@ -130,13 +204,9 @@ export function subscribeAuth() {
})
);
} catch (err) {
- /* Token invalid / expired / user not found */
-
console.warn("[Auth] subscribeAuth failed:", err.message);
- localStorage.removeItem(LS_TOKEN);
-
- localStorage.removeItem(LS_USER_ID);
+ clearAuth();
store.dispatch(signOut());
} finally {
@@ -167,7 +237,7 @@ export async function createUserAccount(user) {
}),
});
- store.dispatch(errorOccured("")); // clear previous error
+ store.dispatch(errorOccured(""));
} catch (err) {
store.dispatch(errorOccured(err.message));
} finally {
@@ -175,41 +245,32 @@ export async function createUserAccount(user) {
}
}
-/* ---------------------- signInUser (FIXED) -------------------------- */
+/* ---------------------- signInUser ---------------------------------- */
export async function signInUser(user) {
store.dispatch(loadingStarted());
try {
- /* 1. Login with email + password (as API expects) */
-
const url = `${LOGIN_URL}?email=${encodeURIComponent(
user.email
)}&password=${encodeURIComponent(user.password)}`;
const { token, user_id } = await $fetch(url);
- /* 2. Persist session */
+ /* persist + put into RAM */
- localStorage.setItem(LS_TOKEN, token);
+ setAuth(token, user_id);
- localStorage.setItem(LS_USER_ID, user_id);
+ /* get full profile */
- /* 3. Fetch FULL user list, find our profile */
-
- const users = await $fetch(`${USERS_URL}?limit=-1`, {
- headers: { Authorization: `Bearer ${token}` },
- });
+ const users = await $fetch(`${USERS_URL}?limit=-1`);
const profile = users.find((u) => u.user_id === user_id);
if (!profile) throw new Error("User not found");
- if (!profile.isEmailVerified) {
+ if (!profile.isEmailVerified)
throw new Error("Please verify your email before to continue");
- }
-
- /* 4. Dispatch sign-in */
store.dispatch(
signIn({
@@ -225,9 +286,7 @@ export async function signInUser(user) {
} catch (err) {
store.dispatch(errorOccured(err.message));
- localStorage.removeItem(LS_TOKEN);
-
- localStorage.removeItem(LS_USER_ID);
+ clearAuth();
} finally {
store.dispatch(loadingFinished());
}
@@ -236,13 +295,9 @@ export async function signInUser(user) {
/* ---------------------- signUserOut ---------------------------------- */
export async function signUserOut() {
- /* mark offline in DB */
- await patchOnline(false);
+ await patchOnline(false); /* mark offline */
- /* clear session */
- localStorage.removeItem(LS_TOKEN);
-
- localStorage.removeItem(LS_USER_ID);
+ clearAuth();
store.dispatch(signOut());
}
diff --git a/src/components/StorageImage.jsx b/src/components/StorageImage.jsx
index 0e4d270..19c2664 100644
--- a/src/components/StorageImage.jsx
+++ b/src/components/StorageImage.jsx
@@ -1,102 +1,68 @@
-import React, { useState } from "react";
-import { getImageURL } from "../backend/backend";
-import { useSelector, useDispatch } from "react-redux";
+import React, { useEffect, useState } from "react";
+
import placeholderImage from "../images/placeholder-image.jpg";
+
import fakebookAvatar from "../images/fakebook-avatar.jpeg";
+
import backgroundServer from "../images/background-server.jpg";
-import { imageAdded, imageUrlFound } from "../features/images/imagesSlice";
-import { useEffect } from "react";
-const StorageImage = (props) => {
- const { storagePath, alt, ...rest } = props;
+const API_BASE = "https://alexerdei-team.us.ainiro.io/magic/modules/fakebook";
- const PLACEHOLDER_AVATAR_STORAGE_PATH = "fakebook-avatar.jpeg";
- const PLACEHOLDER_BACKGROUND_STORAGE_PATH = "background-server.jpg";
+/* ------------------------------------------------------------------ */
- //We use the images slice as a buffer. Fetching the actual url of the image
- //in the storage takes relatively long time and uses Firebase. We render the same
- //image in the app several times. Our goal to fetch the url only once for each image
- //to save resources.
- const images = useSelector((state) => state.images);
- const dispatch = useDispatch();
+function toWebp(name) {
+ if (name.toLowerCase().endsWith(".webp")) return name;
- const [url, setUrl] = useState(placeholderImage);
+ const dot = name.lastIndexOf(".");
- function changeStoragePath(storagePath) {
- const words = storagePath.split(".");
- words[words.length - 2] += "_400x400";
- return words.join(".");
- }
+ return dot === -1 ? `${name}.webp` : `${name.slice(0, dot)}.webp`;
+}
+
+async function fetchImageURL(storagePath) {
+ if (!storagePath) return placeholderImage;
+
+ if (storagePath === "fakebook-avatar.jpeg") return fakebookAvatar;
+
+ if (storagePath === "background-server.jpg") return backgroundServer;
+
+ const [folder, ...rest] = storagePath.split("/");
+
+ const rawFilename = rest.join("/");
+
+ const filenameWebp = toWebp(rawFilename); // ← only change
+
+ const url =
+ `${API_BASE}/image?folder=fakebook/${encodeURIComponent(folder)}` +
+ `&filename=${encodeURIComponent(filenameWebp)}`;
+
+ const res = await fetch(url);
+
+ if (!res.ok) throw new Error("Image request failed");
+
+ const blob = await res.blob();
+
+ return URL.createObjectURL(blob);
+}
+
+/* ================================================================== */
+
+const StorageImage = ({ storagePath, ...rest }) => {
+ const [src, setSrc] = useState(placeholderImage);
useEffect(() => {
- let shouldUpdate = true;
- const cleanup = () => (shouldUpdate = false);
+ let cancelled = false;
- //We filter out placeholder pictures
- if (storagePath === PLACEHOLDER_AVATAR_STORAGE_PATH) {
- setUrl(fakebookAvatar);
- return cleanup;
- }
- if (storagePath === PLACEHOLDER_BACKGROUND_STORAGE_PATH) {
- setUrl(backgroundServer);
- return cleanup;
- }
+ fetchImageURL(storagePath)
+ .then((url) => !cancelled && setSrc(url))
- //We look for the url in images slice first
- let imageIndex = images
- .map((image) => image.storagePath)
- .indexOf(storagePath);
- if (imageIndex === -1) {
- imageIndex = images
- .map((image) => image.storagePath)
- .indexOf(changeStoragePath(storagePath));
- //If we are unable to find it anyway we add the image to the slice
- if (imageIndex === -1) {
- dispatch(
- imageAdded({
- storagePath,
- url,
- })
- );
- }
- //We add the url for the image to the images slice when we have actually got it
- //We also update the local state to show the right image
- getImageURL(storagePath)
- .then((url) => {
- setUrl(url);
- dispatch(
- imageUrlFound({
- storagePath,
- url,
- })
- );
- })
- .catch((_error) => {
- getImageURL(changeStoragePath(storagePath)).then((url) => {
- setUrl(url);
- dispatch(
- imageUrlFound({
- storagePath,
- url,
- })
- );
- });
- });
- } else {
- //If we are able to find the url in the images slice we just use it instead of fetching
- const newUrl = images[imageIndex].url;
- //We only update the state if it contains different value and we should update because
- //the component has not been unmounted by the time the promise resolves
- if (newUrl !== url && shouldUpdate) setUrl(newUrl);
- }
- //We return a cleanup function which runs when the component unmounts. We set
- //the shouldUpdate to false, so after this the state cannot be updated. If
- //we don't do this React gives us error messages about state update on our
- //unmounted component
- return cleanup;
- }, [images, storagePath, url, dispatch]);
+ .catch(() => !cancelled && setSrc(placeholderImage));
- return
;
+ return () => {
+ cancelled = true;
+ };
+ }, [storagePath]);
+
+ return
;
};
export default StorageImage;
diff --git a/src/features/images/imagesSlice.js b/src/features/images/imagesSlice.js
deleted file mode 100644
index a505ae5..0000000
--- a/src/features/images/imagesSlice.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { createSlice } from "@reduxjs/toolkit";
-
-export const imagesSlice = createSlice({
- name: "images",
- initialState: [],
- reducers: {
- imageAdded: (state, action) => {
- const index = state
- .map((image) => image.storagePath)
- .indexOf(action.payload.storagePath);
- if (index === -1) state.push(action.payload);
- },
- imageUrlFound: (state, action) => {
- const index = state
- .map((image) => image.storagePath)
- .indexOf(action.payload.storagePath);
- if (index > -1) state[index].url = action.payload.url;
- },
- },
-});
-
-export const { imageAdded, imageUrlFound } = imagesSlice.actions;
-
-export default imagesSlice.reducer;