mirror of
https://github.com/bubblecup-12/VogelSocialMedia.git
synced 2025-07-06 15:18:48 +00:00
Feed and user Feed done
This commit is contained in:
parent
5f9e3a24f7
commit
133cec2fb4
29 changed files with 331 additions and 10621 deletions
|
@ -10,7 +10,9 @@
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@mui/icons-material": "^7.1.2",
|
"@mui/icons-material": "^7.1.2",
|
||||||
|
"@mui/joy": "^5.0.0-beta.52",
|
||||||
"@mui/material": "^7.1.1",
|
"@mui/material": "^7.1.1",
|
||||||
|
"@mui/system": "^7.1.1",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.1.0",
|
"@testing-library/react": "^16.1.0",
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
import React, { use } from 'react';
|
|
||||||
import logo from './logo.svg';
|
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import LoginAndSignUpPage from './pages/LoginAndSignUpPage';
|
import LoginAndSignUpPage from './pages/LoginAndSignUpPage';
|
||||||
import PostCreation from './pages/PostCreation';
|
import PostCreation from './pages/PostCreation';
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
@ -9,13 +6,14 @@ import "./styles/colors.css";
|
||||||
import "./styles/fonts.css";
|
import "./styles/fonts.css";
|
||||||
import "./styles/sizes.css";
|
import "./styles/sizes.css";
|
||||||
import Footer from "./components/footer/Footer";
|
import Footer from "./components/footer/Footer";
|
||||||
import Feed, {UserFeedRoute} from "./components/feed/Feed";
|
import Feed, {UserFeedRoute} from "./pages/feed/Feed";
|
||||||
import Header from './components/header/Header';
|
import Header from './components/header/Header';
|
||||||
import Profile from "./pages/Profile";
|
import Profile from "./pages/Profile";
|
||||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom";
|
||||||
import { Auth } from "./api/Auth";
|
import { Auth } from "./api/Auth";
|
||||||
import { NotFound } from "./pages/404Page/NotFoundPage";
|
import { NotFound } from "./pages/404Page/NotFoundPage";
|
||||||
import ScrollToAnchor from "./components/ScrollToAnchor";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
@ -23,7 +21,6 @@ function App() {
|
||||||
<Auth>
|
<Auth>
|
||||||
<Router>
|
<Router>
|
||||||
<Header />
|
<Header />
|
||||||
<ScrollToAnchor />
|
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
|
@ -39,6 +36,13 @@ function App() {
|
||||||
<Route path="/createpost" element={<PostCreation />}></Route>
|
<Route path="/createpost" element={<PostCreation />}></Route>
|
||||||
<Route path="/profile" element={<Profile />}></Route>
|
<Route path="/profile" element={<Profile />}></Route>
|
||||||
<Route path="/feed/:user" element={<UserFeedRoute />}></Route>
|
<Route path="/feed/:user" element={<UserFeedRoute />}></Route>
|
||||||
|
<Route path="/" element={<Navigate to="/feed" replace />} />
|
||||||
|
<Route path="/feed" element={<Feed />} />
|
||||||
|
<Route path="/feed/:user" element={<UserFeedRoute />} />
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
|
<Route path="/login" element={<LoginAndSignUpPage signupProp={false} />} />
|
||||||
|
<Route path="/register" element={<LoginAndSignUpPage signupProp={true} />} />
|
||||||
|
<Route path="/profile/:username" element={<Profile />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,129 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import { styled } from '@mui/material/styles';
|
|
||||||
import Card from '@mui/material/Card';
|
|
||||||
import CardHeader from '@mui/material/CardHeader';
|
|
||||||
import CardMedia from '@mui/material/CardMedia';
|
|
||||||
import CardContent from '@mui/material/CardContent';
|
|
||||||
import CardActions from '@mui/material/CardActions';
|
|
||||||
import Collapse from '@mui/material/Collapse';
|
|
||||||
import Avatar from '@mui/material/Avatar';
|
|
||||||
import IconButton, { IconButtonProps } from '@mui/material/IconButton';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import { red } from '@mui/material/colors';
|
|
||||||
import FavoriteIcon from '@mui/icons-material/Favorite';
|
|
||||||
import ShareIcon from '@mui/icons-material/Share';
|
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
||||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
|
||||||
|
|
||||||
interface ExpandMoreProps extends IconButtonProps {
|
|
||||||
expand: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExpandMore = styled((props: ExpandMoreProps) => {
|
|
||||||
const { expand, ...other } = props;
|
|
||||||
return <IconButton {...other} />;
|
|
||||||
})(({ theme }) => ({
|
|
||||||
marginLeft: 'auto',
|
|
||||||
transition: theme.transitions.create('transform', {
|
|
||||||
duration: theme.transitions.duration.shortest,
|
|
||||||
}),
|
|
||||||
variants: [
|
|
||||||
{
|
|
||||||
props: ({ expand }) => !expand,
|
|
||||||
style: {
|
|
||||||
transform: 'rotate(0deg)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
props: ({ expand }) => !!expand,
|
|
||||||
style: {
|
|
||||||
transform: 'rotate(180deg)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function Post() {
|
|
||||||
const [expanded, setExpanded] = React.useState(false);
|
|
||||||
|
|
||||||
const handleExpandClick = () => {
|
|
||||||
setExpanded(!expanded);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card sx={{ maxWidth: 345 }}>
|
|
||||||
<CardHeader
|
|
||||||
avatar={
|
|
||||||
<Avatar sx={{ bgcolor: red[500] }} aria-label="recipe">
|
|
||||||
R
|
|
||||||
</Avatar>
|
|
||||||
}
|
|
||||||
action={
|
|
||||||
<IconButton aria-label="settings">
|
|
||||||
<MoreVertIcon />
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
title="Shrimp and Chorizo Paella"
|
|
||||||
subheader="September 14, 2016"
|
|
||||||
/>
|
|
||||||
<CardMedia
|
|
||||||
component="img"
|
|
||||||
height="194"
|
|
||||||
image="/static/images/cards/paella.jpg"
|
|
||||||
alt="Paella dish"
|
|
||||||
/>
|
|
||||||
<CardContent>
|
|
||||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
|
||||||
This impressive paella is a perfect party dish and a fun meal to cook
|
|
||||||
together with your guests. Add 1 cup of frozen peas along with the mussels,
|
|
||||||
if you like.
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
|
||||||
<CardActions disableSpacing>
|
|
||||||
<IconButton aria-label="add to favorites">
|
|
||||||
<FavoriteIcon />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton aria-label="share">
|
|
||||||
<ShareIcon />
|
|
||||||
</IconButton>
|
|
||||||
<ExpandMore
|
|
||||||
expand={expanded}
|
|
||||||
onClick={handleExpandClick}
|
|
||||||
aria-expanded={expanded}
|
|
||||||
aria-label="show more"
|
|
||||||
>
|
|
||||||
<ExpandMoreIcon />
|
|
||||||
</ExpandMore>
|
|
||||||
</CardActions>
|
|
||||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
|
||||||
<CardContent>
|
|
||||||
<Typography sx={{ marginBottom: 2 }}>Method:</Typography>
|
|
||||||
<Typography sx={{ marginBottom: 2 }}>
|
|
||||||
Heat 1/2 cup of the broth in a pot until simmering, add saffron and set
|
|
||||||
aside for 10 minutes.
|
|
||||||
</Typography>
|
|
||||||
<Typography sx={{ marginBottom: 2 }}>
|
|
||||||
Heat oil in a (14- to 16-inch) paella pan or a large, deep skillet over
|
|
||||||
medium-high heat. Add chicken, shrimp and chorizo, and cook, stirring
|
|
||||||
occasionally until lightly browned, 6 to 8 minutes. Transfer shrimp to a
|
|
||||||
large plate and set aside, leaving chicken and chorizo in the pan. Add
|
|
||||||
pimentón, bay leaves, garlic, tomatoes, onion, salt and pepper, and cook,
|
|
||||||
stirring often until thickened and fragrant, about 10 minutes. Add
|
|
||||||
saffron broth and remaining 4 1/2 cups chicken broth; bring to a boil.
|
|
||||||
</Typography>
|
|
||||||
<Typography sx={{ marginBottom: 2 }}>
|
|
||||||
Add rice and stir very gently to distribute. Top with artichokes and
|
|
||||||
peppers, and cook without stirring, until most of the liquid is absorbed,
|
|
||||||
15 to 18 minutes. Reduce heat to medium-low, add reserved shrimp and
|
|
||||||
mussels, tucking them down into the rice, and cook again without
|
|
||||||
stirring, until mussels have opened and rice is just tender, 5 to 7
|
|
||||||
minutes more. (Discard any mussels that don't open.)
|
|
||||||
</Typography>
|
|
||||||
<Typography>
|
|
||||||
Set aside off of the heat to let rest for 10 minutes, and then serve.
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</Collapse>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { useEffect, useRef } from 'react';
|
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
// use like this: onClick={() => navigate(`/feed/${username}#${post.id}`)}
|
|
||||||
|
|
||||||
function ScrollToAnchor() {
|
|
||||||
const location = useLocation();
|
|
||||||
const lastHash = useRef('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (location.hash) {
|
|
||||||
lastHash.current = location.hash.slice(1);
|
|
||||||
}
|
|
||||||
if (lastHash.current && document.getElementById(lastHash.current)) {
|
|
||||||
setTimeout(() => {
|
|
||||||
document
|
|
||||||
.getElementById(lastHash.current)
|
|
||||||
?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
lastHash.current = '';
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}, [location]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ScrollToAnchor;
|
|
|
@ -1,29 +1,45 @@
|
||||||
import { Avatar, Box, Typography } from "@mui/material";
|
import { Avatar, Box, Typography } from "@mui/material";
|
||||||
import api from "../api/axios";
|
import api from "../api/axios";
|
||||||
import { useState,useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
interface UserAvatarProps {
|
interface UserAvatarProps {
|
||||||
username: string|null;
|
username: string | null;
|
||||||
src?: string;
|
src?: string;
|
||||||
size?: number | string;
|
size?: number | string;
|
||||||
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UserAvatar({ username, size = 40 }: UserAvatarProps) {
|
export default function UserAvatar({
|
||||||
const[pb, setPb] = useState<string>();
|
username,
|
||||||
|
size = 40,
|
||||||
|
onClick,
|
||||||
|
}: UserAvatarProps) {
|
||||||
|
const [pb, setPb] = useState<string>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get(`/profile/getProfilePicture/${username}`)
|
const res = await api.get(`/profile/getProfilePicture/${username}`);
|
||||||
setPb((res.data as { url: string }).url);
|
setPb((res.data as { url: string }).url);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
|
if (error.status !== 404) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
}, [username]);
|
}, [username]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, maxWidth: "600px"}}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
maxWidth: "600px",
|
||||||
|
cursor: onClick ? "pointer" : "default",
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={pb}
|
src={pb}
|
||||||
alt={username || "avatar"}
|
alt={username || "avatar"}
|
||||||
|
@ -39,7 +55,11 @@ export default function UserAvatar({ username, size = 40 }: UserAvatarProps) {
|
||||||
<Typography
|
<Typography
|
||||||
component="span"
|
component="span"
|
||||||
fontWeight={500}
|
fontWeight={500}
|
||||||
sx={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis"}}
|
sx={{
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{username}
|
{username}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
15
code/frontend/src/components/buttons/LogInButton.tsx
Normal file
15
code/frontend/src/components/buttons/LogInButton.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import ButtonRotkehlchen from "./buttonRotkehlchen/ButtonRotkehlchen";
|
||||||
|
|
||||||
|
export default function LogInButton() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonRotkehlchen
|
||||||
|
style={"primary"}
|
||||||
|
label={"Login"}
|
||||||
|
type={"button"}
|
||||||
|
onClick={() => navigate("/login")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
15
code/frontend/src/components/buttons/SignUpButton.tsx
Normal file
15
code/frontend/src/components/buttons/SignUpButton.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import ButtonRotkehlchen from "./buttonRotkehlchen/ButtonRotkehlchen";
|
||||||
|
|
||||||
|
export default function LogInButton() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonRotkehlchen
|
||||||
|
style={"secondary"}
|
||||||
|
label={"Sign Up"}
|
||||||
|
type={"button"}
|
||||||
|
onClick={() => navigate("/login")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,16 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import "./welcomeMessage.css";
|
|
||||||
|
|
||||||
export default function WelcomeMessage() {
|
|
||||||
return (
|
|
||||||
<div className="welcome-message">
|
|
||||||
<h1 className="welcome-title">Welcome!</h1>
|
|
||||||
<p className="welcome-text">
|
|
||||||
Become a part of our big, bird loving community!
|
|
||||||
</p>
|
|
||||||
<p className="desktop-welcome-text">
|
|
||||||
Exchange pictures and information about birds and be part of our big bird loving community! Pellentesque vulputate a enim ac feugiat. Donec dictum magna sit amet arcu commodo, quis vehicula nunc commodo. Pellentesque varius congue varius.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
26
code/frontend/src/components/naggingFooter/NaggingFooter.tsx
Normal file
26
code/frontend/src/components/naggingFooter/NaggingFooter.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import "./naggingFooter.css";
|
||||||
|
import LogInButton from "../buttons/LogInButton";
|
||||||
|
import SignUpButton from "../buttons/SignUpButton";
|
||||||
|
|
||||||
|
export default function NaggingFooter() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<div className="footer-container">
|
||||||
|
<img
|
||||||
|
className="header-icon header-icon-feather"
|
||||||
|
src="/assets/icons/BirdIconO.ico"
|
||||||
|
alt="featherIcon"
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
/>
|
||||||
|
<p className="header-title" onClick={() => navigate("/")}>
|
||||||
|
Feather Feed
|
||||||
|
</p>
|
||||||
|
<div className="footer-spacer" />
|
||||||
|
<div className="signup-button">
|
||||||
|
<SignUpButton />
|
||||||
|
</div>
|
||||||
|
<LogInButton />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
61
code/frontend/src/components/naggingFooter/naggingFooter.css
Normal file
61
code/frontend/src/components/naggingFooter/naggingFooter.css
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
.footer-container {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100vw;
|
||||||
|
min-height: 32px;
|
||||||
|
max-height: 15vh;
|
||||||
|
background-color: var(--Rotkehlchen-gray);
|
||||||
|
z-index: 2000;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.footer-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.header-title {
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 0.3rem;
|
||||||
|
margin-right: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-spacer {
|
||||||
|
flex: 1 1 2rem;
|
||||||
|
min-width: 0.5rem;
|
||||||
|
max-width: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-button,
|
||||||
|
.footer-container button,
|
||||||
|
.header-icon-feather {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-button {
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-right: 0.2rem;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-container button {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 8px;
|
||||||
|
margin-right: 6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon-feather {
|
||||||
|
width: 30px;
|
||||||
|
margin: 0px !important;
|
||||||
|
margin-left: 6px !important;
|
||||||
|
}
|
|
@ -6,25 +6,23 @@ import CardMedia from "@mui/material/CardMedia";
|
||||||
import CardContent from "@mui/material/CardContent";
|
import CardContent from "@mui/material/CardContent";
|
||||||
import CardActions from "@mui/material/CardActions";
|
import CardActions from "@mui/material/CardActions";
|
||||||
import Collapse from "@mui/material/Collapse";
|
import Collapse from "@mui/material/Collapse";
|
||||||
import Avatar from "@mui/material/Avatar";
|
|
||||||
import IconButton, { IconButtonProps } from "@mui/material/IconButton";
|
import IconButton, { IconButtonProps } from "@mui/material/IconButton";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { red } from "@mui/material/colors";
|
|
||||||
import FavoriteIcon from "@mui/icons-material/Favorite";
|
import FavoriteIcon from "@mui/icons-material/Favorite";
|
||||||
import ShareIcon from "@mui/icons-material/Share";
|
|
||||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
import api from "../../api/axios";
|
||||||
import api from "../api/axios";
|
|
||||||
import { Url } from "url";
|
import { Url } from "url";
|
||||||
import { LogLevel } from "vite";
|
|
||||||
import "./post.css";
|
import "./post.css";
|
||||||
import UserAvatar from "./UserAvatar";
|
import UserAvatar from "../UserAvatar";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useEffect, useRef, useCallback } from "react";
|
||||||
|
|
||||||
interface ExpandMoreProps extends IconButtonProps {
|
interface ExpandMoreProps extends IconButtonProps {
|
||||||
expand: boolean;
|
expand: boolean;
|
||||||
}
|
}
|
||||||
interface PostProps {
|
interface PostProps {
|
||||||
postId: string;
|
postId: string;
|
||||||
|
autoScroll?: boolean;
|
||||||
}
|
}
|
||||||
interface PostResponse {
|
interface PostResponse {
|
||||||
description: string;
|
description: string;
|
||||||
|
@ -58,17 +56,17 @@ const ExpandMore = styled((props: ExpandMoreProps) => {
|
||||||
transform: expand ? "rotate(180deg)" : "rotate(0deg)",
|
transform: expand ? "rotate(180deg)" : "rotate(0deg)",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default function Post({ postId }: PostProps) {
|
export default function Post({ postId, autoScroll }: PostProps) {
|
||||||
const [expanded, setExpanded] = React.useState(false);
|
const [expanded, setExpanded] = React.useState(false);
|
||||||
const [post, setPost] = React.useState<PostResponse | null>(null);
|
const [post, setPost] = React.useState<PostResponse | null>(null);
|
||||||
const [currentImage, setCurrentImage] = React.useState(0);
|
const [currentImage, setCurrentImage] = React.useState(0);
|
||||||
const [like, setLike] = React.useState(false);
|
const [like, setLike] = React.useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const postRef = useRef<HTMLDivElement>(null);
|
||||||
|
const scrollTimeoutsRef = useRef<number[]>([]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
// Using useCallback to memoize the function and avoid dependency issues
|
||||||
getPostbyID();
|
const getPostbyID = useCallback(async (): Promise<void> => {
|
||||||
}, [postId]);
|
|
||||||
|
|
||||||
async function getPostbyID(): Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
const response = await api.get<PostResponse>(
|
const response = await api.get<PostResponse>(
|
||||||
`/posts/getPost/{postId}?postId=${postId}`
|
`/posts/getPost/{postId}?postId=${postId}`
|
||||||
|
@ -79,7 +77,43 @@ export default function Post({ postId }: PostProps) {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch post:", error);
|
console.error("Failed to fetch post:", error);
|
||||||
}
|
}
|
||||||
|
}, [postId]);
|
||||||
|
|
||||||
|
// Auto-scroll effect with cleanup for timeouts
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoScroll && post) {
|
||||||
|
console.log("Preparing to scroll to post:", postId);
|
||||||
|
|
||||||
|
// Clear any existing timeouts first
|
||||||
|
scrollTimeoutsRef.current.forEach(clearTimeout);
|
||||||
|
scrollTimeoutsRef.current = [];
|
||||||
|
|
||||||
|
// Using forEach instead of map since we don't need to return values
|
||||||
|
[50, 100, 300, 500, 1000, 1500].forEach((delay) => {
|
||||||
|
const timeoutId = window.setTimeout(() => {
|
||||||
|
if (postRef.current) {
|
||||||
|
console.log(`Scrolling attempt at ${delay}ms`);
|
||||||
|
postRef.current.scrollIntoView({
|
||||||
|
behavior: "auto",
|
||||||
|
block: "start",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}, delay);
|
||||||
|
scrollTimeoutsRef.current.push(timeoutId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up timeouts when component unmounts or dependencies change
|
||||||
|
return () => {
|
||||||
|
scrollTimeoutsRef.current.forEach(clearTimeout);
|
||||||
|
scrollTimeoutsRef.current = [];
|
||||||
|
};
|
||||||
|
}, [autoScroll, post, postId]);
|
||||||
|
|
||||||
|
// Fetch post data when postId changes
|
||||||
|
useEffect(() => {
|
||||||
|
getPostbyID();
|
||||||
|
}, [getPostbyID]); // Now getPostbyID is a dependency
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return (
|
return (
|
||||||
|
@ -122,9 +156,19 @@ export default function Post({ postId }: PostProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledEngineProvider injectFirst>
|
<StyledEngineProvider injectFirst>
|
||||||
<Card className="body-l" sx={{ maxWidth: 600, width: "100%", margin: 2 }}>
|
<Card
|
||||||
|
ref={postRef}
|
||||||
|
className="body-l"
|
||||||
|
sx={{ maxWidth: 600, width: "100%", margin: 2 }}
|
||||||
|
>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
avatar={<UserAvatar username={post.user.name} size={60} />}
|
avatar={
|
||||||
|
<UserAvatar
|
||||||
|
username={post.user.name}
|
||||||
|
size={60}
|
||||||
|
onClick={() => navigate(`/profile/${post.user.name}`)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{images.length > 0 && (
|
{images.length > 0 && (
|
||||||
<div className="post-image-carousel">
|
<div className="post-image-carousel">
|
||||||
|
@ -187,20 +231,16 @@ export default function Post({ postId }: PostProps) {
|
||||||
</CardActions>
|
</CardActions>
|
||||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
{post.tags && post.tags.length > 0 && (
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||||
Tags: {post.tags.join(", ")}
|
Tags: {post.tags.join(", ")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
)}
|
||||||
Following: {post.following ? "Ja" : "Nein"}
|
<Typography variant="body2" color="text.secondary">
|
||||||
</Typography>
|
Created at: {new Date(post.createdAt).toLocaleString()}
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
|
||||||
Status: {post.status}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
Erstellt am: {new Date(post.createdAt).toLocaleString()}
|
Last updated: {new Date(post.updatedAt).toLocaleString()}
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
Zuletzt aktualisiert: {new Date(post.updatedAt).toLocaleString()}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Collapse>
|
</Collapse>
|
|
@ -5,7 +5,7 @@ import { useState } from "react";
|
||||||
import "./bio.css";
|
import "./bio.css";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import EditSquareIcon from "@mui/icons-material/EditSquare";
|
import EditSquareIcon from "@mui/icons-material/EditSquare";
|
||||||
import ButtonPrimary from "../ButtonRotkehlchen";
|
import ButtonPrimary from "../buttons/buttonRotkehlchen/ButtonRotkehlchen";
|
||||||
import api from "../../api/axios";
|
import api from "../../api/axios";
|
||||||
|
|
||||||
export default function BioTextField({ ownAccount, bioText, setBio } : { ownAccount: boolean, bioText: string | undefined, setBio: (bio: string) => void }) {
|
export default function BioTextField({ ownAccount, bioText, setBio } : { ownAccount: boolean, bioText: string | undefined, setBio: (bio: string) => void }) {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import EditSquareIcon from "@mui/icons-material/EditSquare";
|
import EditSquareIcon from "@mui/icons-material/EditSquare";
|
||||||
import "./changeAvatarDialog.css";
|
import "./changeAvatarDialog.css";
|
||||||
import ButtonRotkehlchen from "../ButtonRotkehlchen";
|
import ButtonRotkehlchen from "../buttons/buttonRotkehlchen/ButtonRotkehlchen";
|
||||||
import Username from "./Username";
|
import Username from "./Username";
|
||||||
import "./username.css";
|
import "./username.css";
|
||||||
import api from "../../api/axios";
|
import api from "../../api/axios";
|
||||||
|
@ -55,17 +55,17 @@ export default function AvatarDialog({
|
||||||
console.log("Saving profile picture:", file);
|
console.log("Saving profile picture:", file);
|
||||||
if (file) {
|
if (file) {
|
||||||
formData.append("image", file);
|
formData.append("image", file);
|
||||||
const response = await api.post(
|
const response = await api.post<{ url: string }>(
|
||||||
"/profile/uploadProfilePicture",
|
"/profile/uploadProfilePicture",
|
||||||
formData
|
formData
|
||||||
);
|
);
|
||||||
console.log("Profile picture saved:", response.data);
|
console.log("Profile picture saved:", response.data);
|
||||||
setUserData((prevData) => (prevData ?{
|
setUserData((prevData) => (prevData ? {
|
||||||
...prevData,
|
...prevData,
|
||||||
profilePictureUrl: response.data.url
|
profilePictureUrl: response.data.url
|
||||||
} : null));
|
} : null));
|
||||||
}
|
}
|
||||||
setOpen(false); // Close the dialog after saving
|
setOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving profile picture:", error);
|
console.error("Error saving profile picture:", error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,16 @@ import { useEffect, useState } from "react";
|
||||||
import { UserProfile } from "../../types/UserProfile";
|
import { UserProfile } from "../../types/UserProfile";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
type Post = {
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PostImagesResponse = {
|
||||||
|
images: { url: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
export default function StandardImageList({ user }: { user: UserProfile }) {
|
export default function StandardImageList({ user }: { user: UserProfile }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [images, setImages] = useState<
|
const [images, setImages] = useState<
|
||||||
|
@ -22,17 +32,15 @@ export default function StandardImageList({ user }: { user: UserProfile }) {
|
||||||
|
|
||||||
const fetchUserPosts = async () => {
|
const fetchUserPosts = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/posts/getUserPosts/${user.username}`);
|
const response = await api.get<{ posts: Post[] }>(
|
||||||
|
`/posts/getUserPosts/${user.username}`
|
||||||
|
);
|
||||||
const posts = response.data.posts;
|
const posts = response.data.posts;
|
||||||
const fetchedImages = await Promise.all(
|
const fetchedImages = await Promise.all(
|
||||||
posts.map(
|
posts.map(
|
||||||
async (post: {
|
async (post: Post) => {
|
||||||
id: string;
|
|
||||||
description: string;
|
|
||||||
createdAt: string;
|
|
||||||
}) => {
|
|
||||||
try {
|
try {
|
||||||
const response = await api.get(
|
const response = await api.get<PostImagesResponse>(
|
||||||
`/posts/getPost/{postId}?postId=${post.id}`
|
`/posts/getPost/{postId}?postId=${post.id}`
|
||||||
);
|
);
|
||||||
if (response.data && response.data.images.length > 0) {
|
if (response.data && response.data.images.length > 0) {
|
||||||
|
@ -42,6 +50,7 @@ export default function StandardImageList({ user }: { user: UserProfile }) {
|
||||||
id: post.id,
|
id: post.id,
|
||||||
description: post.description || "",
|
description: post.description || "",
|
||||||
createdAt: post.createdAt,
|
createdAt: post.createdAt,
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -51,7 +60,12 @@ export default function StandardImageList({ user }: { user: UserProfile }) {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
console.log("Fetched images:", fetchedImages);
|
console.log("Fetched images:", fetchedImages);
|
||||||
setImages(fetchedImages.filter((image) => image !== undefined));
|
setImages(
|
||||||
|
fetchedImages.filter(
|
||||||
|
(image): image is { imageUrl: string; id: string; description: string; createdAt: string } =>
|
||||||
|
image !== undefined
|
||||||
|
)
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching user posts:", error);
|
console.error("Error fetching user posts:", error);
|
||||||
}
|
}
|
||||||
|
@ -68,10 +82,7 @@ export default function StandardImageList({ user }: { user: UserProfile }) {
|
||||||
<img
|
<img
|
||||||
src={item.imageUrl}
|
src={item.imageUrl}
|
||||||
alt={item.description}
|
alt={item.description}
|
||||||
onClick={
|
onClick={() => navigate(`/feed/${user.username}#${item.id}`)}
|
||||||
() => navigate("/feed")
|
|
||||||
// anchor to post that was clicked
|
|
||||||
}
|
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import "./welcomeMessage.css";
|
||||||
|
|
||||||
|
export default function WelcomeMessage() {
|
||||||
|
return (
|
||||||
|
<div className="welcome-message">
|
||||||
|
<h1 className="welcome-title">Welcome!</h1>
|
||||||
|
<p className="welcome-text">Join our bird-loving community!</p>
|
||||||
|
<p className="desktop-welcome-text">
|
||||||
|
Exchange pictures, share your experiences, and discover interesting
|
||||||
|
facts about birds from all over the world. Celebrate
|
||||||
|
the beauty and diversity of birds with us!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import "./notFound.css";
|
import "./notFound.css";
|
||||||
import ButtonPrimary from "../../components/ButtonRotkehlchen";
|
import ButtonPrimary from "../../components/buttons/buttonRotkehlchen/ButtonRotkehlchen";
|
||||||
|
|
||||||
type Block = {
|
type Block = {
|
||||||
x: number;
|
x: number;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import "./loginAndSignUpPage.css";
|
import "./loginAndSignUpPage.css";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import api from "../api/axios";
|
import api from "../api/axios";
|
||||||
import ButtonRotkehlchen from "../components/ButtonRotkehlchen";
|
import ButtonRotkehlchen from "../components/buttons/buttonRotkehlchen/ButtonRotkehlchen";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from "../api/Auth";
|
import { useAuth } from "../api/Auth";
|
||||||
import { createTheme, useMediaQuery } from "@mui/material";
|
import { createTheme, useMediaQuery } from "@mui/material";
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
|
||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
import Autocomplete from '@mui/material/Autocomplete';
|
import Autocomplete from '@mui/material/Autocomplete';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import ButtonPrimary from "../components/ButtonRotkehlchen";
|
import ButtonPrimary from "../components/buttons/buttonRotkehlchen/ButtonRotkehlchen";
|
||||||
|
|
||||||
import api from "../api/axios";
|
import api from "../api/axios";
|
||||||
import { useAuth } from "../api/Auth";
|
import { useAuth } from "../api/Auth";
|
||||||
|
|
|
@ -3,7 +3,7 @@ import QuiltedImageList from "../components/profile/QuiltedImageList";
|
||||||
import { StyledEngineProvider, Divider } from "@mui/material";
|
import { StyledEngineProvider, Divider } from "@mui/material";
|
||||||
import ChangeAvatarDialog from "../components/profile/ChangeAvatarDialog";
|
import ChangeAvatarDialog from "../components/profile/ChangeAvatarDialog";
|
||||||
import Bio from "../components/profile/Bio";
|
import Bio from "../components/profile/Bio";
|
||||||
import RotkehlchenButton from "../components/ButtonRotkehlchen";
|
import RotkehlchenButton from "../components/buttons/buttonRotkehlchen/ButtonRotkehlchen";
|
||||||
import api, { redirectToLogin } from "../api/axios";
|
import api, { redirectToLogin } from "../api/axios";
|
||||||
import { useAuth } from "../api/Auth";
|
import { useAuth } from "../api/Auth";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
@ -28,11 +28,11 @@ function Profile() {
|
||||||
|
|
||||||
const userProfile = async () => {
|
const userProfile = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/profile/${username}`);
|
const response = await api.get<{ data: UserProfile }>(`/profile/${username}`);
|
||||||
setUserData(response.data.data);
|
setUserData(response.data.data);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
navigate("/"); /* replace to 404 page */
|
navigate("/notfound");
|
||||||
console.error("Error fetching user profile:", error);
|
console.error("Error fetching user profile:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import Post from "../Post";
|
import Post from "../../components/post/Post";
|
||||||
import "./feed.css";
|
import "./feed.css";
|
||||||
import api from "../../api/axios";
|
import api from "../../api/axios";
|
||||||
import { create } from "axios";
|
import WelcomeMessage from "../../components/welcomeMessage/welcomeMessage";
|
||||||
import WelcomeMessage from "./welcomeMessage/welcomeMessage";
|
|
||||||
import { useAuth } from "../../api/Auth";
|
import { useAuth } from "../../api/Auth";
|
||||||
import ButtonRotkehlchen from "../ButtonRotkehlchen";
|
import LogInButton from "../../components/buttons/LogInButton";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import SignUpButton from "../../components/buttons/SignUpButton";
|
||||||
|
import NaggingFooter from "../../components/naggingFooter/NaggingFooter";
|
||||||
|
import { useLocation, useParams } from "react-router-dom";
|
||||||
|
|
||||||
interface PostListItem {
|
interface PostListItem {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -26,7 +27,19 @@ function Feed({ username }: FeedProps) {
|
||||||
const feedRef = useRef<HTMLDivElement | null>(null);
|
const feedRef = useRef<HTMLDivElement | null>(null);
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const navigate = useNavigate();
|
const scrollTargetRef = useRef<string | null>(null);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Remove the # character from the hash
|
||||||
|
const hashValue = location.hash.replace("#", "");
|
||||||
|
console.log(hashValue);
|
||||||
|
console.log("Hash value:", hashValue);
|
||||||
|
|
||||||
|
if (hashValue) {
|
||||||
|
scrollTargetRef.current = hashValue;
|
||||||
|
}
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
const fetchPosts = async () => {
|
const fetchPosts = async () => {
|
||||||
if (loading || !hasMore) return;
|
if (loading || !hasMore) return;
|
||||||
|
@ -41,7 +54,9 @@ function Feed({ username }: FeedProps) {
|
||||||
} else {
|
} else {
|
||||||
url = `/feed?limit=${PAGE_SIZE}`;
|
url = `/feed?limit=${PAGE_SIZE}`;
|
||||||
if (nextCursor) {
|
if (nextCursor) {
|
||||||
url = `/feed?createdAt=${encodeURIComponent(nextCursor)}&limit=${PAGE_SIZE}`;
|
url = `/feed?createdAt=${encodeURIComponent(
|
||||||
|
nextCursor
|
||||||
|
)}&limit=${PAGE_SIZE}`;
|
||||||
}
|
}
|
||||||
interface FeedResponse {
|
interface FeedResponse {
|
||||||
posts: PostListItem[];
|
posts: PostListItem[];
|
||||||
|
@ -49,7 +64,8 @@ function Feed({ username }: FeedProps) {
|
||||||
}
|
}
|
||||||
const response = await api.get<FeedResponse>(url);
|
const response = await api.get<FeedResponse>(url);
|
||||||
const { posts: newPosts, nextCursor: newCursor } = response.data;
|
const { posts: newPosts, nextCursor: newCursor } = response.data;
|
||||||
setPosts((prev) => [...prev, ...newPosts]);
|
const tempPost: PostListItem[] = [...posts, ...newPosts];
|
||||||
|
setPosts(tempPost);
|
||||||
setNextCursor(newCursor);
|
setNextCursor(newCursor);
|
||||||
setHasMore(!!newCursor && newPosts.length > 0);
|
setHasMore(!!newCursor && newPosts.length > 0);
|
||||||
}
|
}
|
||||||
|
@ -59,11 +75,6 @@ function Feed({ username }: FeedProps) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchPosts();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (username) return;
|
if (username) return;
|
||||||
|
|
||||||
|
@ -94,30 +105,25 @@ function Feed({ username }: FeedProps) {
|
||||||
{!user && (
|
{!user && (
|
||||||
<div className="welcome-for-logged-out">
|
<div className="welcome-for-logged-out">
|
||||||
<WelcomeMessage />
|
<WelcomeMessage />
|
||||||
<ButtonRotkehlchen
|
<SignUpButton />
|
||||||
style={"secondary"}
|
<LogInButton />
|
||||||
label={"Sign Up"}
|
|
||||||
type={"button"}
|
|
||||||
onClick={() => navigate("/register")}
|
|
||||||
/>
|
|
||||||
<ButtonRotkehlchen
|
|
||||||
style={"primary"}
|
|
||||||
label={"Login"}
|
|
||||||
type={"button"}
|
|
||||||
onClick={() => navigate("/login")}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<main className="feedContent" ref={feedRef}>
|
<main className="feedContent" ref={feedRef}>
|
||||||
{posts.length === 0 && !loading && <div>Keine Posts gefunden.</div>}
|
{posts.length === 0 && !loading && <div>Keine Posts gefunden.</div>}
|
||||||
{posts.map((post) => (
|
{posts.map((post) => (
|
||||||
<div id={post.id} key={post.id}>
|
<div id={post.id} key={post.id} className="feed-post-container">
|
||||||
<Post postId={post.id} />
|
<Post
|
||||||
|
postId={post.id}
|
||||||
|
autoScroll={post.id === scrollTargetRef.current}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{loading && <div className="loading">Loading more posts...</div>}
|
{loading && <div className="loading">Loading more posts...</div>}
|
||||||
{!hasMore && <div>No more posts</div>}
|
{!hasMore && <div className="no-more-posts-message">No more posts</div>}
|
||||||
</main>
|
</main>
|
||||||
|
{!user && <NaggingFooter />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -1,7 +1,5 @@
|
||||||
.feedContainer {
|
.feedContainer {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
/* Subtract he */
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +7,8 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
|
@ -23,9 +22,19 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
.feed-post-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0.1rem auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.no-more-posts-message {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop view*/
|
||||||
/* Desktop responsive behavior */
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.feedContainer {
|
.feedContainer {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
@ -44,7 +53,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem 0;
|
padding: 0 2rem 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,7 +99,7 @@
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
width: 50vw;
|
width: 50vw;
|
||||||
height: 60vh;
|
height: 60vh;
|
||||||
min-height: 400px;
|
min-height: 60vh;
|
||||||
min-width: 500px;
|
min-width: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
import React, { useMemo } from "react";
|
|
||||||
import "./testPost.css";
|
|
||||||
|
|
||||||
interface TestPostProps {
|
|
||||||
postId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getRandomColor = () => {
|
|
||||||
const letters = "0123456789ABCDEF";
|
|
||||||
let color = "#";
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
color += letters[Math.floor(Math.random() * 16)];
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TestPost: React.FC<TestPostProps> = ({ postId }) => {
|
|
||||||
const bgColor = useMemo(() => getRandomColor(), []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="testPostCard"
|
|
||||||
style={{ backgroundColor: bgColor }}
|
|
||||||
>
|
|
||||||
<span className="testPostNumber">{postId}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TestPost;
|
|
|
@ -1,17 +0,0 @@
|
||||||
.testPostCard {
|
|
||||||
width: 100%;
|
|
||||||
height: 180px;
|
|
||||||
border-radius: 10px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 3rem;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
user-select: none;
|
|
||||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.testPostNumber {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
10296
code/frontend/yarn.lock
10296
code/frontend/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue