mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
added about / redesigned main page
This commit is contained in:
1 parent
e7193c70c2
commit
63753c65fd
5 files changed
+514
-270
No files matched your search
+36
-12
@@ -41,33 +41,36 @@ const FlavorDetail = lazy(() => import('./pages/flavors/FlavorDetail'));
|
||||
const Dashboard = lazy(() => import('./pages/Dashboard'));
|
||||
const PublicProfile = lazy(() => import('./pages/PublicProfile'));
|
||||
const CommunityFeed = lazy(() => import('./pages/CommunityFeed'));
|
||||
const About = lazy(() => import('./pages/About'));
|
||||
const Login = lazy(() => import('./pages/Login'));
|
||||
const Settings = lazy(() => import('./pages/Settings'));
|
||||
|
||||
interface SearchFlavor {
|
||||
interface SearchResult {
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'flavor' | 'category' | 'user';
|
||||
subtitle: string;
|
||||
image_url: string | null;
|
||||
category_name: string;
|
||||
slug: string | null;
|
||||
}
|
||||
|
||||
const GlobalSearch = () => {
|
||||
const [query, setQuery] = useState('');
|
||||
const [options, setOptions] = useState<SearchFlavor[]>([]);
|
||||
const [options, setOptions] = useState<SearchResult[]>([]);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFlavors = async () => {
|
||||
const fetchSearchOptions = async () => {
|
||||
try {
|
||||
// Fetch from unpaginated search endpoint for full catalog coverage
|
||||
// Fetch unified results: flavors, categories, and users
|
||||
const res = await api.get('flavors/search/');
|
||||
setOptions(res.data);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch search options');
|
||||
}
|
||||
};
|
||||
fetchFlavors();
|
||||
fetchSearchOptions();
|
||||
}, []); // Only fetch once to avoid excessive API calls
|
||||
|
||||
useEffect(() => {
|
||||
@@ -93,13 +96,15 @@ const GlobalSearch = () => {
|
||||
options={options}
|
||||
getOptionLabel={(option) => {
|
||||
if (typeof option === 'string') return option;
|
||||
return option.category_name ? `${option.name} (${option.category_name})` : (option.name || '');
|
||||
return option.name || '';
|
||||
}}
|
||||
inputValue={query}
|
||||
onInputChange={(_, newValue) => setQuery(typeof newValue === 'string' ? newValue : '')}
|
||||
onChange={(_, newValue) => {
|
||||
if (newValue && typeof newValue !== 'string') {
|
||||
navigate(`/flavor/${newValue.id}`);
|
||||
if (newValue.type === 'flavor') navigate(`/flavor/${newValue.id}`);
|
||||
if (newValue.type === 'category') navigate(`/category/${newValue.slug}`);
|
||||
if (newValue.type === 'user') navigate(`/profile/${newValue.slug}`);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
@@ -111,7 +116,7 @@ const GlobalSearch = () => {
|
||||
<TextField
|
||||
{...params}
|
||||
fullWidth
|
||||
placeholder="Search flavors..."
|
||||
placeholder="Search..."
|
||||
sx={{
|
||||
bgcolor: 'action.hover',
|
||||
borderRadius: 2,
|
||||
@@ -134,12 +139,16 @@ const GlobalSearch = () => {
|
||||
const { key, ...optionProps } = props as any;
|
||||
return (
|
||||
<Box component="li" key={key} {...optionProps} sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
{option.image_url && (
|
||||
<Box component="img" src={option.image_url} sx={{ width: 32, height: 32, objectFit: 'contain' }} />
|
||||
{option.image_url ? (
|
||||
<Avatar src={option.image_url} sx={{ width: 32, height: 32, borderRadius: 1 }} />
|
||||
) : (
|
||||
<Avatar sx={{ width: 32, height: 32, borderRadius: 1, bgcolor: 'primary.main', fontSize: '0.8rem' }}>
|
||||
{option.name.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
)}
|
||||
<Box>
|
||||
<Typography variant="body2">{option.name}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{option.category_name}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{option.subtitle}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -243,6 +252,12 @@ const App: React.FC = () => {
|
||||
<ListItemText primary="Community" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton component={Link} to="/about" onClick={() => setDrawerOpen(false)}>
|
||||
<ListItemText primary="About" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
<ListItemButton onClick={() => setCatOpen(!catOpen)}>
|
||||
<ListItemText primary="Categories" />
|
||||
@@ -343,6 +358,14 @@ const App: React.FC = () => {
|
||||
>
|
||||
Community
|
||||
</Button>
|
||||
<Button
|
||||
color="inherit"
|
||||
component={Link}
|
||||
to="/about"
|
||||
sx={{ fontWeight: 'bold', textTransform: 'none', fontSize: '1rem', mr: 2 }}
|
||||
>
|
||||
About
|
||||
</Button>
|
||||
<Button
|
||||
color="inherit"
|
||||
endIcon={<ArrowDropDownIcon />}
|
||||
@@ -433,6 +456,7 @@ const App: React.FC = () => {
|
||||
<Route path="/flavor/:id" element={<FlavorDetail />} />
|
||||
<Route path="/profile/:username" element={<PublicProfile />} />
|
||||
<Route path="/community" element={<CommunityFeed />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/settings" element={<Settings themeName={themeName} onThemeChange={handleThemeChange} />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import { Typography, Box, Container, Paper, Divider, Button } from '@mui/material';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import { useTitle } from '../hooks/useTitle';
|
||||
|
||||
const About: React.FC = () => {
|
||||
useTitle('About & Impressum');
|
||||
|
||||
return (
|
||||
<Container maxWidth="md" sx={{ py: 8 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
component={Link}
|
||||
to="/"
|
||||
startIcon={<ArrowBackIcon />}
|
||||
sx={{ mb: 4, textTransform: 'none', borderRadius: 2 }}
|
||||
>
|
||||
Back to Archive
|
||||
</Button>
|
||||
|
||||
<Paper sx={{ p: { xs: 3, md: 6 }, borderRadius: 4 }}>
|
||||
<Typography variant="h3" gutterBottom sx={{ fontWeight: 'bold' }}>About This Project</Typography>
|
||||
<Typography variant="body1" paragraph sx={{ fontSize: '1.1rem', lineHeight: 1.8 }}>
|
||||
<strong>Holy Flavors Archive</strong> is a community-driven project dedicated to documenting and rating
|
||||
the vast world of Holy Energy products. Our goal is to create a definitive, high-density catalog of every
|
||||
flavor ever released—including legacy and limited editions.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ my: 4 }}>
|
||||
<Typography variant="h5" gutterBottom sx={{ fontWeight: 'bold' }}>What you can do here:</Typography>
|
||||
<Typography component="ul" sx={{ lineHeight: 2 }}>
|
||||
<li><strong>Explore:</strong> Find details on any Holy Energy, Iced Tea, or Milkshake product.</li>
|
||||
<li><strong>Rate:</strong> Share your honest opinion and score flavors from 1 to 10.</li>
|
||||
<li><strong>Community:</strong> Follow other fans, see their latest tests, and discover new favorites together.</li>
|
||||
<li><strong>Taste Profile:</strong> Build your own personal tiered leaderboard to share with the world.</li>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
This website is a fan project and is not officially affiliated with, authorized, maintained, sponsored,
|
||||
or endorsed by HOLY Energy GmbH. All product names, logos, and brands are property of their respective owners.
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: 6 }} />
|
||||
|
||||
<Typography variant="h4" gutterBottom sx={{ fontWeight: 'bold' }}>Impressum</Typography>
|
||||
<Typography variant="body2" sx={{ lineHeight: 2 }}>
|
||||
<strong>Angaben gemäß § 5 TMG:</strong><br />
|
||||
[Your Name or Project Name]<br />
|
||||
[Your Street Address]<br />
|
||||
[Postal Code and City]<br />
|
||||
Germany
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>Kontakt:</Typography>
|
||||
<Typography variant="body2">
|
||||
E-Mail: [Your Support Email Address]<br />
|
||||
Internet: holy.narl.io
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV:</Typography>
|
||||
<Typography variant="body2">
|
||||
[Your Name]<br />
|
||||
[Your Street Address]<br />
|
||||
[Postal Code and City]
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 4 }}>
|
||||
<Typography variant="h6" gutterBottom sx={{ fontWeight: 'bold' }}>Haftungsausschluss (Disclaimer)</Typography>
|
||||
<Typography variant="caption" color="text.secondary" component="p" sx={{ mb: 2 }}>
|
||||
<strong>Haftung für Inhalte:</strong> Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte
|
||||
auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter
|
||||
jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen.
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" component="p">
|
||||
<strong>Urheberrecht:</strong> Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten
|
||||
unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der
|
||||
Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
ListItemButton
|
||||
ListItemButton,
|
||||
Pagination,
|
||||
Paper as MuiPaper
|
||||
} from '@mui/material';
|
||||
import StarIcon from '@mui/icons-material/Star';
|
||||
import { Link } from 'react-router-dom';
|
||||
@@ -48,53 +50,52 @@ const CommunityFeed: React.FC = () => {
|
||||
const [ratings, setRatings] = useState<FeedRating[]>([]);
|
||||
const [following, setFollowing] = useState<FollowedUser[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [nextPage, setNextPage] = useState<string | null>(null);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
|
||||
const fetchInitialData = async () => {
|
||||
const fetchFeedData = async (pageNum: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [feedRes, followingRes] = await Promise.all([
|
||||
api.get('ratings/feed/'),
|
||||
api.get(`ratings/feed/?page=${pageNum}`),
|
||||
api.get('users/following_list/')
|
||||
]);
|
||||
|
||||
const feedData = feedRes.data.results || feedRes.data;
|
||||
setRatings(Array.isArray(feedData) ? feedData : []);
|
||||
setNextPage(feedRes.data.next || null);
|
||||
const feedData = feedRes.data.results || [];
|
||||
setRatings(feedData);
|
||||
|
||||
// Calculate total pages (DRF returns total count)
|
||||
const count = feedRes.data.count || 0;
|
||||
setTotalPages(Math.ceil(count / 10)); // 10 is our page_size
|
||||
|
||||
setFollowing(followingRes.data);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch initial feed data', err);
|
||||
console.error('Failed to fetch feed data', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadMore = async () => {
|
||||
if (!nextPage || loadingMore) return;
|
||||
setLoadingMore(true);
|
||||
try {
|
||||
// nextPage is a full URL from DRF, we can use axios directly or parse it
|
||||
const res = await api.get(nextPage);
|
||||
const newData = res.data.results || res.data;
|
||||
setRatings(prev => [...prev, ...(Array.isArray(newData) ? newData : [])]);
|
||||
setNextPage(res.data.next || null);
|
||||
} catch (err) {
|
||||
console.error('Failed to load more feed items', err);
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
useEffect(() => {
|
||||
fetchFeedData(page);
|
||||
}, [page]);
|
||||
|
||||
const handlePageChange = (_: React.ChangeEvent<unknown>, value: number) => {
|
||||
setPage(value);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchInitialData();
|
||||
}, []);
|
||||
|
||||
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', mt: 8 }}><CircularProgress /></Box>;
|
||||
if (loading && ratings.length === 0) return <Box sx={{ display: 'flex', justifyContent: 'center', mt: 8 }}><CircularProgress /></Box>;
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||
<Typography variant="h4" sx={{ mb: 4, fontWeight: 'bold' }}>Community Feed</Typography>
|
||||
<Box sx={{ mb: 6 }}>
|
||||
<Typography variant="h3" sx={{ mb: 1, fontWeight: 'bold' }}>Community Activity</Typography>
|
||||
<Typography variant="h6" color="text.secondary" sx={{ maxWidth: 700, lineHeight: 1.6 }}>
|
||||
Discover which Holy Energy flavors your circle is currently testing.
|
||||
See their latest ratings, read their reviews, and find your next favorite together.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={4}>
|
||||
{/* Main Feed */}
|
||||
@@ -111,87 +112,95 @@ const CommunityFeed: React.FC = () => {
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{ratings.map(rating => (
|
||||
<Card key={rating.id} sx={{ borderRadius: 3, transition: 'transform 0.2s', '&:hover': { transform: 'translateY(-2px)' } }}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Link to={`/profile/${rating.user}`} style={{ textDecoration: 'none', color: 'inherit', display: 'flex', alignItems: 'center' }}>
|
||||
<Avatar src={rating.user_avatar || undefined} sx={{ width: 40, height: 40, mr: 2 }}>
|
||||
{!rating.user_avatar && rating.user.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold', '&:hover': { color: 'primary.main' } }}>
|
||||
{rating.user}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{new Date(rating.created_at).toLocaleDateString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 3, flexDirection: { xs: 'column', sm: 'row' } }}>
|
||||
<Link to={`/flavor/${rating.flavor}`} style={{ textDecoration: 'none' }}>
|
||||
<Box sx={{ width: { xs: '100%', sm: 100 }, aspectRatio: '1/1', borderRadius: 2, overflow: 'hidden', border: '1px solid', borderColor: 'divider' }}>
|
||||
<Box
|
||||
component="img"
|
||||
src={rating.flavor_image || undefined}
|
||||
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
</Box>
|
||||
</Link>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Link to={`/flavor/${rating.flavor}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Typography variant="h6" sx={{ fontSize: '1.1rem', mb: 0.5, '&:hover': { color: 'primary.main' } }}>
|
||||
{rating.flavor_name}
|
||||
</Typography>
|
||||
</Link>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1.5 }}>
|
||||
{rating.category_name}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1.5 }}>
|
||||
{/* Compact rating for mobile, stars for desktop */}
|
||||
<Box sx={{ display: { xs: 'flex', sm: 'none' }, alignItems: 'center', gap: 0.5 }}>
|
||||
<StarIcon sx={{ fontSize: '1rem', color: 'primary.main' }} />
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>{rating.score}/10</Typography>
|
||||
{loading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}><CircularProgress /></Box>
|
||||
) : (
|
||||
<>
|
||||
{ratings.map(rating => (
|
||||
<Card key={rating.id} sx={{ borderRadius: 3, transition: 'transform 0.2s', '&:hover': { transform: 'translateY(-2px)' } }}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Link to={`/profile/${rating.user}`} style={{ textDecoration: 'none', color: 'inherit', display: 'flex', alignItems: 'center' }}>
|
||||
<Avatar src={rating.user_avatar || undefined} sx={{ width: 40, height: 40, mr: 2 }}>
|
||||
{!rating.user_avatar && rating.user.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold', '&:hover': { color: 'primary.main' } }}>
|
||||
{rating.user}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{new Date(rating.created_at).toLocaleDateString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Link>
|
||||
</Box>
|
||||
<Box sx={{ display: { xs: 'none', sm: 'flex' }, alignItems: 'center' }}>
|
||||
<MuiRating value={rating.score} readOnly max={10} size="small" />
|
||||
<Typography variant="body2" sx={{ ml: 1, fontWeight: 'bold' }}>{rating.score}/10</Typography>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 3, flexDirection: { xs: 'column', sm: 'row' } }}>
|
||||
<Link to={`/flavor/${rating.flavor}`} style={{ textDecoration: 'none' }}>
|
||||
<Box sx={{ width: { xs: '100%', sm: 100 }, aspectRatio: '1/1', borderRadius: 2, overflow: 'hidden', border: '1px solid', borderColor: 'divider' }}>
|
||||
<Box
|
||||
component="img"
|
||||
src={rating.flavor_image || undefined}
|
||||
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
</Box>
|
||||
</Link>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Link to={`/flavor/${rating.flavor}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Typography variant="h6" sx={{ fontSize: '1.1rem', mb: 0.5, '&:hover': { color: 'primary.main' } }}>
|
||||
{rating.flavor_name}
|
||||
</Typography>
|
||||
</Link>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1.5 }}>
|
||||
{rating.category_name}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1.5 }}>
|
||||
{/* Compact rating for mobile, stars for desktop */}
|
||||
<Box sx={{ display: { xs: 'flex', sm: 'none' }, alignItems: 'center', gap: 0.5 }}>
|
||||
<StarIcon sx={{ fontSize: '1rem', color: 'primary.main' }} />
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>{rating.score}/10</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: { xs: 'none', sm: 'flex' }, alignItems: 'center' }}>
|
||||
<MuiRating value={rating.score} readOnly max={10} size="small" />
|
||||
<Typography variant="body2" sx={{ ml: 1, fontWeight: 'bold' }}>{rating.score}/10</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{rating.comment && (
|
||||
<Typography variant="body2" sx={{
|
||||
fontStyle: 'italic',
|
||||
color: 'text.secondary',
|
||||
borderLeft: '3px solid',
|
||||
borderColor: 'divider',
|
||||
pl: 2,
|
||||
py: 0.5
|
||||
}}>
|
||||
"{rating.comment}"
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{rating.comment && (
|
||||
<Typography variant="body2" sx={{
|
||||
fontStyle: 'italic',
|
||||
color: 'text.secondary',
|
||||
borderLeft: '3px solid',
|
||||
borderColor: 'divider',
|
||||
pl: 2,
|
||||
py: 0.5
|
||||
}}>
|
||||
"{rating.comment}"
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{nextPage && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleLoadMore}
|
||||
disabled={loadingMore}
|
||||
sx={{ borderRadius: 2, px: 4 }}
|
||||
>
|
||||
{loadingMore ? <CircularProgress size={24} /> : 'Load More Activity'}
|
||||
</Button>
|
||||
{totalPages > 1 && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
|
||||
<Pagination
|
||||
count={totalPages}
|
||||
page={page}
|
||||
onChange={handlePageChange}
|
||||
color="primary"
|
||||
size="large"
|
||||
sx={{
|
||||
'& .MuiPaginationItem-root': { borderRadius: 2, fontWeight: 'bold' }
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
+235
-143
@@ -210,153 +210,245 @@ const MainPage: React.FC = () => {
|
||||
const currentTop = topFlavors[activeIndex];
|
||||
const featuredReview = currentTop?.ratings?.find(r => r.comment) || null;
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
{/* Hero Section / Activity Feed */}
|
||||
{isLoggedIn ? (
|
||||
<Box sx={{
|
||||
py: { xs: 4, md: 8 },
|
||||
px: { xs: 2, sm: 4, md: 6 },
|
||||
background: (theme) => theme.palette.mode === 'dark'
|
||||
? 'linear-gradient(180deg, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0) 100%)'
|
||||
: 'linear-gradient(180deg, rgba(0,0,0,0.05) 0%, rgba(0,0,0,0) 100%)',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
mb: 4
|
||||
}}>
|
||||
<Container maxWidth="lg">
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', mb: 4, flexWrap: 'wrap', gap: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="h3" sx={{ fontWeight: 'bold', mb: 1, fontSize: { xs: '2rem', md: '3rem' } }}>Community Activity</Typography>
|
||||
<Typography variant="body1" color="text.secondary">Latest reviews from people you follow</Typography>
|
||||
</Box>
|
||||
<Button component={Link} to="/community" variant="outlined" sx={{ borderRadius: 2, textTransform: 'none' }}>
|
||||
View Full Feed
|
||||
</Button>
|
||||
</Box>
|
||||
return (
|
||||
|
||||
<Box sx={{ width: '100%' }}>
|
||||
|
||||
{/* 1. Hero Section / Activity Feed */}
|
||||
|
||||
{isLoggedIn ? (
|
||||
|
||||
<Box sx={{
|
||||
|
||||
py: { xs: 4, md: 8 },
|
||||
|
||||
px: { xs: 2, sm: 4, md: 6 },
|
||||
|
||||
{feedRatings.length === 0 ? (
|
||||
<Paper sx={{ p: 4, textAlign: 'center', borderRadius: 4, bgcolor: 'action.hover', border: '2px dashed', borderColor: 'divider', elevation: 0 }}>
|
||||
<Typography variant="h6">Your feed is a bit quiet</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>Follow more users to see their activity here!</Typography>
|
||||
<Button variant="contained" component={Link} to="/" onClick={() => document.getElementById('categories')?.scrollIntoView({ behavior: 'smooth' })} size="small" sx={{ borderRadius: 2 }}>
|
||||
Discover Reviewers
|
||||
</Button>
|
||||
</Paper>
|
||||
) : (
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' }, gap: 3 }}>
|
||||
{feedRatings.map(rating => (
|
||||
<Card key={rating.id} sx={{ borderRadius: 3, transition: 'transform 0.2s', '&:hover': { transform: 'translateY(-4px)' } }}>
|
||||
<CardContent sx={{ p: { xs: 1.5, sm: 2 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1.5 }}>
|
||||
<Link to={`/profile/${rating.user}`} style={{ textDecoration: 'none', color: 'inherit', display: 'flex', alignItems: 'center' }}>
|
||||
<Avatar src={rating.user_avatar || undefined} sx={{ width: 24, height: 24, mr: 1 }}>
|
||||
{!rating.user_avatar && rating.user.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', fontSize: '0.8rem' }}>{rating.user}</Typography>
|
||||
</Link>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 0.5 }}>rated</Typography>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{new Date(rating.created_at).toLocaleDateString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center' }}>
|
||||
<Box
|
||||
component="img"
|
||||
src={rating.flavor_image || undefined}
|
||||
sx={{ width: 50, height: 50, borderRadius: 1, objectFit: 'cover', border: '1px solid', borderColor: 'divider' }}
|
||||
/>
|
||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||
<Link to={`/flavor/${rating.flavor}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold', display: '-webkit-box', WebkitLineClamp: 1, WebkitBoxOrient: 'vertical', overflow: 'hidden', fontSize: '0.85rem', mb: 0.5 }}>
|
||||
{rating.flavor_name}
|
||||
</Typography>
|
||||
</Link>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{/* Compact rating for anything below Large desktop to prevent cutoffs */}
|
||||
<Box sx={{ display: { xs: 'flex', lg: 'none' }, alignItems: 'center', gap: 0.5 }}>
|
||||
<StarIcon sx={{ fontSize: '1rem', color: 'primary.main' }} />
|
||||
<Typography variant="caption" sx={{ fontWeight: 'bold' }}>{rating.score}/10</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: { xs: 'none', lg: 'flex' }, alignItems: 'center' }}>
|
||||
<MuiRating value={rating.score} readOnly size="small" max={10} />
|
||||
<Typography variant="caption" sx={{ ml: 1, fontWeight: 'bold' }}>{rating.score}/10</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Container>
|
||||
</Box>
|
||||
) : (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: { xs: 4, md: 10 },
|
||||
mb: 4,
|
||||
borderRadius: 0,
|
||||
background: (theme) => theme.palette.mode === 'dark'
|
||||
? 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)'
|
||||
: 'linear-gradient(135deg, #fdf6f7 0%, #f8e1e5 100%)',
|
||||
textAlign: 'center',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.1)',
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<Typography variant="h2" component="h1" gutterBottom sx={{ fontWeight: 'bold', fontSize: { xs: '2.2rem', sm: '3rem', md: '4.5rem' }, overflowWrap: 'break-word', px: 2 }}>
|
||||
The Ultimate Holy Archive
|
||||
</Typography>
|
||||
<Typography variant="h5" color="text.secondary" sx={{ mb: 4, maxWidth: 800, mx: 'auto', px: 2 }}>
|
||||
Browse every flavor ever released, discover new favorites, and share your ratings with the community.
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 2, flexWrap: 'wrap', px: 2 }}>
|
||||
<Button variant="contained" size="large" component={Link} to="/login" sx={{ px: 4, py: 1.5, borderRadius: 2 }}>
|
||||
Join Community
|
||||
</Button>
|
||||
<Button variant="outlined" size="large" onClick={() => document.getElementById('categories')?.scrollIntoView({ behavior: 'smooth' })} sx={{ px: 4, py: 1.5, borderRadius: 2 }}>
|
||||
Explore Categories
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<Container maxWidth={false} sx={{ px: { xs: 2, sm: 4, md: 6 }, pb: 8 }}>
|
||||
|
||||
{/* Categories Grid - MOVED UP */}
|
||||
<Box id="categories" sx={{ mb: 8 }}>
|
||||
<Typography variant="h4" sx={{ mb: 4, fontWeight: 'bold' }}>Browse by Category</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
|
||||
{categories.map(category => (
|
||||
<Box key={category.id} sx={{ flex: { xs: '1 1 100%', sm: '1 1 45%', lg: '1 1 23%' }, minWidth: 280 }}>
|
||||
<Link to={`/category/${category.slug}`} style={{ textDecoration: 'none' }}>
|
||||
<Card sx={{
|
||||
height: 100,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.3s',
|
||||
borderRadius: 3,
|
||||
'&:hover': { transform: 'scale(1.02)', bgcolor: 'primary.main', '& .cat-name': { color: 'white' } }
|
||||
}}>
|
||||
<CardContent>
|
||||
<Typography variant="h5" className="cat-name" color="primary" sx={{ fontWeight: 'bold' }}>{category.name}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</Box>
|
||||
))}
|
||||
? 'linear-gradient(180deg, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0) 100%)'
|
||||
|
||||
: 'linear-gradient(180deg, rgba(0,0,0,0.05) 0%, rgba(0,0,0,0) 100%)',
|
||||
|
||||
borderBottom: '1px solid',
|
||||
|
||||
borderColor: 'divider',
|
||||
|
||||
mb: 4
|
||||
|
||||
}}>
|
||||
|
||||
<Container maxWidth="lg">
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', mb: 4, flexWrap: 'wrap', gap: 2 }}>
|
||||
|
||||
<Box>
|
||||
|
||||
<Typography variant="h3" sx={{ fontWeight: 'bold', mb: 1, fontSize: { xs: '2rem', md: '3rem' } }}>Community Activity</Typography>
|
||||
|
||||
<Typography variant="body1" color="text.secondary">Latest reviews from people you follow</Typography>
|
||||
|
||||
</Box>
|
||||
|
||||
<Button component={Link} to="/community" variant="outlined" sx={{ borderRadius: 2, textTransform: 'none' }}>
|
||||
|
||||
View Full Feed
|
||||
|
||||
</Button>
|
||||
|
||||
</Box>
|
||||
|
||||
|
||||
|
||||
{feedRatings.length === 0 ? (
|
||||
|
||||
<Paper sx={{ p: 4, textAlign: 'center', borderRadius: 4, bgcolor: 'action.hover', border: '2px dashed', borderColor: 'divider', elevation: 0 }}>
|
||||
|
||||
<Typography variant="h6">Your feed is a bit quiet</Typography>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>Follow more users to see their activity here!</Typography>
|
||||
|
||||
<Button variant="contained" component={Link} to="/" size="small" sx={{ borderRadius: 2 }}>Explore Flavors</Button>
|
||||
|
||||
</Paper>
|
||||
|
||||
) : (
|
||||
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' }, gap: 3 }}>
|
||||
|
||||
{feedRatings.map(rating => (
|
||||
|
||||
<Card key={rating.id} sx={{ borderRadius: 3, transition: 'transform 0.2s', '&:hover': { transform: 'translateY(-4px)' } }}>
|
||||
|
||||
<CardContent sx={{ p: { xs: 1.5, sm: 2 } }}>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1.5 }}>
|
||||
|
||||
<Link to={`/profile/${rating.user}`} style={{ textDecoration: 'none', color: 'inherit', display: 'flex', alignItems: 'center' }}>
|
||||
|
||||
<Avatar src={rating.user_avatar || undefined} sx={{ width: 24, height: 24, mr: 1 }}>
|
||||
|
||||
{!rating.user_avatar && rating.user.charAt(0).toUpperCase()}
|
||||
|
||||
</Avatar>
|
||||
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', fontSize: '0.8rem' }}>{rating.user}</Typography>
|
||||
|
||||
</Link>
|
||||
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 0.5 }}>rated</Typography>
|
||||
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
|
||||
{new Date(rating.created_at).toLocaleDateString()}
|
||||
|
||||
</Typography>
|
||||
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center' }}>
|
||||
|
||||
<Box
|
||||
|
||||
component="img"
|
||||
|
||||
src={rating.flavor_image || undefined}
|
||||
|
||||
sx={{ width: 50, height: 50, borderRadius: 1, objectFit: 'cover', border: '1px solid', borderColor: 'divider' }}
|
||||
|
||||
/>
|
||||
|
||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||
|
||||
<Link to={`/flavor/${rating.flavor}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold', display: '-webkit-box', WebkitLineClamp: 1, WebkitBoxOrient: 'vertical', overflow: 'hidden', fontSize: '0.85rem', mb: 0.5 }}>
|
||||
|
||||
{rating.flavor_name}
|
||||
|
||||
</Typography>
|
||||
|
||||
</Link>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
|
||||
{/* Compact rating for anything below Large desktop to prevent cutoffs */}
|
||||
|
||||
<Box sx={{ display: { xs: 'flex', lg: 'none' }, alignItems: 'center', gap: 0.5 }}>
|
||||
|
||||
<StarIcon sx={{ fontSize: '1rem', color: 'primary.main' }} />
|
||||
|
||||
<Typography variant="caption" sx={{ fontWeight: 'bold' }}>{rating.score}/10</Typography>
|
||||
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: { xs: 'none', lg: 'flex' }, alignItems: 'center' }}>
|
||||
|
||||
<MuiRating value={rating.score} readOnly size="small" max={10} />
|
||||
|
||||
<Typography variant="caption" sx={{ ml: 1, fontWeight: 'bold' }}>{rating.score}/10</Typography>
|
||||
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
|
||||
</CardContent>
|
||||
|
||||
</Card>
|
||||
|
||||
))}
|
||||
|
||||
</Box>
|
||||
|
||||
)}
|
||||
|
||||
</Container>
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
) : (
|
||||
|
||||
<Paper
|
||||
|
||||
elevation={0}
|
||||
|
||||
sx={{
|
||||
|
||||
p: { xs: 4, md: 10 },
|
||||
|
||||
mb: 4,
|
||||
|
||||
borderRadius: 0,
|
||||
|
||||
background: (theme) => theme.palette.mode === 'dark'
|
||||
|
||||
? 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)'
|
||||
|
||||
: 'linear-gradient(135deg, #fdf6f7 0%, #f8e1e5 100%)',
|
||||
|
||||
textAlign: 'center',
|
||||
|
||||
borderBottom: '1px solid rgba(255,255,255,0.1)',
|
||||
|
||||
width: '100%',
|
||||
|
||||
boxSizing: 'border-box',
|
||||
|
||||
overflow: 'hidden'
|
||||
|
||||
}}
|
||||
|
||||
>
|
||||
|
||||
<Typography variant="h2" component="h1" gutterBottom sx={{ fontWeight: 'bold', fontSize: { xs: '2.2rem', sm: '3rem', md: '4.5rem' }, overflowWrap: 'break-word', px: 2 }}>
|
||||
|
||||
The Ultimate Holy Archive
|
||||
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" color="text.secondary" sx={{ mb: 4, maxWidth: 800, mx: 'auto', px: 2 }}>
|
||||
|
||||
Browse every flavor ever released, discover new favorites, and share your ratings with the community.
|
||||
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 2, flexWrap: 'wrap', px: 2 }}>
|
||||
|
||||
<Button variant="contained" size="large" component={Link} to="/login" sx={{ px: 4, py: 1.5, borderRadius: 2 }}>
|
||||
|
||||
Join Community
|
||||
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" size="large" component={Link} to="/about" sx={{ px: 4, py: 1.5, borderRadius: 2 }}>
|
||||
|
||||
What is this for?
|
||||
|
||||
</Button>
|
||||
|
||||
</Box>
|
||||
|
||||
</Paper>
|
||||
|
||||
)}
|
||||
|
||||
|
||||
|
||||
<Container maxWidth={false} sx={{ px: { xs: 2, sm: 4, md: 6 }, pb: 8 }}>
|
||||
|
||||
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', xl: 'row' }, gap: 6, mb: 10 }}>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', xl: 'row' }, gap: 6, mb: 10 }}>
|
||||
{/* Hall of Fame Carousel - SIDE BY SIDE */}
|
||||
|
||||
Reference in new issue
Block a user