Skip to content

Commit 782cd34

Browse files
committed
Move metadata below the description on mobile
1 parent 6db1108 commit 782cd34

7 files changed

Lines changed: 204 additions & 138 deletions

File tree

frontend/src/modules/core/components/Navbar.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -330,12 +330,12 @@ export const Navbar: React.FC<NavbarProps> = ({
330330
<div id="mobile-nav-menu" ref={mobileMenuRef} className="absolute top-24 left-0 right-0 bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-white/10 shadow-2xl p-4 flex flex-col gap-2 animate-in slide-in-from-top-2 z-50">
331331
<div className="space-y-1">
332332
<div className="px-3 py-2 text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-wider">Browse</div>
333-
<Link to={SiteRoutes.browse()} onClick={() => setIsMobileMenuOpen(false)} className="flex items-center px-5 py-2.5 rounded-lg hover:bg-slate-50 dark:hover:bg-white/5 font-bold text-slate-700 dark:text-slate-200 text-left text-sm"><Layout className="w-4 h-4 mr-3" /> Hytale Mods</Link>
334-
<Link to={SiteRoutes.browse('PLUGIN')} onClick={() => setIsMobileMenuOpen(false)} className="flex items-center px-5 py-2.5 rounded-lg hover:bg-slate-50 dark:hover:bg-white/5 font-bold text-slate-700 dark:text-slate-200 text-left text-sm"><FileCode className="w-4 h-4 mr-3" /> Hytale Plugins</Link>
335-
<Link to={SiteRoutes.browse('MODPACK')} onClick={() => setIsMobileMenuOpen(false)} className="flex items-center px-5 py-2.5 rounded-lg hover:bg-slate-50 dark:hover:bg-white/5 font-bold text-slate-700 dark:text-slate-200 text-left text-sm"><Layers className="w-4 h-4 mr-3" /> Hytale Modpacks</Link>
336-
<Link to={SiteRoutes.browse('SAVE')} onClick={() => setIsMobileMenuOpen(false)} className="flex items-center px-5 py-2.5 rounded-lg hover:bg-slate-50 dark:hover:bg-white/5 font-bold text-slate-700 dark:text-slate-200 text-left text-sm"><Save className="w-4 h-4 mr-3" /> Hytale Worlds</Link>
337-
<Link to={SiteRoutes.browse('ART')} onClick={() => setIsMobileMenuOpen(false)} className="flex items-center px-5 py-2.5 rounded-lg hover:bg-slate-50 dark:hover:bg-white/5 font-bold text-slate-700 dark:text-slate-200 text-left text-sm"><Palette className="w-4 h-4 mr-3" /> Hytale Art Assets</Link>
338-
<Link to={SiteRoutes.browse('DATA')} onClick={() => setIsMobileMenuOpen(false)} className="flex items-center px-5 py-2.5 rounded-lg hover:bg-slate-50 dark:hover:bg-white/5 font-bold text-slate-700 dark:text-slate-200 text-left text-sm"><Database className="w-4 h-4 mr-3" /> Hytale Data Assets</Link>
333+
<Link to={SiteRoutes.browse()} onClick={() => setIsMobileMenuOpen(false)} className="flex items-center px-5 py-2.5 rounded-lg hover:bg-slate-50 dark:hover:bg-white/5 font-bold text-slate-700 dark:text-slate-200 text-left text-sm"><Layout className="w-4 h-4 mr-3" /> Mods</Link>
334+
<Link to={SiteRoutes.browse('PLUGIN')} onClick={() => setIsMobileMenuOpen(false)} className="flex items-center px-5 py-2.5 rounded-lg hover:bg-slate-50 dark:hover:bg-white/5 font-bold text-slate-700 dark:text-slate-200 text-left text-sm"><FileCode className="w-4 h-4 mr-3" /> Plugins</Link>
335+
<Link to={SiteRoutes.browse('MODPACK')} onClick={() => setIsMobileMenuOpen(false)} className="flex items-center px-5 py-2.5 rounded-lg hover:bg-slate-50 dark:hover:bg-white/5 font-bold text-slate-700 dark:text-slate-200 text-left text-sm"><Layers className="w-4 h-4 mr-3" /> Modpacks</Link>
336+
<Link to={SiteRoutes.browse('SAVE')} onClick={() => setIsMobileMenuOpen(false)} className="flex items-center px-5 py-2.5 rounded-lg hover:bg-slate-50 dark:hover:bg-white/5 font-bold text-slate-700 dark:text-slate-200 text-left text-sm"><Save className="w-4 h-4 mr-3" /> Worlds</Link>
337+
<Link to={SiteRoutes.browse('ART')} onClick={() => setIsMobileMenuOpen(false)} className="flex items-center px-5 py-2.5 rounded-lg hover:bg-slate-50 dark:hover:bg-white/5 font-bold text-slate-700 dark:text-slate-200 text-left text-sm"><Palette className="w-4 h-4 mr-3" /> Art Assets</Link>
338+
<Link to={SiteRoutes.browse('DATA')} onClick={() => setIsMobileMenuOpen(false)} className="flex items-center px-5 py-2.5 rounded-lg hover:bg-slate-50 dark:hover:bg-white/5 font-bold text-slate-700 dark:text-slate-200 text-left text-sm"><Database className="w-4 h-4 mr-3" /> Data Assets</Link>
339339
</div>
340340

341341
<div className="h-px bg-slate-100 dark:bg-white/5 my-2"></div>

frontend/src/modules/project/components/ActionBar.tsx

Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,29 @@
11
import React, { useRef, useState, useEffect } from 'react';
22
import { Link, useLocation } from 'react-router-dom';
3-
import { Download, BookOpen, Image as ImageIcon, List, MessageSquare, Box, ChevronDown, ExternalLink, Link as LinkIcon } from 'lucide-react';
3+
import { Download, BookOpen, Image as ImageIcon, List, MessageSquare, ChevronDown, ExternalLink, Link as LinkIcon } from 'lucide-react';
44
import { theme } from '@/styles/theme';
5-
import { OptimizedImage } from '@/components/ui/OptimizedImage';
6-
import { SiteRoutes } from '@/utils/routes';
7-
import { BACKEND_URL } from '@/utils/api';
8-
import type { Project, ProjectDependency } from '@/types';
5+
import type { Project } from '@/types';
96

107
interface ActionBarProps {
118
project: Project;
129
projectUrl: string;
13-
latestDependencies: ProjectDependency[];
14-
depMeta: Record<string, { icon: string, title: string }>;
1510
links: { type: string, url: string, icon: any, label: string, colorClass: string }[];
16-
isMobile: boolean;
1711
commentsRef: React.RefObject<HTMLDivElement | null>;
1812
}
1913

20-
export const ActionBar: React.FC<ActionBarProps> = ({ project, projectUrl, latestDependencies, depMeta, links, isMobile, commentsRef }) => {
14+
export const ActionBar: React.FC<ActionBarProps> = ({ project, projectUrl, links, commentsRef }) => {
2115
const [showMobileLinks, setShowMobileLinks] = useState(false);
22-
const [showMobileDeps, setShowMobileDeps] = useState(false);
2316
const dropdownRef = useRef<HTMLDivElement>(null);
24-
const depsDropdownRef = useRef<HTMLDivElement>(null);
2517
const location = useLocation();
2618

2719
useEffect(() => {
2820
const handleClick = (e: MouseEvent) => {
2921
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) setShowMobileLinks(false);
30-
if (depsDropdownRef.current && !depsDropdownRef.current.contains(e.target as Node)) setShowMobileDeps(false);
3122
};
3223
document.addEventListener('mousedown', handleClick);
3324
return () => document.removeEventListener('mousedown', handleClick);
3425
}, []);
3526

36-
const resolveUrl = (url: string) => url.startsWith('/api') ? `${BACKEND_URL}${url}` : url;
3727
const hasWiki = Boolean(project.hmWikiEnabled && project.hmWikiSlug);
3828
const hasGallery = Boolean(project.galleryImages && project.galleryImages.length > 0);
3929
const mediaButtonClass = hasWiki && hasGallery ? 'col-span-1 md:col-span-1' : 'col-span-2 md:col-span-1';
@@ -74,36 +64,6 @@ export const ActionBar: React.FC<ActionBarProps> = ({ project, projectUrl, lates
7464
</div>
7565

7666
<div className="w-full 2xl:w-auto flex flex-col md:flex-row justify-start 2xl:justify-end gap-2 mt-1 2xl:mt-0">
77-
{isMobile && project.classification === 'MODPACK' && latestDependencies.length > 0 && (
78-
<div className="relative w-full" ref={depsDropdownRef}>
79-
<button onClick={() => { setShowMobileDeps(!showMobileDeps); setShowMobileLinks(false); }} className={`w-full flex items-center justify-center gap-2 p-3 rounded-xl ${theme.colors.bgSurfaceAlt} border ${theme.colors.border} font-bold text-slate-700 dark:text-slate-300`}>
80-
<Box className="w-4 h-4" aria-hidden="true" /> Included Projects <ChevronDown className={`w-4 h-4 transition-transform ${showMobileDeps ? 'rotate-180' : ''}`} aria-hidden="true" />
81-
</button>
82-
{showMobileDeps && (
83-
<div className={`absolute top-full left-0 right-0 mt-1.5 ${theme.colors.bgBase} border ${theme.colors.border} rounded-xl shadow-xl z-50 overflow-hidden animate-in fade-in slide-in-from-top-2 p-1 max-h-[300px] overflow-y-auto`}>
84-
{latestDependencies.map((dep, idx) => {
85-
const meta = depMeta[dep.projectId];
86-
const title = meta?.title || dep.projectTitle || dep.projectId;
87-
const path = SiteRoutes.project({ id: dep.projectId, title: title });
88-
89-
return (
90-
<Link key={idx} to={path} onClick={() => setShowMobileDeps(false)} className={`w-full flex items-center gap-3 p-2.5 ${theme.colors.bgSurfaceHover} transition-colors ${theme.colors.textSecondary} hover:${theme.colors.textPrimary} text-left`}>
91-
<div className={`w-8 h-8 rounded-lg ${theme.colors.bgSurfaceAlt} flex items-center justify-center border ${theme.colors.border} shrink-0 overflow-hidden`}>
92-
{meta?.icon ? <OptimizedImage src={resolveUrl(meta.icon)} alt={`${title} Icon`} baseWidth={32} className="w-full h-full" /> : <Box className="w-4 h-4 text-slate-500" aria-hidden="true" />}
93-
</div>
94-
<div className="min-w-0 flex-1">
95-
<div className="text-sm font-bold truncate">{title}</div>
96-
<div className="text-[10px] text-slate-500 dark:text-slate-400 font-mono">v{dep.versionNumber}</div>
97-
</div>
98-
<ExternalLink className="w-3 h-3 opacity-50 shrink-0" aria-hidden="true" />
99-
</Link>
100-
);
101-
})}
102-
</div>
103-
)}
104-
</div>
105-
)}
106-
10767
<div className="hidden md:flex gap-2 flex-wrap justify-start 2xl:justify-end">
10868
{links.map((link, idx) => (
10969
<a key={idx} href={link.url.startsWith('http') ? link.url : `https://${link.url}`} target="_blank" rel="noreferrer" className={`rounded-xl border transition-all flex items-center justify-center px-4 py-2.5 2xl:p-0 2xl:w-[42px] 2xl:h-[42px] gap-2 2xl:gap-0 ${link.colorClass}`} title={link.label} aria-label={link.label}>
@@ -115,7 +75,7 @@ export const ActionBar: React.FC<ActionBarProps> = ({ project, projectUrl, lates
11575

11676
{links.length > 0 && (
11777
<div className="md:hidden relative w-full" ref={dropdownRef}>
118-
<button onClick={() => { setShowMobileLinks(!showMobileLinks); setShowMobileDeps(false); }} className={`w-full flex items-center justify-center gap-2 p-3 rounded-xl ${theme.colors.bgSurfaceAlt} border ${theme.colors.border} font-bold text-slate-700 dark:text-slate-300`}>
78+
<button onClick={() => { setShowMobileLinks(!showMobileLinks); }} className={`w-full flex items-center justify-center gap-2 p-3 rounded-xl ${theme.colors.bgSurfaceAlt} border ${theme.colors.border} font-bold text-slate-700 dark:text-slate-300`}>
11979
<LinkIcon className="w-4 h-4" aria-hidden="true" /> External Links <ChevronDown className={`w-4 h-4 transition-transform ${showMobileLinks ? 'rotate-180' : ''}`} aria-hidden="true" />
12080
</button>
12181
{showMobileLinks && (
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import React from 'react';
2+
import { Link } from 'react-router-dom';
3+
import { Box, Gamepad2, Link as LinkIcon, Tag } from 'lucide-react';
4+
5+
import { SidebarSection } from '@/modules/project/components/ProjectLayout';
6+
import { OptimizedImage } from '@/components/ui/OptimizedImage';
7+
import { SiteRoutes } from '@/utils/routes';
8+
import { BACKEND_URL } from '@/utils/api';
9+
import type { Project, ProjectDependency } from '@/types';
10+
11+
interface ProjectMetaSectionsProps {
12+
project: Project;
13+
dependencies?: ProjectDependency[];
14+
depMeta: Record<string, { icon: string; title: string; classification?: string; slug?: string }>;
15+
}
16+
17+
export const ProjectMetaSections: React.FC<ProjectMetaSectionsProps> = React.memo(({
18+
project,
19+
dependencies,
20+
depMeta
21+
}) => {
22+
const gameVersions = React.useMemo(() => {
23+
const set = new Set<string>();
24+
(project.versions || []).forEach(v => v.gameVersions?.forEach(gv => set.add(gv)));
25+
return Array.from(set).sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
26+
}, [project.versions]);
27+
28+
const isModpack = project.classification === 'MODPACK';
29+
const getIconUrl = (path?: string) => path ? (path.startsWith('http') ? path : `${BACKEND_URL}${path}`) : null;
30+
31+
return (
32+
<>
33+
{gameVersions.length > 0 && (
34+
<SidebarSection title="Supported Versions" icon={Gamepad2}>
35+
<div className="flex flex-wrap gap-2">
36+
{gameVersions.map(v => (
37+
<span key={v} className="px-2.5 py-1 bg-slate-100 dark:bg-white/5 border border-slate-200 dark:border-white/5 rounded-md text-[10px] font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wide">
38+
{v}
39+
</span>
40+
))}
41+
</div>
42+
</SidebarSection>
43+
)}
44+
45+
<SidebarSection title="Tags" icon={Tag}>
46+
<div className="flex flex-wrap gap-2">
47+
{project.tags?.map((tag) => (
48+
<span key={tag} className="px-3 py-1.5 bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-white/10 rounded-lg text-xs font-bold text-slate-700 dark:text-slate-300 hover:text-modtale-accent hover:border-modtale-accent transition-all cursor-default">
49+
{tag}
50+
</span>
51+
))}
52+
{(!project.tags || project.tags.length === 0) && <span className="text-xs text-slate-500 italic">No tags.</span>}
53+
</div>
54+
</SidebarSection>
55+
56+
{dependencies && dependencies.length > 0 && (
57+
<SidebarSection title={isModpack ? "Included Projects" : "Dependencies"} icon={isModpack ? Box : LinkIcon}>
58+
<div className="space-y-2">
59+
{dependencies.map((dep, idx) => {
60+
const meta = depMeta[dep.projectId];
61+
const iconUrl = getIconUrl(meta?.icon);
62+
const title = meta?.title || dep.projectTitle || dep.projectId;
63+
64+
const targetProjectParams = {
65+
id: dep.projectId,
66+
title: title,
67+
slug: meta?.slug,
68+
classification: meta?.classification
69+
};
70+
71+
const path = SiteRoutes.project(targetProjectParams);
72+
73+
return (
74+
<Link
75+
key={idx}
76+
to={path}
77+
className="w-full flex items-center gap-3 p-3 rounded-xl bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-white/5 hover:border-modtale-accent/50 hover:shadow-md transition-all group text-left block"
78+
>
79+
<div className="w-8 h-8 rounded-lg bg-slate-100 dark:bg-black/20 flex items-center justify-center text-slate-400 group-hover:text-modtale-accent transition-colors overflow-hidden shrink-0">
80+
{iconUrl ? (
81+
<OptimizedImage
82+
src={iconUrl}
83+
alt={`${title} Icon`}
84+
baseWidth={32}
85+
className="w-full h-full"
86+
/>
87+
) : <Box className="w-4 h-4" aria-hidden="true" />}
88+
</div>
89+
<div className="min-w-0">
90+
<div className="text-xs font-bold text-slate-800 dark:text-slate-200 group-hover:text-modtale-accent truncate">{title}</div>
91+
<div className="text-[10px] text-slate-600 dark:text-slate-400 flex items-center gap-2">
92+
{!isModpack && <span className={dep.isOptional ? '' : 'text-amber-600 dark:text-amber-500 font-bold'}>{dep.isOptional ? 'Optional' : 'Required'}</span>}
93+
{dep.isEmbedded && <span className="text-emerald-600 dark:text-emerald-400 font-bold">Embedded</span>}
94+
<span className="font-mono opacity-75">v{dep.versionNumber}</span>
95+
</div>
96+
</div>
97+
</Link>
98+
);
99+
})}
100+
</div>
101+
</SidebarSection>
102+
)}
103+
</>
104+
);
105+
});
106+
107+
ProjectMetaSections.displayName = 'ProjectMetaSections';

0 commit comments

Comments
 (0)