Skip to content

Commit 8e03533

Browse files
authored
Merge pull request #378 from widgetify-app/feat/explorer-link-types-and-layout
Feat/explorer link types and layout
2 parents 4c307fb + 27517e1 commit 8e03533

File tree

2 files changed

+160
-54
lines changed

2 files changed

+160
-54
lines changed

src/layouts/explorer/explorer.tsx

Lines changed: 143 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@ import { useGetContents } from '@/services/hooks/content/get-content.hook'
22
import { useRef, useState, useEffect } from 'react'
33
import Analytics from '@/analytics'
44
import { getFaviconFromUrl } from '@/common/utils/icon'
5+
import { useTheme } from '@/context/theme.context'
6+
import { useAppearanceSetting } from '@/context/appearance.context'
57

68
interface LinkItem {
79
name: string
810
url: string
11+
type?: 'SITE' | 'REMOTE_IFRAME'
912
icon?: string
1013
badge?: string
1114
badgeColor?: string
15+
span?: {
16+
col?: number | null
17+
row?: number | null
18+
}
19+
height?: number
1220
}
1321

1422
interface CategoryItem {
@@ -17,9 +25,14 @@ interface CategoryItem {
1725
banner?: string
1826
links: LinkItem[]
1927
icon?: string
28+
span?: {
29+
col?: number | null
30+
row?: number | null
31+
}
2032
}
21-
2233
export function ExplorerContent() {
34+
const { theme } = useTheme()
35+
const { fontFamily } = useAppearanceSetting()
2336
const { data: catalogData } = useGetContents()
2437
const [activeCategory, setActiveCategory] = useState<string | null>(null)
2538
const categoryRefs = useRef<{ [key: string]: HTMLDivElement | null }>({})
@@ -66,16 +79,16 @@ export function ExplorerContent() {
6679

6780
return (
6881
<div className="flex flex-row w-full h-screen overflow-hidden">
69-
<aside className="flex-col items-center hidden w-20 gap-3 py-4 md:flex bg-white/[0.02] backdrop-blur-sm border border-white/[0.08] rounded-[2.5rem] lg:mt-4 h-fit max-h-[calc(100vh-160px)] sticky top-4">
82+
<aside className="flex-col items-center hidden w-20 gap-3 py-4 md:flex bg-white/2 backdrop-blur-sm border border-white/8 rounded-3xl lg:mt-4 h-fit max-h-[calc(100vh-160px)] sticky top-4">
7083
<div className="flex flex-col items-center w-full gap-2 px-2 py-2 overflow-x-hidden overflow-y-auto scrollbar-none">
7184
{catalogData?.contents?.map((cat: CategoryItem) => (
7285
<button
7386
key={cat.id}
7487
onClick={() => scrollToCategory(cat.id)}
75-
className={`relative group flex flex-col items-center justify-center w-14 min-h-14 max-h-14 rounded-[1.5rem] transition-all duration-500 cursor-pointer border ${
88+
className={`relative group flex flex-col items-center justify-center w-14 min-h-14 max-h-14 rounded-3xl transition-all duration-500 cursor-pointer border ${
7689
activeCategory === cat.id
7790
? 'bg-primary text-white shadow-md shadow-primary/40 scale-110 border-primary/50'
78-
: 'bg-white/[0.03] border-white/5 text-base-content/60 hover:bg-primary/10 hover:text-primary hover:scale-105 hover:border-primary/30'
91+
: 'bg-white/3 border-white/5 text-base-content/60 hover:bg-primary/10 hover:text-primary hover:scale-105 hover:border-primary/30'
7992
}`}
8093
>
8194
<div
@@ -111,7 +124,7 @@ export function ExplorerContent() {
111124
</div>
112125
</aside>
113126

114-
<div className="flex flex-col w-full h-full gap-3 px-2 py-3 overflow-hidden md:px-24">
127+
<div className="flex flex-col w-full h-full gap-3 px-2 py-3 overflow-hidden md:px-10">
115128
<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">
116129
{catalogData?.contents?.map((cat: CategoryItem) => (
117130
<button
@@ -137,7 +150,7 @@ export function ExplorerContent() {
137150
ref={scrollContainerRef}
138151
className="flex-1 pb-10 pr-1 overflow-y-auto scrollbar-none scroll-smooth"
139152
>
140-
<div className="grid max-w-5xl grid-cols-1 gap-4 pb-[50vh] mx-auto md:grid-cols-2">
153+
<div className="grid max-w-5xl grid-cols-1 gap-4 pb-[50vh] mx-auto md:grid-cols-3">
141154
{catalogData?.contents?.map(
142155
(category: CategoryItem, index: number) => (
143156
<div
@@ -146,9 +159,17 @@ export function ExplorerContent() {
146159
ref={(el) => {
147160
categoryRefs.current[category.id] = el
148161
}}
162+
style={{
163+
gridColumn: category.span?.col
164+
? `span ${category.span.col} / span ${category.span.col}`
165+
: undefined,
166+
gridRow: category.span?.row
167+
? `span ${category.span.row} / span ${category.span.row}`
168+
: undefined,
169+
}}
149170
className={`relative overflow-hidden border scroll-mt-4 bg-content bg-glass border-base-300 rounded-3xl transition-all duration-300 ${
150171
index === 0
151-
? 'md:col-span-2' // آیتم اول همیشه تمام عرض
172+
? 'md:col-span-2'
152173
: (
153174
index ===
154175
catalogData.contents
@@ -159,7 +180,7 @@ export function ExplorerContent() {
159180
2 ===
160181
0
161182
)
162-
? 'md:col-span-2' // اگر تعداد کل زوج بود (مثل ۶ تا)، آخری تمام عرض شود تا جای خالی پر شود
183+
? 'md:col-span-2'
163184
: 'md:col-span-1'
164185
}`}
165186
>
@@ -179,7 +200,7 @@ export function ExplorerContent() {
179200
</div>
180201
)}
181202
<div className="p-5">
182-
<div className="flex items-center gap-4 mb-6">
203+
<div className="flex items-center gap-4 mb-4">
183204
<div className="flex items-center gap-2.5">
184205
{category.icon ? (
185206
<img
@@ -190,54 +211,18 @@ export function ExplorerContent() {
190211
) : (
191212
<div className="w-1 h-3.5 rounded-full bg-primary" />
192213
)}
193-
<h3 className="text-[10px] font-black tracking-widest uppercase opacity-40">
214+
<h3 className="text-xs font-black tracking-widest uppercase opacity-70">
194215
{category.category}
195216
</h3>
196217
</div>
197218
<div className="flex-1 h-px bg-linear-to-r from-base-content/10 to-transparent" />
198219
</div>
199-
<div
200-
className={`grid gap-y-6 gap-x-2 grid-cols-3 sm:grid-cols-5`}
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-500 bg-base-200/40 rounded-2xl group-hover/item:bg-primary/20 group-hover/item:shadow-lg group-hover/item:shadow-primary/20 group-hover/item:-translate-y-1.5 border border-transparent group-hover/item:border-primary/20">
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 shadow-sm"
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-all duration-500 rounded group-hover/item:scale-110 group-hover/item:brightness-110"
232-
alt={link.name}
233-
/>
234-
</div>
235-
<span className="text-[10px] font-semibold tracking-tighter text-center truncate w-full opacity-40 group-hover/item:opacity-100 group-hover/item:text-primary transition-all duration-300">
236-
{link.name}
237-
</span>
238-
</a>
239-
))}
240-
</div>
220+
221+
<HandleCatalogs
222+
category={category}
223+
theme={theme}
224+
fontFamily={fontFamily}
225+
/>
241226
</div>
242227
</div>
243228
)
@@ -254,3 +239,109 @@ export function ExplorerContent() {
254239
function getUrl(url: string) {
255240
return url.startsWith('http') ? url : `https://${url}`
256241
}
242+
243+
interface Prop {
244+
category: CategoryItem
245+
theme: string
246+
fontFamily: string
247+
}
248+
function HandleCatalogs({ category, theme, fontFamily }: Prop) {
249+
return (
250+
<div className={`grid gap-y-6 gap-x-2 grid-cols-3 sm:grid-cols-5`}>
251+
{category.links?.map((link) =>
252+
link.type === 'REMOTE_IFRAME' ? (
253+
<RenderIframeLinks
254+
key={link.url}
255+
link={link}
256+
theme={theme}
257+
fontFamily={fontFamily}
258+
/>
259+
) : (
260+
<RenderSiteLinks key={link.url} link={link} />
261+
)
262+
)}
263+
</div>
264+
)
265+
}
266+
267+
interface SiteProp {
268+
link: LinkItem
269+
}
270+
function RenderSiteLinks({ link }: SiteProp) {
271+
return (
272+
<a
273+
href={getUrl(link.url)}
274+
target="_blank"
275+
rel="noopener noreferrer"
276+
className="flex flex-col items-center gap-3 group/item active:scale-95"
277+
style={{
278+
gridColumn: link.span?.col
279+
? `span ${link.span.col} / span ${link.span.col}`
280+
: undefined,
281+
gridRow: link.span?.row
282+
? `span ${link.span.row} / span ${link.span.row}`
283+
: undefined,
284+
}}
285+
>
286+
<div className="relative flex items-center justify-center w-12 h-12 transition-all duration-500 bg-base-200/40 rounded-2xl group-hover/item:bg-primary/20 group-hover/item:shadow-lg group-hover/item:shadow-primary/20 group-hover/item:-translate-y-1.5 border border-transparent group-hover/item:border-primary/20">
287+
{link.badge && (
288+
<span
289+
className="absolute -top-1 -right-1 z-20 px-1.5 py-0.5 rounded-lg text-[8px] font-black border border-white/10 shadow-sm"
290+
style={{
291+
backgroundColor: link.badgeColor || 'var(--p)',
292+
color: '#fff',
293+
}}
294+
>
295+
{link.badge}
296+
</span>
297+
)}
298+
<img
299+
src={link.icon || getFaviconFromUrl(link.url)}
300+
className="object-contain w-6 h-6 transition-all duration-500 rounded group-hover/item:scale-110 group-hover/item:brightness-110"
301+
alt={link.name}
302+
/>
303+
</div>
304+
<span className="text-[10px] font-semibold tracking-tighter text-center truncate w-full opacity-40 group-hover/item:opacity-100 group-hover/item:text-primary transition-all duration-300">
305+
{link.name}
306+
</span>
307+
</a>
308+
)
309+
}
310+
311+
interface IframeProp {
312+
link: LinkItem
313+
theme: string
314+
fontFamily: string
315+
}
316+
function RenderIframeLinks({ link, theme, fontFamily }: IframeProp) {
317+
const urlObj = new URL(link.url)
318+
urlObj.searchParams.set('theme', encodeURIComponent(theme))
319+
urlObj.searchParams.set('font', encodeURIComponent(fontFamily))
320+
urlObj.searchParams.set('referrer', 'extension')
321+
const url = urlObj.toString()
322+
323+
return (
324+
<div
325+
className="w-full"
326+
style={{
327+
gridColumn: link.span?.col
328+
? `span ${link.span.col} / span ${link.span.col}`
329+
: 'span 3 / span 3',
330+
gridRow: link.span?.row
331+
? `span ${link.span.row} / span ${link.span.row}`
332+
: undefined,
333+
}}
334+
>
335+
<iframe
336+
src={url}
337+
height={link.height}
338+
style={{
339+
border: 'none',
340+
borderRadius: '1.5rem',
341+
width: '100%',
342+
}}
343+
title={link.name}
344+
/>
345+
</div>
346+
)
347+
}

src/services/hooks/content/get-content.hook.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,22 @@ export interface FetchedContent {
55
contents: {
66
id: string
77
category: string
8-
links: { name: string; url: string; icon?: string }[]
8+
icon?: string
9+
links: {
10+
name: string
11+
url: string
12+
type: 'SITE' | 'REMOTE_IFRAME'
13+
icon?: string
14+
span?: {
15+
col?: number | null
16+
row?: number | null
17+
}
18+
height?: number
19+
}[]
20+
span?: {
21+
col?: number | null
22+
row?: number | null
23+
}
924
}[]
1025
}
1126

@@ -15,7 +30,7 @@ export const useGetContents = () => {
1530
queryFn: async () => {
1631
const api = await getMainClient()
1732

18-
const { data } = await api.get<FetchedContent>('/contents/beta')
33+
const { data } = await api.get<FetchedContent>('/contents')
1934
return data
2035
},
2136
staleTime: 5 * 60 * 1000, // 5 minutes

0 commit comments

Comments
 (0)