Skip to content

Commit 493bd8c

Browse files
committed
feat: reorderable home page.
1 parent 6a182dc commit 493bd8c

File tree

7 files changed

+233
-126
lines changed

7 files changed

+233
-126
lines changed

public/roadmap/roadmap.piml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
(description) Integrate a cloud provider and add support for backend for Fezcodex.
1616
(category) Infrastructure
1717
(epic) Core Infrastructure
18-
(status) Planned
18+
(status) On Hold
1919
(priority) Medium
2020
(created_at) 2025-11-29T10:00:00+03:00
2121
(due_date) 2026-01-15T00:00:00+03:00
@@ -146,3 +146,14 @@
146146
(created_at) 2025-12-12T08:34:00+03:00
147147
(notes) More modern sidebar that fits well with the navbar as well
148148

149+
> (issues)
150+
(id) FEZ-14
151+
(title) Reorderable Homepage
152+
(description)
153+
There are two main parts in home page. Pinned Projects and Latest Blogposts part.
154+
In settings users should be able to change the order of this two components.
155+
(category) Feature
156+
(status) Completed
157+
(priority) High
158+
(created_at) 2025-12-12T08:34:00+03:00
159+
(notes) We assume explore section is also inside latest blogpost.

src/App.js

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import ScrollToTop from './components/ScrollToTop';
77
import ContactModal from './components/ContactModal';
88
import GenericModal from './components/GenericModal';
99
import DigitalRain from './components/DigitalRain';
10-
import BSOD from './components/BSOD'; // Import BSOD
11-
import { AnimationProvider } from './context/AnimationContext'; // Import AnimationProvider
10+
import BSOD from './components/BSOD';
11+
import { AnimationProvider } from './context/AnimationContext';
1212
import { CommandPaletteProvider } from './context/CommandPaletteContext';
1313
import { VisualSettingsProvider } from './context/VisualSettingsContext';
1414
import { AchievementProvider } from './context/AchievementContext';
1515
import AchievementListeners from './components/AchievementListeners';
1616
import { SidePanelProvider } from './context/SidePanelContext';
17+
import { HomepageOrderProvider } from './context/HomepageOrderContext';
1718

