mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-04 00:47:02 +01:00
Merge branch 'main' into narl.io
This commit is contained in:
commit
3ac2c203e1
2 files changed
+201
-68
No files matched your search
@@ -30,10 +30,12 @@ const GenerativeBanner: React.FC<GenerativeBannerProps> = ({ username, palette,
|
||||
let animationFrameId: number;
|
||||
const seed = getSeed(username);
|
||||
|
||||
const nodeCount = Math.min(12 + Math.floor(ratingsCount / 4), 30);
|
||||
const speedMultiplier = 0.15 + (Math.min(followersCount, 50) / 120);
|
||||
// Dynamic count and speed
|
||||
const nodeCount = Math.min(20 + Math.floor(ratingsCount / 2), 50);
|
||||
const speedMultiplier = 0.25 + (Math.min(followersCount, 50) / 80);
|
||||
|
||||
const colors = palette.length > 0 ? palette : [theme.palette.primary.main, theme.palette.secondary.main];
|
||||
// Prefer vibrant colors
|
||||
const colors = palette.length >= 2 ? palette : [theme.palette.primary.main, theme.palette.secondary.main, theme.palette.info.main];
|
||||
|
||||
interface Node {
|
||||
x: number;
|
||||
@@ -44,35 +46,50 @@ const GenerativeBanner: React.FC<GenerativeBannerProps> = ({ username, palette,
|
||||
size: number;
|
||||
}
|
||||
|
||||
const nodes: Node[] = [];
|
||||
let nodes: Node[] = [];
|
||||
|
||||
const pseudoRandom = (offset: number) => {
|
||||
const x = Math.sin(seed + offset) * 10000;
|
||||
return x - Math.floor(x);
|
||||
};
|
||||
|
||||
const initNodes = (width: number, height: number) => {
|
||||
const newNodes = [];
|
||||
for (let i = 0; i < nodeCount; i++) {
|
||||
newNodes.push({
|
||||
x: pseudoRandom(i) * width,
|
||||
y: pseudoRandom(i + 100) * height,
|
||||
vx: (pseudoRandom(i + 200) - 0.5) * speedMultiplier,
|
||||
vy: (pseudoRandom(i + 300) - 0.5) * speedMultiplier,
|
||||
color: colors[i % colors.length],
|
||||
size: 2 + pseudoRandom(i + 400) * 5
|
||||
});
|
||||
}
|
||||
return newNodes;
|
||||
};
|
||||
|
||||
const resize = () => {
|
||||
const parent = canvas.parentElement;
|
||||
if (parent) {
|
||||
canvas.width = parent.clientWidth;
|
||||
canvas.height = parent.clientHeight;
|
||||
const rect = parent.getBoundingClientRect();
|
||||
// Scale for high DPI
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = rect.width * dpr;
|
||||
canvas.height = rect.height * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
// Adjust CSS display size
|
||||
canvas.style.width = `${rect.width}px`;
|
||||
canvas.style.height = `${rect.height}px`;
|
||||
|
||||
nodes = initNodes(rect.width, rect.height);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
const resizeObserver = new ResizeObserver(() => resize());
|
||||
if (canvas.parentElement) resizeObserver.observe(canvas.parentElement);
|
||||
|
||||
resize();
|
||||
|
||||
for (let i = 0; i < nodeCount; i++) {
|
||||
nodes.push({
|
||||
x: pseudoRandom(i) * canvas.width,
|
||||
y: pseudoRandom(i + nodeCount) * canvas.height,
|
||||
vx: (pseudoRandom(i + 2) - 0.5) * speedMultiplier,
|
||||
vy: (pseudoRandom(i + 3) - 0.5) * speedMultiplier,
|
||||
color: colors[i % colors.length],
|
||||
size: 1 + pseudoRandom(i + 4) * 3
|
||||
});
|
||||
}
|
||||
|
||||
let mouse = { x: -1000, y: -1000, down: false };
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
@@ -91,22 +108,30 @@ const GenerativeBanner: React.FC<GenerativeBannerProps> = ({ username, palette,
|
||||
const handleMouseDown = () => mouse.down = true;
|
||||
const handleMouseUp = () => mouse.down = false;
|
||||
|
||||
canvas.addEventListener('mousemove', handleMouseMove);
|
||||
canvas.addEventListener('touchmove', handleTouchMove);
|
||||
canvas.addEventListener('mousedown', handleMouseDown);
|
||||
canvas.addEventListener('mouseup', handleMouseUp);
|
||||
canvas.addEventListener('touchstart', (e) => { mouse.down = true; handleTouchMove(e); });
|
||||
canvas.addEventListener('touchend', handleMouseUp);
|
||||
// Attach to window for better tracking
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('touchmove', handleTouchMove);
|
||||
window.addEventListener('mousedown', handleMouseDown);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
const draw = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
// Get CSS dimensions for drawing
|
||||
const width = canvas.width / (window.devicePixelRatio || 1);
|
||||
const height = canvas.height / (window.devicePixelRatio || 1);
|
||||
|
||||
if (!width || !height) {
|
||||
animationFrameId = requestAnimationFrame(draw);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
nodes.forEach((node) => {
|
||||
node.x += node.vx;
|
||||
node.y += node.vy;
|
||||
|
||||
if (node.x < 0 || node.x > canvas.width) node.vx *= -1;
|
||||
if (node.y < 0 || node.y > canvas.height) node.vy *= -1;
|
||||
if (node.x < 0 || node.x > width) node.vx *= -1;
|
||||
if (node.y < 0 || node.y > height) node.vy *= -1;
|
||||
|
||||
const dx = mouse.x - node.x;
|
||||
const dy = mouse.y - node.y;
|
||||
@@ -116,7 +141,6 @@ const GenerativeBanner: React.FC<GenerativeBannerProps> = ({ username, palette,
|
||||
if (dist < maxDist) {
|
||||
const force = (maxDist - dist) / maxDist;
|
||||
const strength = mouse.down ? 0.2 : 0.08;
|
||||
// Pull when down, repel when hovering
|
||||
if (mouse.down) {
|
||||
node.x += dx * force * strength;
|
||||
node.y += dy * force * strength;
|
||||
@@ -127,31 +151,30 @@ const GenerativeBanner: React.FC<GenerativeBannerProps> = ({ username, palette,
|
||||
}
|
||||
});
|
||||
|
||||
// Draw connections and triangles
|
||||
// Draw mesh connections
|
||||
ctx.lineWidth = 1.5;
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
for (let j = i + 1; j < nodes.length; j++) {
|
||||
const ddist = Math.sqrt(Math.pow(nodes[i].x - nodes[j].x, 2) + Math.pow(nodes[i].y - nodes[j].y, 2));
|
||||
|
||||
if (ddist < 150) {
|
||||
// Lines
|
||||
if (ddist < 160) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(nodes[i].x, nodes[i].y);
|
||||
ctx.lineTo(nodes[j].x, nodes[j].y);
|
||||
ctx.strokeStyle = alpha(nodes[i].color, (1 - ddist / 150) * 0.15);
|
||||
ctx.strokeStyle = alpha(nodes[i].color, (1 - ddist / 160) * 0.3);
|
||||
ctx.stroke();
|
||||
|
||||
// Search for a third point to make a triangle
|
||||
for (let k = j + 1; k < nodes.length; k++) {
|
||||
const ddist2 = Math.sqrt(Math.pow(nodes[j].x - nodes[k].x, 2) + Math.pow(nodes[j].y - nodes[k].y, 2));
|
||||
const ddist3 = Math.sqrt(Math.pow(nodes[i].x - nodes[k].x, 2) + Math.pow(nodes[i].y - nodes[k].y, 2));
|
||||
|
||||
if (ddist2 < 150 && ddist3 < 150) {
|
||||
if (ddist2 < 160 && ddist3 < 160) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(nodes[i].x, nodes[i].y);
|
||||
ctx.lineTo(nodes[j].x, nodes[j].y);
|
||||
ctx.lineTo(nodes[k].x, nodes[k].y);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = alpha(nodes[i].color, 0.03);
|
||||
ctx.fillStyle = alpha(nodes[i].color, 0.06);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
@@ -159,10 +182,24 @@ const GenerativeBanner: React.FC<GenerativeBannerProps> = ({ username, palette,
|
||||
}
|
||||
}
|
||||
|
||||
// Draw nodes with high-visibility glow
|
||||
nodes.forEach(node => {
|
||||
// Core
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, node.size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = alpha(node.color, 0.4);
|
||||
ctx.fillStyle = alpha(node.color, 0.8);
|
||||
ctx.fill();
|
||||
|
||||
// Inner Glow
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, node.size * 2, 0, Math.PI * 2);
|
||||
ctx.fillStyle = alpha(node.color, 0.2);
|
||||
ctx.fill();
|
||||
|
||||
// Outer Atmosphere
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, node.size * 4, 0, Math.PI * 2);
|
||||
ctx.fillStyle = alpha(node.color, 0.05);
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
@@ -172,12 +209,11 @@ const GenerativeBanner: React.FC<GenerativeBannerProps> = ({ username, palette,
|
||||
draw();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resize);
|
||||
canvas.removeEventListener('mousemove', handleMouseMove);
|
||||
canvas.removeEventListener('touchmove', handleTouchMove);
|
||||
canvas.removeEventListener('mousedown', handleMouseDown);
|
||||
canvas.removeEventListener('mouseup', handleMouseUp);
|
||||
canvas.removeEventListener('touchend', handleMouseUp);
|
||||
resizeObserver.disconnect();
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('touchmove', handleTouchMove);
|
||||
window.removeEventListener('mousedown', handleMouseDown);
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, [username, palette, ratingsCount, followersCount, theme]);
|
||||
@@ -189,9 +225,9 @@ const GenerativeBanner: React.FC<GenerativeBannerProps> = ({ username, palette,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
pointerEvents: 'auto'
|
||||
pointerEvents: 'none',
|
||||
zIndex: 10,
|
||||
mixBlendMode: 'plus-lighter' // Makes it pop against dark backgrounds
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Avatar,
|
||||
Tabs,
|
||||
Tab,
|
||||
Paper,
|
||||
Grid,
|
||||
alpha,
|
||||
useTheme,
|
||||
@@ -26,6 +27,7 @@ import ShareIcon from '@mui/icons-material/Share';
|
||||
import CommentIcon from '@mui/icons-material/Comment';
|
||||
import VerifiedIcon from '@mui/icons-material/Verified';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import ColorThief from 'colorthief';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import api from '../api';
|
||||
@@ -33,6 +35,7 @@ import { useTitle } from '../hooks/useTitle';
|
||||
import RatingBadge from '../components/RatingBadge';
|
||||
import MentionTextField from '../components/MentionTextField';
|
||||
import RichText from '../components/RichText';
|
||||
import GenerativeBanner from '../components/GenerativeBanner';
|
||||
import { formatDate } from '../utils/date';
|
||||
|
||||
interface DashboardData {
|
||||
@@ -52,6 +55,7 @@ const Dashboard: React.FC = () => {
|
||||
const [data, setData] = useState<DashboardData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [palette, setPalette] = useState<string[]>([]);
|
||||
|
||||
// Filtering states for Explore New
|
||||
const [exploreCategory, setExploreCategory] = useState<string>('All');
|
||||
@@ -99,6 +103,42 @@ const Dashboard: React.FC = () => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.user?.avatar) {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.src = data.user.avatar;
|
||||
img.onload = () => {
|
||||
const colorThief = new ColorThief();
|
||||
try {
|
||||
const rawPalette = colorThief.getPalette(img, 10);
|
||||
const processed = rawPalette.map(c => {
|
||||
const r = c[0] / 255, g = c[1] / 255, b = c[2] / 255;
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
const l = (max + min) / 2;
|
||||
const s = max === min ? 0 : (l > 0.5 ? (max - min) / (2 - max - min) : (max - min) / (max + min));
|
||||
return { rgb: `rgb(${c[0]}, ${c[1]}, ${c[2]})`, l, s };
|
||||
});
|
||||
const vibrant = processed
|
||||
.filter(c => c.l > 0.2 && c.l < 0.85)
|
||||
.sort((a, b) => b.s - a.s);
|
||||
|
||||
if (vibrant.length >= 2) {
|
||||
setPalette([vibrant[0].rgb, vibrant[1].rgb]);
|
||||
} else if (vibrant.length === 1) {
|
||||
setPalette([vibrant[0].rgb, vibrant[0].rgb]);
|
||||
} else {
|
||||
setPalette([]);
|
||||
}
|
||||
} catch (e) {
|
||||
setPalette([]);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
setPalette([]);
|
||||
}
|
||||
}, [data?.user?.avatar]);
|
||||
|
||||
// Update Review Logic
|
||||
const handleUpdateRating = async (ratingId: number) => {
|
||||
try {
|
||||
@@ -210,39 +250,96 @@ const Dashboard: React.FC = () => {
|
||||
|
||||
{/* Header Profile Info */}
|
||||
<Card elevation={0} sx={{
|
||||
borderRadius: 4,
|
||||
borderRadius: 5,
|
||||
mb: 4,
|
||||
bgcolor: (theme) => alpha(theme.palette.background.paper, 0.6),
|
||||
backdropFilter: 'blur(12px)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '1px solid',
|
||||
borderColor: 'divider'
|
||||
borderColor: 'divider',
|
||||
overflow: 'hidden',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<CardContent sx={{ p: { xs: 2, sm: 4 } }}>
|
||||
<Grid container spacing={3} alignItems="center">
|
||||
<Grid size={{ xs: 12, sm: 'auto' }}>
|
||||
<Avatar src={data.user.avatar || undefined} sx={{ width: 100, height: 100, border: '4px solid', borderColor: 'primary.main' }}>
|
||||
<Box sx={{
|
||||
height: { xs: 100, sm: 140 },
|
||||
background: `
|
||||
radial-gradient(at 0% 0%, ${alpha(palette[0] || theme.palette.primary.main, 0.8)} 0px, transparent 55%),
|
||||
radial-gradient(at 100% 0%, ${alpha(palette[1] || theme.palette.secondary.main, 0.7)} 0px, transparent 55%),
|
||||
radial-gradient(at 50% 100%, ${alpha(theme.palette.primary.main, 0.4)} 0px, transparent 55%),
|
||||
linear-gradient(135deg, ${alpha(palette[0] || theme.palette.primary.main, 0.2)} 0%, ${alpha(palette[1] || theme.palette.secondary.main, 0.2)} 100%)
|
||||
`,
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<GenerativeBanner
|
||||
username={data.user.username}
|
||||
palette={palette}
|
||||
ratingsCount={data.rated_count}
|
||||
followersCount={0}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<CardContent sx={{ pt: 0, px: { xs: 2, sm: 4 }, pb: 4 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, alignItems: { xs: 'center', sm: 'flex-end' }, gap: { xs: 2, sm: 4 } }}>
|
||||
<Box sx={{
|
||||
position: 'relative',
|
||||
mt: { xs: -6, sm: -8 },
|
||||
p: 0.5,
|
||||
borderRadius: '50%',
|
||||
background: `linear-gradient(135deg, ${palette[0] || theme.palette.primary.main}, ${theme.palette.secondary.main})`,
|
||||
boxShadow: '0 12px 48px rgba(0,0,0,0.25)',
|
||||
display: 'flex'
|
||||
}}>
|
||||
<Avatar
|
||||
src={data.user.avatar || undefined}
|
||||
sx={{
|
||||
width: { xs: 100, sm: 120 },
|
||||
height: { xs: 100, sm: 120 },
|
||||
border: '4px solid',
|
||||
borderColor: (theme) => theme.palette.background.paper,
|
||||
fontSize: '3rem',
|
||||
bgcolor: (theme) => theme.palette.background.paper,
|
||||
color: palette[0] || 'primary.main',
|
||||
}}
|
||||
>
|
||||
{!data.user.avatar && data.user.username.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 8, md: 9 }}>
|
||||
<Typography variant="h3" sx={{ fontWeight: '900', mb: 0.5 }}>{data.user.username}</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: 1, textAlign: { xs: 'center', sm: 'left' }, mt: { xs: 1, sm: 4 } }}>
|
||||
<Typography variant="h3" sx={{ fontWeight: '900', mb: 0.5, letterSpacing: -1 }}>{data.user.username}</Typography>
|
||||
<Typography variant="subtitle1" color="text.secondary" gutterBottom>
|
||||
{t('dashboard.welcome', { username: data.user.username })}
|
||||
</Typography>
|
||||
|
||||
<Stack direction="row" spacing={3} sx={{ mt: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: '900', color: 'primary.main' }}>{data.rated_count}</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1 }}>{t('dashboard.myRatings')}</Typography>
|
||||
</Box>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: '900' }}>{data.missing_count}</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1 }}>{t('dashboard.missing')}</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
bgcolor: (theme: any) => alpha(theme.palette.text.primary, 0.04),
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
overflow: 'hidden',
|
||||
backdropFilter: 'blur(8px)'
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{ label: t('dashboard.myRatings'), val: data.rated_count },
|
||||
{ label: t('dashboard.missing'), val: data.missing_count }
|
||||
].map((stat, i) => (
|
||||
<React.Fragment key={stat.label}>
|
||||
<Box sx={{ py: 1, width: { xs: 100, sm: 140 }, textAlign: 'center' }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: '900', lineHeight: 1, color: 'text.primary' }}>{stat.val}</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: '900', textTransform: 'uppercase', opacity: 0.5, fontSize: '0.6rem', letterSpacing: 1 }}>{stat.label}</Typography>
|
||||
</Box>
|
||||
{i < 1 && <Divider orientation="vertical" flexItem sx={{ borderStyle: 'solid', opacity: 0.1 }} />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
Reference in new issue
Block a user