Skip to content

Commit 303c4c2

Browse files
authored
Merge pull request #376 from widgetify-app/feat/add-explorer-view
Feat/add explorer view
2 parents 6cf1f57 + 19b480d commit 303c4c2

9 files changed

Lines changed: 391 additions & 18 deletions

File tree

src/common/utils/call-event.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ export interface EventName {
3131
name: string
3232
template: string
3333
}
34-
openJumpPage: null
35-
closeJumpPage: null
34+
openExplorerPage: null
35+
closeExplorerPage: null
3636

3737
// setting keys
3838
wigiPadDateSettingsChanged: WigiPadDateSetting

src/layouts/explorer/explorer.tsx

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
import { useGetContents } from '@/services/hooks/content/get-content.hook'
2+
import { useRef, useState, useEffect } from 'react'
3+
import Analytics from '@/analytics'
4+
import { getFaviconFromUrl } from '@/common/utils/icon'
5+
import { HiOutlineLightBulb } from 'react-icons/hi2'
6+
7+
interface LinkItem {
8+
name: string
9+
url: string
10+
icon?: string
11+
badge?: string
12+
badgeColor?: string
13+
}
14+
15+
interface CategoryItem {
16+
id: string
17+
category: string
18+
banner?: string
19+
links: LinkItem[]
20+
icon?: string
21+
}
22+
23+
export function ExplorerContent() {
24+
const { data: catalogData } = useGetContents()
25+
const [activeCategory, setActiveCategory] = useState<string | null>(null)
26+
const categoryRefs = useRef<{ [key: string]: HTMLDivElement | null }>({})
27+
const scrollContainerRef = useRef<HTMLDivElement>(null)
28+
29+
useEffect(() => {
30+
const observerOptions = {
31+
root: scrollContainerRef.current,
32+
rootMargin: '-10% 0px -80% 0px',
33+
threshold: 0,
34+
}
35+
36+
const observerCallback = (entries: IntersectionObserverEntry[]) => {
37+
entries.forEach((entry) => {
38+
if (entry.isIntersecting) {
39+
setActiveCategory(entry.target.id)
40+
}
41+
})
42+
}
43+
44+
const observer = new IntersectionObserver(observerCallback, observerOptions)
45+
Object.values(categoryRefs.current).forEach((div) => {
46+
if (div) observer.observe(div)
47+
})
48+
return () => observer.disconnect()
49+
}, [catalogData])
50+
51+
const scrollToCategory = (id: string) => {
52+
setActiveCategory(id)
53+
categoryRefs.current[id]?.scrollIntoView({
54+
behavior: 'smooth',
55+
block: 'start',
56+
})
57+
Analytics.event('explorer_click_category')
58+
}
59+
60+
return (
61+
<div className="flex flex-row w-full h-screen overflow-hidden">
62+
<aside className="flex-col items-center hidden w-24 gap-3 py-2 md:flex border-base-300 bg-content bg-glass lg:mt-3 rounded-t-2xl rounded-b-2xl h-[calc(100vh-10rem)]">
63+
<div className="flex flex-col items-center w-full gap-3 px-2 overflow-x-hidden overflow-y-auto scrollbar-none">
64+
{catalogData?.contents?.map((cat: CategoryItem) => (
65+
<button
66+
key={cat.id}
67+
onClick={() => scrollToCategory(cat.id)}
68+
className={`relative group flex flex-col items-center justify-center w-full py-3 px-2 rounded-2xl transition-all duration-300 cursor-pointer ${
69+
activeCategory === cat.id
70+
? 'bg-primary/80 text-white shadow-sm shadow-primary/20'
71+
: 'bg-base-200/60 hover:bg-base-300 text-base-content/70 hover:scale-102'
72+
}`}
73+
>
74+
{activeCategory === cat.id && (
75+
<div className="absolute w-1 h-10 rounded-r-full -left-2 bg-primary" />
76+
)}
77+
78+
{cat.icon ? (
79+
<img
80+
src={cat.icon}
81+
className={`w-8 h-8 mb-2 object-contain transition-transform ${
82+
activeCategory === cat.id
83+
? 'scale-110'
84+
: 'opacity-70 group-hover:opacity-100'
85+
}`}
86+
alt=""
87+
/>
88+
) : (
89+
<div
90+
className={`w-8 h-8 mb-2 rounded-xl flex items-center justify-center text-lg font-bold transition-all ${
91+
activeCategory === cat.id
92+
? 'bg-white/20'
93+
: 'bg-base-content/10 group-hover:bg-base-content/20'
94+
}`}
95+
>
96+
{cat.category.substring(0, 1)}
97+
</div>
98+
)}
99+
100+
<span
101+
className={`text-[9px] font-bold text-center leading-tight transition-all line-clamp-2 ${
102+
activeCategory === cat.id
103+
? 'opacity-100'
104+
: 'opacity-60 group-hover:opacity-90'
105+
}`}
106+
>
107+
{cat.category}
108+
</span>
109+
110+
<div className="absolute z-50 px-3 py-2 ml-4 text-xs font-semibold transition-opacity rounded-lg shadow-xl opacity-0 pointer-events-none left-full bg-neutral text-neutral-content group-hover:opacity-100 whitespace-nowrap">
111+
{cat.category}
112+
<div className="absolute -translate-y-1/2 border-4 border-transparent right-full top-1/2 border-r-neutral" />
113+
</div>
114+
</button>
115+
))}
116+
</div>
117+
</aside>
118+
119+
<div className="flex flex-col flex-1 w-full h-full gap-3 px-2 py-3 overflow-hidden md:px-6">
120+
<div className="md:hidden sticky top-0 z-50 flex items-center w-full gap-2 p-1.5 overflow-x-auto bg-base-100/80 backdrop-blur-xl rounded-2xl border border-white/10 shadow-lg no-scrollbar flex-nowrap">
121+
{catalogData?.contents?.map((cat: CategoryItem) => (
122+
<button
123+
key={cat.id}
124+
onClick={() => scrollToCategory(cat.id)}
125+
className={`flex items-center gap-2 px-4 py-2 text-[10px] font-bold whitespace-nowrap rounded-xl transition-all shrink-0 ${
126+
activeCategory === cat.id
127+
? 'bg-primary text-white shadow-md'
128+
: 'bg-base-200/50'
129+
}`}
130+
>
131+
{cat.icon && (
132+
<img src={cat.icon} className="w-4 h-4" alt="" />
133+
)}
134+
{cat.category}
135+
</button>
136+
))}
137+
</div>
138+
139+
<div className="grid w-full h-full grid-cols-1 gap-6 overflow-hidden lg:grid-cols-12">
140+
<div className="flex flex-col h-full gap-4 overflow-hidden lg:col-span-8">
141+
<div
142+
ref={scrollContainerRef}
143+
className="flex-1 pb-10 pr-1 overflow-y-auto scrollbar-none scroll-smooth"
144+
>
145+
<div className="grid grid-cols-1 gap-4 pb-10 md:grid-cols-2">
146+
{catalogData?.contents?.map(
147+
(category: CategoryItem, index: number) => (
148+
<div
149+
key={category.id}
150+
id={category.id}
151+
ref={(el) => {
152+
categoryRefs.current[category.id] = el
153+
}}
154+
className={`relative overflow-hidden border scroll-mt-2 bg-content bg-glass border-base-300 rounded-3xl transition-all duration-300 ${
155+
index % 3 === 0
156+
? 'md:col-span-2'
157+
: 'md:col-span-1'
158+
}`}
159+
>
160+
{category.banner && (
161+
<div className="w-full overflow-hidden h-28">
162+
<img
163+
src={category.banner}
164+
className="object-cover w-full h-full"
165+
style={{
166+
maskImage:
167+
'linear-gradient(to bottom, black 0%, transparent 100%)',
168+
WebkitMaskImage:
169+
'linear-gradient(to bottom, black 0%, transparent 100%)',
170+
}}
171+
alt=""
172+
/>
173+
</div>
174+
)}
175+
176+
<div className="p-5">
177+
<div className="flex items-center gap-4 mb-6">
178+
<div className="flex items-center gap-2.5">
179+
{category.icon ? (
180+
<img
181+
src={category.icon}
182+
className="w-4 h-4 opacity-70"
183+
alt=""
184+
/>
185+
) : (
186+
<div className="w-1 h-3.5 rounded-full bg-primary" />
187+
)}
188+
<h3 className="text-[10px] font-black tracking-widest uppercase opacity-40">
189+
{category.category}
190+
</h3>
191+
</div>
192+
<div className="flex-1 h-px bg-linear-to-r from-base-content/10 to-transparent" />
193+
</div>
194+
195+
<div
196+
className={`grid gap-y-6 gap-x-2 ${
197+
index % 3 === 0
198+
? 'grid-cols-4 sm:grid-cols-6 lg:grid-cols-8'
199+
: 'grid-cols-3 sm:grid-cols-4'
200+
}`}
201+
>
202+
{category.links?.map((link, idx) => (
203+
<a
204+
key={idx}
205+
href={getUrl(link.url)}
206+
target="_blank"
207+
rel="noopener noreferrer"
208+
className="flex flex-col items-center gap-3 group/item active:scale-95"
209+
>
210+
<div className="relative flex items-center justify-center w-12 h-12 transition-all duration-300 bg-base-200/40 rounded-2xl group-hover/item:bg-primary/10">
211+
{link.badge && (
212+
<span
213+
className="absolute -top-1 -right-1 z-20 px-1.5 py-0.5 rounded-lg text-[8px] font-black border border-white/10"
214+
style={{
215+
backgroundColor:
216+
link.badgeColor ||
217+
'var(--p)',
218+
color: '#fff',
219+
}}
220+
>
221+
{link.badge}
222+
</span>
223+
)}
224+
<img
225+
src={
226+
link.icon ||
227+
getFaviconFromUrl(
228+
link.url
229+
)
230+
}
231+
className="object-contain w-6 h-6 transition-transform rounded group-hover/item:scale-110"
232+
alt={link.name}
233+
/>
234+
</div>
235+
<span className="text-[10px] font-medium tracking-tighter text-center truncate w-full opacity-50 group-hover/item:opacity-100 transition-opacity">
236+
{link.name}
237+
</span>
238+
</a>
239+
))}
240+
</div>
241+
</div>
242+
</div>
243+
)
244+
)}
245+
</div>
246+
</div>
247+
</div>
248+
<div className="hidden h-full pb-4 space-y-4 overflow-y-auto lg:block lg:col-span-4 scrollbar-none">
249+
<div className="sticky top-0 flex flex-col gap-4">
250+
<a
251+
href="https://feedback.widgetify.ir"
252+
target="_blank"
253+
rel="noopener noreferrer"
254+
className="relative flex flex-col items-center justify-center p-5 overflow-hidden text-center transition-all duration-500 border border-dashed group bg-content bg-glass border-base-300 rounded-3xl hover:border-primary/50 hover:bg-primary/5 min-h-20"
255+
>
256+
<div className="p-3 mb-3 transition-all duration-500 rounded-2xl bg-base-200/50 text-base-content/40 group-hover:scale-110 group-hover:rotate-12 group-hover:text-warning group-hover:bg-warning/10">
257+
<HiOutlineLightBulb
258+
size={28}
259+
className="transition-transform duration-500"
260+
/>
261+
</div>
262+
263+
<h3 className="font-bold tracking-tight transition-colors duration-300 text-muted group-hover:text-primary">
264+
چیزی جا انداختیم؟
265+
</h3>
266+
267+
<p className="mt-1 text-[10px] font-medium opacity-40 transition-opacity duration-300 group-hover:opacity-100">
268+
بهمون بگو تا اضافه‌اش کنیم
269+
</p>
270+
271+
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-0 h-0.5 bg-primary/30 transition-all duration-500 group-hover:w-1/3" />
272+
</a>
273+
</div>
274+
</div>
275+
</div>
276+
</div>
277+
</div>
278+
)
279+
}
280+
281+
function getUrl(url: string) {
282+
return url.startsWith('http') ? url : `https://${url}`
283+
}

