Skip to content

Commit b5b6d2e

Browse files
committed
feat: add explorer view with navbar tabs and content integration
1 parent 6cf1f57 commit b5b6d2e

File tree

5 files changed

+324
-9
lines changed

5 files changed

+324
-9
lines changed

src/layouts/explorer/explorer.tsx

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { DateProvider } from '@/context/date.context'
2+
import { useGetContents } from '@/services/hooks/content/get-content.hook'
3+
import { useRef, useState, useEffect } from 'react'
4+
import { NetworkLayout } from '../widgets/network/network.layout'
5+
import { ToolsLayout } from '../widgets/tools/tools.layout'
6+
import Analytics from '@/analytics'
7+
import { getFaviconFromUrl } from '@/common/utils/icon'
8+
9+
interface LinkItem {
10+
name: string
11+
url: string
12+
icon?: string
13+
badge?: string
14+
badgeColor?: string
15+
}
16+
17+
interface CategoryItem {
18+
id: string
19+
category: string
20+
banner?: string
21+
links: LinkItem[]
22+
icon?: string
23+
}
24+
25+
export function ExplorerContent() {
26+
const { data: catalogData } = useGetContents()
27+
const [activeCategory, setActiveCategory] = useState<string | null>(null)
28+
const categoryRefs = useRef<{ [key: string]: HTMLDivElement | null }>({})
29+
const scrollContainerRef = useRef<HTMLDivElement>(null)
30+
31+
useEffect(() => {
32+
const observerOptions = {
33+
root: scrollContainerRef.current,
34+
rootMargin: '-10% 0px -80% 0px',
35+
threshold: 0,
36+
}
37+
38+
const observerCallback = (entries: IntersectionObserverEntry[]) => {
39+
entries.forEach((entry) => {
40+
if (entry.isIntersecting) {
41+
setActiveCategory(entry.target.id)
42+
}
43+
})
44+
}
45+
46+
const observer = new IntersectionObserver(observerCallback, observerOptions)
47+
48+
Object.values(categoryRefs.current).forEach((div) => {
49+
if (div) observer.observe(div)
50+
})
51+
52+
return () => observer.disconnect()
53+
}, [catalogData])
54+
55+
const scrollToCategory = (id: string) => {
56+
categoryRefs.current[id]?.scrollIntoView({
57+
behavior: 'smooth',
58+
block: 'start',
59+
})
60+
Analytics.event('explorer_click_category')
61+
}
62+
63+
return (
64+
<div className="flex flex-col items-center flex-1 w-full h-screen gap-3 px-2 py-3 overflow-hidden md:px-6">
65+
<div className="grid w-full h-full grid-cols-1 gap-6 overflow-hidden lg:grid-cols-12">
66+
<div className="flex flex-col h-full gap-4 overflow-hidden lg:col-span-8">
67+
<div className="sticky top-0 z-10 flex items-center w-full gap-2 p-2 overflow-x-auto overflow-y-hidden shadow-sm bg-content bg-glass backdrop-blur-md h-14 rounded-2xl flex-nowrap scroll-smooth">
68+
{catalogData?.contents?.map((cat: CategoryItem) => (
69+
<button
70+
key={cat.id}
71+
onClick={() => scrollToCategory(cat.id)}
72+
className={`px-4 py-1.5 text-xs font-bold whitespace-nowrap rounded-xl transition-all duration-300 shrink-0 cursor-pointer ${
73+
activeCategory === cat.id
74+
? 'bg-primary text-white shadow-lg scale-105'
75+
: 'bg-base-200 hover:bg-base-300'
76+
}`}
77+
>
78+
{cat.category}
79+
</button>
80+
))}
81+
</div>
82+
83+
<div
84+
ref={scrollContainerRef}
85+
className="flex-1 pb-4 pr-1 space-y-6 overflow-y-auto scrollbar-none scroll-smooth"
86+
>
87+
{/* <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
88+
<div className="p-6 border shadow md:col-span-2 rounded-3xl bg-content bg-glass border-white/10">
89+
<h2 className="mb-2 text-lg font-bold">سوپرایزم کن</h2>
90+
<p className="text-sm opacity-80">
91+
92+
</p>
93+
<button className="mt-4 text-white transition-transform border-none shadow btn btn-sm bg-primary rounded-xl hover:scale-105">
94+
95+
</button>
96+
</div>
97+
<div className="flex flex-col gap-4">
98+
<div className="flex items-center justify-center flex-1 text-xs border opacity-50 bg-content bg-glass rounded-2xl border-base-300/50">
99+
100+
</div>
101+
<div className="flex items-center justify-center flex-1 text-xs border opacity-50 bg-content bg-glass rounded-2xl border-base-300/50">
102+
103+
</div>
104+
</div>
105+
</div> */}
106+
107+
<div className="flex flex-col gap-4 pb-10">
108+
{catalogData?.contents?.map((category: CategoryItem) => (
109+
<div
110+
key={category.id}
111+
id={category.id}
112+
ref={(el) => {
113+
categoryRefs.current[category.id] = el
114+
}}
115+
className="relative overflow-hidden border scroll-mt-4 bg-content bg-glass border-base-300 rounded-2xl"
116+
>
117+
{category.banner && (
118+
<div className="w-full overflow-hidden h-36">
119+
<img
120+
src={category.banner}
121+
className="object-cover w-full h-full transition-transform duration-1000 group-hover:scale-105"
122+
style={{
123+
maskImage:
124+
'linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.5) 70%, rgba(0, 0, 0, 0) 100%)',
125+
WebkitMaskImage:
126+
'linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.5) 70%, rgba(0, 0, 0, 0) 100%)',
127+
}}
128+
alt=""
129+
/>
130+
</div>
131+
)}
132+
133+
<div className="p-4">
134+
<div className="flex items-center gap-4 mb-8">
135+
<div className="flex items-center gap-2.5">
136+
{category.icon ? (
137+
<div className="relative flex items-center justify-center w-5 h-5 shrink-0">
138+
<img
139+
src={category.icon}
140+
className="relative z-10 object-contain w-full h-full transition-all duration-300 opacity-80 group-hover:opacity-100 group-hover:scale-110"
141+
alt=""
142+
/>
143+
</div>
144+
) : (
145+
<div className="w-1 h-3.5 rounded-full bg-primary/60 shadow-[0_0_8px_rgba(var(--p),0.4)] shrink-0" />
146+
)}
147+
148+
<h3 className="text-[11px] font-black tracking-[0.25em] uppercase opacity-40 group-hover:opacity-100 transition-opacity">
149+
{category.category}
150+
</h3>
151+
</div>
152+
<div className="flex-1 h-px bg-linear-to-r from-base-content/10 to-transparent" />
153+
</div>
154+
155+
<div className="grid grid-cols-4 sm:grid-cols-6 lg:grid-cols-7 gap-y-6 gap-x-2">
156+
{category.links?.map((link, index) => (
157+
<a
158+
key={index}
159+
href={getUrl(link.url)}
160+
target="_blank"
161+
rel="noopener noreferrer"
162+
className="flex flex-col items-center gap-3 transition-all duration-300 cursor-pointer group/item active:scale-95 hover:scale-105"
163+
>
164+
<div className="relative flex items-center justify-center w-12 h-12 transition-all duration-300 border border-content rounded-2xl group-hover/item:border-primary!">
165+
{link.badge && (
166+
<span
167+
className="absolute -top-1.5 -right-1.5 z-20 px-1.5 py-0.5 rounded-lg text-[8px] font-black uppercase tracking-tighter shadow-sm border border-white/10"
168+
style={{
169+
backgroundColor:
170+
link.badgeColor ||
171+
'var(--p)',
172+
color: '#fff',
173+
}}
174+
>
175+
{link.badge}
176+
</span>
177+
)}
178+
179+
<img
180+
src={
181+
link.icon ||
182+
getFaviconFromUrl(
183+
link.url
184+
)
185+
}
186+
className="object-contain w-6 h-6 transition-transform rounded-md group-hover/item:scale-110"
187+
alt={link.name}
188+
onError={(e) => {
189+
e.currentTarget.src =
190+
'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><text y="18" font-size="16">🌐</text></svg>'
191+
}}
192+
/>
193+
</div>
194+
195+
<span className="text-[10px] font-bold tracking-tighter text-center truncate w-full opacity-50 group-hover/item:opacity-100 transition-opacity">
196+
{link.name}
197+
</span>
198+
</a>
199+
))}
200+
</div>
201+
</div>
202+
</div>
203+
))}
204+
</div>
205+
</div>
206+
</div>
207+
<div className="hidden h-full pb-4 space-y-4 lg:block lg:col-span-4">
208+
<div className="sticky space-y-4 ">
209+
<DateProvider>
210+
<ToolsLayout />
211+
</DateProvider>
212+
<NetworkLayout />
213+
</div>
214+
</div>
215+
</div>
216+
</div>
217+
)
218+
}
219+
220+
function getUrl(url: string) {
221+
if (url.startsWith('http')) {
222+
return url
223+
} else {
224+
return `https://${url}`
225+
}
226+
}

