Make images visible in the app
This commit is contained in:
parent
a8898d2ad9
commit
d92d7939dd
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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 <img src={url} alt={alt} {...rest} />;
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [storagePath]);
|
||||
|
||||
return <img src={src} alt='' {...rest} />;
|
||||
};
|
||||
|
||||
export default StorageImage;
|
||||
|
|
|
@ -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;
|
Loading…
Reference in New Issue