Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 143 additions & 52 deletions src/layouts/explorer/explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ import { useGetContents } from '@/services/hooks/content/get-content.hook'
import { useRef, useState, useEffect } from 'react'
import Analytics from '@/analytics'
import { getFaviconFromUrl } from '@/common/utils/icon'
import { useTheme } from '@/context/theme.context'
import { useAppearanceSetting } from '@/context/appearance.context'

interface LinkItem {
name: string
url: string
type?: 'SITE' | 'REMOTE_IFRAME'
icon?: string
badge?: string
badgeColor?: string
span?: {
col?: number | null
row?: number | null
}
height?: number
}

interface CategoryItem {
Expand All @@ -17,9 +25,14 @@ interface CategoryItem {
banner?: string
links: LinkItem[]
icon?: string
span?: {
col?: number | null
row?: number | null
}
}

export function ExplorerContent() {
const { theme } = useTheme()
const { fontFamily } = useAppearanceSetting()
const { data: catalogData } = useGetContents()
const [activeCategory, setActiveCategory] = useState<string | null>(null)
const categoryRefs = useRef<{ [key: string]: HTMLDivElement | null }>({})
Expand Down Expand Up @@ -66,16 +79,16 @@ export function ExplorerContent() {

return (
<div className="flex flex-row w-full h-screen overflow-hidden">
<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">
<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">
<div className="flex flex-col items-center w-full gap-2 px-2 py-2 overflow-x-hidden overflow-y-auto scrollbar-none">
{catalogData?.contents?.map((cat: CategoryItem) => (
<button
key={cat.id}
onClick={() => scrollToCategory(cat.id)}
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 ${
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 ${
activeCategory === cat.id
? 'bg-primary text-white shadow-md shadow-primary/40 scale-110 border-primary/50'
: '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'
: 'bg-white/3 border-white/5 text-base-content/60 hover:bg-primary/10 hover:text-primary hover:scale-105 hover:border-primary/30'
}`}
>
<div
Expand Down Expand Up @@ -111,7 +124,7 @@ export function ExplorerContent() {
</div>
</aside>

<div className="flex flex-col w-full h-full gap-3 px-2 py-3 overflow-hidden md:px-24">
<div className="flex flex-col w-full h-full gap-3 px-2 py-3 overflow-hidden md:px-10">
<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">
{catalogData?.contents?.map((cat: CategoryItem) => (
<button
Expand All @@ -137,7 +150,7 @@ export function ExplorerContent() {
ref={scrollContainerRef}
className="flex-1 pb-10 pr-1 overflow-y-auto scrollbar-none scroll-smooth"
>
<div className="grid max-w-5xl grid-cols-1 gap-4 pb-[50vh] mx-auto md:grid-cols-2">
<div className="grid max-w-5xl grid-cols-1 gap-4 pb-[50vh] mx-auto md:grid-cols-3">
{catalogData?.contents?.map(
(category: CategoryItem, index: number) => (
<div
Expand All @@ -146,9 +159,17 @@ export function ExplorerContent() {
ref={(el) => {
categoryRefs.current[category.id] = el
}}
style={{
gridColumn: category.span?.col
? `span ${category.span.col} / span ${category.span.col}`
: undefined,
gridRow: category.span?.row
? `span ${category.span.row} / span ${category.span.row}`
: undefined,
}}
className={`relative overflow-hidden border scroll-mt-4 bg-content bg-glass border-base-300 rounded-3xl transition-all duration-300 ${
index === 0
? 'md:col-span-2' // آیتم اول همیشه تمام عرض
? 'md:col-span-2'
: (
index ===
catalogData.contents
Expand All @@ -159,7 +180,7 @@ export function ExplorerContent() {
2 ===
0
)
? 'md:col-span-2' // اگر تعداد کل زوج بود (مثل ۶ تا)، آخری تمام عرض شود تا جای خالی پر شود
? 'md:col-span-2'
: 'md:col-span-1'
}`}
>
Expand All @@ -179,7 +200,7 @@ export function ExplorerContent() {
</div>
)}
<div className="p-5">
<div className="flex items-center gap-4 mb-6">
<div className="flex items-center gap-4 mb-4">
<div className="flex items-center gap-2.5">
{category.icon ? (
<img
Expand All @@ -190,54 +211,18 @@ export function ExplorerContent() {
) : (
<div className="w-1 h-3.5 rounded-full bg-primary" />
)}
<h3 className="text-[10px] font-black tracking-widest uppercase opacity-40">
<h3 className="text-xs font-black tracking-widest uppercase opacity-70">
{category.category}
</h3>
</div>
<div className="flex-1 h-px bg-linear-to-r from-base-content/10 to-transparent" />
</div>
<div
className={`grid gap-y-6 gap-x-2 grid-cols-3 sm:grid-cols-5`}
>
{category.links?.map((link, idx) => (
<a
key={idx}
href={getUrl(link.url)}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col items-center gap-3 group/item active:scale-95"
>
<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">
{link.badge && (
<span
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"
style={{
backgroundColor:
link.badgeColor ||
'var(--p)',
color: '#fff',
}}
>
{link.badge}
</span>
)}
<img
src={
link.icon ||
getFaviconFromUrl(
link.url
)
}
className="object-contain w-6 h-6 transition-all duration-500 rounded group-hover/item:scale-110 group-hover/item:brightness-110"
alt={link.name}
/>
</div>
<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">
{link.name}
</span>
</a>
))}
</div>

<HandleCatalogs
category={category}
theme={theme}
fontFamily={fontFamily}
/>
</div>
</div>
)
Expand All @@ -254,3 +239,109 @@ export function ExplorerContent() {
function getUrl(url: string) {
return url.startsWith('http') ? url : `https://${url}`
}

interface Prop {
category: CategoryItem
theme: string
fontFamily: string
}
function HandleCatalogs({ category, theme, fontFamily }: Prop) {
return (
<div className={`grid gap-y-6 gap-x-2 grid-cols-3 sm:grid-cols-5`}>
{category.links?.map((link) =>
link.type === 'REMOTE_IFRAME' ? (
<RenderIframeLinks
key={link.url}
link={link}
theme={theme}
fontFamily={fontFamily}
/>
) : (
<RenderSiteLinks key={link.url} link={link} />
)
)}
</div>
)
}

interface SiteProp {
link: LinkItem
}
function RenderSiteLinks({ link }: SiteProp) {
return (
<a
href={getUrl(link.url)}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col items-center gap-3 group/item active:scale-95"
style={{
gridColumn: link.span?.col
? `span ${link.span.col} / span ${link.span.col}`
: undefined,
gridRow: link.span?.row
? `span ${link.span.row} / span ${link.span.row}`
: undefined,
}}
>
<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">
{link.badge && (
<span
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"
style={{
backgroundColor: link.badgeColor || 'var(--p)',
color: '#fff',
}}
>
{link.badge}
</span>
)}
<img
src={link.icon || getFaviconFromUrl(link.url)}
className="object-contain w-6 h-6 transition-all duration-500 rounded group-hover/item:scale-110 group-hover/item:brightness-110"
alt={link.name}
/>
</div>
<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">
{link.name}
</span>
</a>
)
}

interface IframeProp {
link: LinkItem
theme: string
fontFamily: string
}
function RenderIframeLinks({ link, theme, fontFamily }: IframeProp) {
const urlObj = new URL(link.url)
urlObj.searchParams.set('theme', encodeURIComponent(theme))
urlObj.searchParams.set('font', encodeURIComponent(fontFamily))
urlObj.searchParams.set('referrer', 'extension')
const url = urlObj.toString()

return (
<div
className="w-full"
style={{
gridColumn: link.span?.col
? `span ${link.span.col} / span ${link.span.col}`
: 'span 3 / span 3',
gridRow: link.span?.row
? `span ${link.span.row} / span ${link.span.row}`
: undefined,
}}
>
<iframe
src={url}
height={link.height}
style={{
border: 'none',
borderRadius: '1.5rem',
width: '100%',
}}
title={link.name}
/>
</div>
)
}
19 changes: 17 additions & 2 deletions src/services/hooks/content/get-content.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,22 @@ export interface FetchedContent {
contents: {
id: string
category: string
links: { name: string; url: string; icon?: string }[]
icon?: string
links: {
name: string
url: string
type: 'SITE' | 'REMOTE_IFRAME'
icon?: string
span?: {
col?: number | null
row?: number | null
}
height?: number
}[]
span?: {
col?: number | null
row?: number | null
}
}[]
}

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

const { data } = await api.get<FetchedContent>('/contents/beta')
const { data } = await api.get<FetchedContent>('/contents')
return data
},
staleTime: 5 * 60 * 1000, // 5 minutes
Expand Down