src/layouts/navbar/navbar.layout.tsx

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,71 @@ import { SyncButton } from './sync/sync'
1313
import { useAppearanceSetting } from '@/context/appearance.context'
1414
import { MarketButton } from './market/market-button'
1515
import Analytics from '@/analytics'
16+
import { HiRectangleGroup } from 'react-icons/hi2'
1617

1718
const WIDGETIFY_URLS = {
1819
website: 'https://widgetify.ir',
1920
} as const
2021

22+
const tabs = [
23+
{
24+
id: 'explorer',
25+
icon: <HiRectangleGroup size={22} />,
26+
hasBadge: true,
27+
},
28+
{
29+
id: 'home',
30+
icon: <HiHome size={22} />,
31+
},
32+
]
33+
export function NavbarTabs() {
34+
const [activeTab, setActiveTab] = useState<string | null>('home')
35+
36+
const handleTabClick = (tab: string) => {
37+
setActiveTab(tab)
38+
if (tab === 'home') callEvent('closeJumpPage')
39+
else callEvent('openJumpPage')
40+
}
41+
42+
return (
43+
<div className="flex items-center gap-0.5">
44+
{tabs.map((tab) => (
45+
<button
46+
key={tab.id}
47+
onClick={() => handleTabClick(tab.id)}
48+
className="relative p-2 cursor-pointer group nav-btn"
49+
>
50+
<span
51+
className={`
52+
relative z-10 transition-all duration-300 block
53+
${activeTab === tab.id ? 'text-primary scale-110' : 'nav-btn text-white/20 hover:text-white/40'}
54+
`}
55+
>
56+
{tab.icon}
57+
58+
{tab.hasBadge && (
59+
<span className="absolute -top-0.5 -right-0.5 flex h-2 w-2">
60+
<span
61+
className={`animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75 ${activeTab === tab.id ? 'block' : 'hidden'}`}
62+
></span>
63+
<span
64+
className={`relative inline-flex rounded-full h-2 w-2 border border-black/50 ${activeTab === tab.id ? 'bg-primary' : 'bg-primary/80'}`}
65+
></span>
66+
</span>
67+
)}
68+
</span>
69+
70+
{activeTab === tab.id && (
71+
<div className="absolute bottom-0 left-0 right-0 h-1 bg-primary rounded-t-full shadow-[0_-4px_12px_rgba(var(--primary-rgb),0.8)]">
72+
<div className="absolute inset-0 bg-primary blur-[2px]" />
73+
</div>
74+
)}
75+
</button>
76+
))}
77+
</div>
78+
)
79+
}
80+
2181
export function NavbarLayout(): JSX.Element {
2282
const { canReOrderWidget, toggleCanReOrderWidget } = useAppearanceSetting()
2383
const [showSettings, setShowSettings] = useState(false)
@@ -124,12 +184,7 @@ export function NavbarLayout(): JSX.Element {
124184
<div className="relative z-10 w-[1px] h-6 bg-white/[0.08]" />
125185

126186
<div className="relative z-10 flex items-center gap-2 pr-1 ml-0.5">
127-
<button
128-
onClick={() => callEvent('closeJumpPage')}
129-
className="relative p-2 transition-all rounded-full text-white bg-primary shadow-[0_5px_15px_rgba(var(--primary-rgb),0.3)] active:scale-90 group"
130-
>
131-
<HiHome size={19} />
132-
</button>
187+
<NavbarTabs />
133188
<a
134189
href={WIDGETIFY_URLS.website}
135190
target="_blank"

src/pages/home.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { WidgetTabKeys } from '@/layouts/widgets-settings/constant/tab-keys
1515
import { WidgetSettingsModal } from '@/layouts/widgets-settings/widget-settings-modal'
1616
import { getRandomWallpaper } from '@/services/hooks/wallpapers/getWallpaperCategories.hook'
1717
import { ContentSection } from './home/content-section'
18+
import { ExplorerContent } from '@/layouts/explorer/explorer'
1819

1920
const steps: Step[] = [
2021
{
@@ -83,6 +84,7 @@ export function HomePage() {
8384
const [showWidgetSettings, setShowWidgetSettings] = useState(false)
8485
const [tab, setTab] = useState<string | null>(null)
8586
const [showTour, setShowTour] = useState(false)
87+
const [currentView, setCurrentView] = useState<'home' | 'explore'>('home')
8688

8789
useEffect(() => {
8890
async function displayModalIfNeeded() {
@@ -160,11 +162,21 @@ export function HomePage() {
160162
}
161163
)
162164

165+
const openJumpPageEvent = listenEvent('openJumpPage', () => {
166+
setCurrentView('catalog')
167+
})
168+
169+
const closeJumpPageEvent = listenEvent('closeJumpPage', () => {
170+
setCurrentView('home')
171+
})
172+
163173
Analytics.pageView('Home', '/')
164174

165175
return () => {
166176
wallpaperChangedEvent()
167177
openWidgetsSettingsEvent()
178+
openJumpPageEvent()
179+
closeJumpPageEvent()
168180
}
169181
}, [])
170182

@@ -254,7 +266,7 @@ export function HomePage() {
254266
<WidgetVisibilityProvider>
255267
<NavbarLayout />
256268

257-
<ContentSection />
269+
{currentView === 'home' ? <ContentSection /> : <ExplorerContent />}
258270
<WidgetSettingsModal
259271
isOpen={showWidgetSettings}
260272
onClose={() => {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { getMainClient } from '@/services/api'
2+
import { useQuery } from '@tanstack/react-query'
3+
4+
export interface FetchedContent {
5+
contents: {
6+
id: string
7+
category: string
8+
links: { name: string; url: string; icon?: string }[]
9+
}[]
10+
}
11+
12+
export const useGetContents = () => {
13+
return useQuery<FetchedContent>({
14+
queryKey: ['contents'],
15+
queryFn: async () => {
16+
const api = await getMainClient()
17+
18+
const { data } = await api.get<FetchedContent>('/contents/beta')
19+
return data
20+
},
21+
staleTime: 5 * 60 * 1000, // 5 minutes
22+
})
23+
}

src/services/hooks/todo/get-tags.hook.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ export const useGetTags = (enabled: boolean) => {
55
return useQuery<string[]>({
66
queryKey: ['getTags'],
77
queryFn: async () => getTags(),
8-
retry: 0,
8+
staleTime: 5 * 60 * 1000, // 5 minutes
99
enabled,
10-
initialData: [],
1110
})
1211
}
1312
export async function getTags(): Promise<string[]> {

0 commit comments

Comments
 (0)