src/layouts/navbar/navbar.layout.tsx

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,72 @@ 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('closeExplorerPage')
39+
else callEvent('openExplorerPage')
40+
Analytics.event(`navbar_tab_${tab}_click`)
41+
}
42+
43+
return (
44+
<div className="flex items-center gap-0.5">
45+
{tabs.map((tab) => (
46+
<button
47+
key={tab.id}
48+
onClick={() => handleTabClick(tab.id)}
49+
className="relative p-2 cursor-pointer group nav-btn"
50+
>
51+
<span
52+
className={`
53+
relative z-10 transition-all duration-300 block
54+
${activeTab === tab.id ? 'text-primary scale-110' : 'nav-btn text-white/20 hover:text-white/40'}
55+
`}
56+
>
57+
{tab.icon}
58+
59+
{tab.hasBadge && (
60+
<span className="absolute -top-0.5 -right-0.5 flex h-2 w-2">
61+
<span
62+
className={`animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75 ${activeTab === tab.id ? 'block' : 'hidden'}`}
63+
></span>
64+
<span
65+
className={`relative inline-flex rounded-full h-2 w-2 border border-black/50 ${activeTab === tab.id ? 'bg-primary' : 'bg-primary/80'}`}
66+
></span>
67+
</span>
68+
)}
69+
</span>
70+
71+
{activeTab === tab.id && (
72+
<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)]">
73+
<div className="absolute inset-0 bg-primary blur-[2px]" />
74+
</div>
75+
)}
76+
</button>
77+
))}
78+
</div>
79+
)
80+
}
81+
2182
export function NavbarLayout(): JSX.Element {
2283
const { canReOrderWidget, toggleCanReOrderWidget } = useAppearanceSetting()
2384
const [showSettings, setShowSettings] = useState(false)
@@ -124,12 +185,7 @@ export function NavbarLayout(): JSX.Element {
124185
<div className="relative z-10 w-[1px] h-6 bg-white/[0.08]" />
125186

126187
<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>
188+
<NavbarTabs />
133189
<a
134190
href={WIDGETIFY_URLS.website}
135191
target="_blank"

0 commit comments

Comments
 (0)