1819
function App() {
1920
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -56,33 +57,35 @@ function App() {
5657
<ToastProvider>
5758
<AchievementProvider>
5859
<AchievementListeners />
59-
<VisualSettingsProvider>
60-
<DigitalRain isActive={isRainActive} />
61-
<BSOD isActive={isBSODActive} toggleBSOD={toggleBSOD} />
62-
<ScrollToTop />
63-
<CommandPaletteProvider>
64-
<SidePanelProvider>
65-
<Layout
66-
toggleModal={toggleModal}
67-
isSearchVisible={isSearchVisible}
68-
toggleSearch={toggleSearch}
69-
openGenericModal={openGenericModal}
70-
toggleDigitalRain={toggleDigitalRain}
71-
toggleBSOD={toggleBSOD}
72-
>
73-
<AnimatedRoutes />
74-
</Layout>
75-
</SidePanelProvider>
76-
</CommandPaletteProvider>
77-
<ContactModal isOpen={isModalOpen} onClose={toggleModal} />
78-
<GenericModal
79-
isOpen={isGenericModalOpen}
80-
onClose={closeGenericModal}
81-
title={genericModalContent.title}
82-
>
83-
{genericModalContent.content}
84-
</GenericModal>
85-
</VisualSettingsProvider>
60+
<HomepageOrderProvider>
61+
<VisualSettingsProvider>
62+
<DigitalRain isActive={isRainActive} />
63+
<BSOD isActive={isBSODActive} toggleBSOD={toggleBSOD} />
64+
<ScrollToTop />
65+
<CommandPaletteProvider>
66+
<SidePanelProvider>
67+
<Layout
68+
toggleModal={toggleModal}
69+
isSearchVisible={isSearchVisible}
70+
toggleSearch={toggleSearch}
71+
openGenericModal={openGenericModal}
72+
toggleDigitalRain={toggleDigitalRain}
73+
toggleBSOD={toggleBSOD}
74+
>
75+
<AnimatedRoutes />
76+
</Layout>
77+
</SidePanelProvider>
78+
</CommandPaletteProvider>
79+
<ContactModal isOpen={isModalOpen} onClose={toggleModal} />
80+
<GenericModal
81+
isOpen={isGenericModalOpen}
82+
onClose={closeGenericModal}
83+
title={genericModalContent.title}
84+
>
85+
{genericModalContent.content}
86+
</GenericModal>
87+
</VisualSettingsProvider>
88+
</HomepageOrderProvider>
8689
</AchievementProvider>
8790
</ToastProvider>
8891
</Router>

src/components/Navbar.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const Navbar = ({
4545
</button>
4646
<Link to="/" className="flex items-center space-x-2">
4747
<Fez />
48-
<span className="text-2xl font-semibold tracking-tight">
48+
<span className="text-2xl tracking-tight font-mono">
4949
fez<span className="text-primary-400">codex</span>
5050
</span>
5151
</Link>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React, { createContext, useState, useEffect, useContext } from 'react';
2+
import {
3+
set as setLocalStorageItem,
4+
get as getLocalStorageItem,
5+
} from '../utils/LocalStorageManager';
6+
import { KEY_HOMEPAGE_SECTION_ORDER } from '../utils/LocalStorageManager';
7+
8+
const HomepageOrderContext = createContext();
9+
10+
const defaultSectionOrder = ['projects', 'blogposts']; // Default: Pinned Projects first, then Latest Blogposts
11+
12+
export const HomepageOrderProvider = ({ children }) => {
13+
const [sectionOrder, setSectionOrder] = useState(() => {
14+
return getLocalStorageItem(KEY_HOMEPAGE_SECTION_ORDER, defaultSectionOrder);
15+
});
16+
17+
useEffect(() => {
18+
setLocalStorageItem(KEY_HOMEPAGE_SECTION_ORDER, sectionOrder);
19+
}, [sectionOrder]);
20+
21+
const toggleSectionOrder = () => {
22+
setSectionOrder((prevOrder) => {
23+
// If current is default, switch to reversed, else switch to default
24+
if (
25+
prevOrder[0] === defaultSectionOrder[0] &&
26+
prevOrder[1] === defaultSectionOrder[1]
27+
) {
28+
return ['blogposts', 'projects']; // Reversed order
29+
} else {
30+
return defaultSectionOrder; // Default order
31+
}
32+
});
33+
};
34+
35+
const resetSectionOrder = () => {
36+
setSectionOrder(defaultSectionOrder);
37+
};
38+
39+
return (
40+
<HomepageOrderContext.Provider
41+
value={{
42+
sectionOrder,
43+
toggleSectionOrder,
44+
resetSectionOrder,
45+
}}
46+
>
47+
{children}
48+
</HomepageOrderContext.Provider>
49+
);
50+
};
51+
52+
export const useHomepageOrder = () => useContext(HomepageOrderContext);

src/pages/HomePage.js

Lines changed: 100 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,8 @@ import PostItem from '../components/PostItem';
1515
import ProjectCard from '../components/ProjectCard';
1616
import { useProjects } from '../utils/projectParser';
1717
import useSeo from '../hooks/useSeo';
18-
19-
const HeroButton = ({ to, children, Icon }) => {
20-
return (
21-
<Link
22-
to={to}
23-
className="relative flex items-center justify-center px-6 py-3 overflow-hidden font-mono text-sm font-bold text-white transition-all duration-300 ease-in-out bg-transparent border border-primary-500 rounded-md shadow-lg group hover:scale-105 hover:border-primary-400"
24-
>
25-
<span className="absolute inset-0 w-full h-full bg-primary-600 rounded-md opacity-0 group-hover:opacity-20 transition-opacity duration-300"></span>
26-
<span className="relative flex items-center gap-2 z-10 text-primary-300 group-hover:text-white transition-colors">
27-
{Icon && <Icon size={20} className="transition-transform group-hover:translate-x-1" />}
28-
{children}
29-
</span>
30-
<span className="absolute inset-0 rounded-md shadow-[0_0_15px_rgba(6,182,212,0)] group-hover:shadow-[0_0_15px_rgba(6,182,212,0.6)] transition-shadow duration-300"></span>
31-
</Link>
32-
);
33-
};
18+
import usePersistentState from '../hooks/usePersistentState';
19+
import { KEY_HOMEPAGE_SECTION_ORDER } from '../utils/LocalStorageManager';
3420

3521
const Hero = () => {
3622
const [currentDateTime, setCurrentDateTime] = useState('');
@@ -150,6 +136,9 @@ const HomePage = () => {
150136
const [loadingPosts, setLoadingPosts] = useState(true);
151137
const { projects: pinnedProjects, loading: loadingProjects } = useProjects(true);
152138

139+
// Use persistent state for homepage section order
140+
const [homepageSectionOrder] = usePersistentState(KEY_HOMEPAGE_SECTION_ORDER, ['projects', 'blogposts']);
141+
153142
useEffect(() => {
154143
const fetchPostSlugs = async () => {
155144
try {
@@ -194,90 +183,106 @@ const HomePage = () => {
194183
);
195184
}
196185

186+
// Helper function to render a section
187+
const renderSection = (sectionName) => {
188+
switch (sectionName) {
189+
case 'projects':
190+
return (
191+
<section className="mb-24 mt-8">
192+
<SectionHeader icon={Cpu} title="Pinned Projects" link="/projects" linkText="View all projects" />
193+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
194+
{pinnedProjects.map((project, index) => (
195+
<motion.div
196+
key={project.slug}
197+
initial={{ opacity: 0, y: 20 }}
198+
whileInView={{ opacity: 1, y: 0 }}
199+
transition={{ delay: index * 0.1 }}
200+
viewport={{ once: true }}
201+
>
202+
<ProjectCard project={{ ...project, description: project.shortDescription }} />
203+
</motion.div>
204+
))}
205+
</div>
206+
</section>
207+
);
208+
case 'blogposts':
209+
return (
210+
<section className="grid grid-cols-1 lg:grid-cols-12 gap-12 mt-8">
211+
{/* Main Feed */}
212+
<div className="lg:col-span-8">
213+
<SectionHeader icon={Article} title="Latest Blogposts" link="/blog" linkText="Read archive" />
214+
<div className="space-y-4">
215+
{posts.slice(0, 5).map((item, index) => (
216+
<motion.div
217+
key={item.slug}
218+
initial={{ opacity: 0, x: -20 }}
219+
whileInView={{ opacity: 1, x: 0 }}
220+
transition={{ delay: index * 0.05 }}
221+
viewport={{ once: true }}
222+
>
223+
{item.isSeries ? (
224+
<PostItem
225+
slug={`series/${item.slug}`}
226+
title={item.title}
227+
date={item.date}
228+
updatedDate={item.updated}
229+
category="series"
230+
isSeries={true}
231+
/>
232+
) : (
233+
<PostItem
234+
slug={item.slug}
235+
title={item.title}
236+
date={item.date}
237+
updatedDate={item.updated}
238+
category={item.category}
239+
series={item.series}
240+
seriesIndex={item.seriesIndex}
241+
/>
242+
)}
243+
</motion.div>
244+
))}
245+
</div>
246+
</div>
247+
248+
{/* Side Widgets */}
249+
<div className="lg:col-span-4 space-y-4 lg:pt-14 sm:pb-14">
250+
<ExploreLinkCard
251+
to="/apps"
252+
title="Explore Apps"
253+
description="Discover a collection of custom-built web applications and tools."
254+
Icon={AppWindow}
255+
/>
256+
<ExploreLinkCard
257+
to="/roadmap"
258+
title="Explore Fezzilla"
259+
description="Dive into the roadmap and development progress of Fezcodex."
260+
Icon={Cube}
261+
/>
262+
<ExploreLinkCard
263+
to="/commands"
264+
title="Explore Commands"
265+
description="Learn about available commands and system interactions."
266+
Icon={Terminal}
267+
/>
268+
</div>
269+
</section>
270+
);
271+
default:
272+
return null;
273+
}
274+
};
275+
197276
return (
198277
<div className="min-h-screen bg-[#0a0a0a]">
199278
<Hero />
200279

201280
<div className="mx-auto max-w-7xl px-6 lg:px-8 pb-24">
202-
{/* Featured Projects Section */}
203-
<section className="mb-24">
204-
<SectionHeader icon={Cpu} title="Pinned Projects" link="/projects" linkText="View all projects" />
205-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
206-
{pinnedProjects.map((project, index) => (
207-
<motion.div
208-
key={project.slug}
209-
initial={{ opacity: 0, y: 20 }}
210-
whileInView={{ opacity: 1, y: 0 }}
211-
transition={{ delay: index * 0.1 }}
212-
viewport={{ once: true }}
213-
>
214-
<ProjectCard project={{ ...project, description: project.shortDescription }} />
215-
</motion.div>
216-
))}
217-
</div>
218-
</section>
219-
220-
{/* Latest Log/Blog Section */}
221-
<section className="grid grid-cols-1 lg:grid-cols-12 gap-12">
222-
{/* Main Feed */}
223-
<div className="lg:col-span-8">
224-
<SectionHeader icon={Article} title="Latest Blogposts" link="/blog" linkText="Read archive" />
225-
<div className="space-y-4">
226-
{posts.slice(0, 5).map((item, index) => (
227-
<motion.div
228-
key={item.slug}
229-
initial={{ opacity: 0, x: -20 }}
230-
whileInView={{ opacity: 1, x: 0 }}
231-
transition={{ delay: index * 0.05 }}
232-
viewport={{ once: true }}
233-
>
234-
{item.isSeries ? (
235-
<PostItem
236-
slug={`series/${item.slug}`}
237-
title={item.title}
238-
date={item.date}
239-
updatedDate={item.updated}
240-
category="series"
241-
isSeries={true}
242-
/>
243-
) : (
244-
<PostItem
245-
slug={item.slug}
246-
title={item.title}
247-
date={item.date}
248-
updatedDate={item.updated}
249-
category={item.category}
250-
series={item.series}
251-
seriesIndex={item.seriesIndex}
252-
/>
253-
)}
254-
</motion.div>
255-
))}
256-
</div>
257-
</div>
258-
259-
{/* Side Widgets */}
260-
<div className="lg:col-span-4 space-y-4">
261-
<ExploreLinkCard
262-
to="/apps"
263-
title="Explore Apps"
264-
description="Discover a collection of custom-built web applications and tools."
265-
Icon={AppWindow}
266-
/>
267-
<ExploreLinkCard
268-
to="/roadmap"
269-
title="Explore Fezzilla"
270-
description="Dive into the roadmap and development progress of Fezcodex."
271-
Icon={Cube}
272-
/>
273-
<ExploreLinkCard
274-
to="/commands"
275-
title="Explore Commands"
276-
description="Learn about available commands and system interactions."
277-
Icon={Terminal}
278-
/>
279-
</div>
280-
</section>
281+
{homepageSectionOrder.map((sectionName) => (
282+
<React.Fragment key={sectionName}>
283+
{renderSection(sectionName)}
284+
</React.Fragment>
285+
))}
281286
</div>
282287
</div>
283288
);

0 commit comments

Comments
 (0)