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
83 changes: 20 additions & 63 deletions app/[locale]/apps/_components/AppsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,12 @@ import { useMemo, useState } from "react"

import { AppData } from "@/lib/types"

import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"

import { trackCustomEvent } from "@/lib/utils/matomo"
import FilterBar from "@/components/FilterBar"

import AppCard from "./AppCard"

import useTranslation from "@/hooks/useTranslation"

const AppsTable = ({ apps }: { apps: AppData[] }) => {
const { t } = useTranslation("page-apps")
const [filterBy, setFilterBy] = useState("All")
const [filterBy, setFilterBy] = useState<string>()

const subCategories = useMemo(
() => [...new Set(apps.flatMap((app) => app.subCategory))],
Expand All @@ -34,63 +23,31 @@ const AppsTable = ({ apps }: { apps: AppData[] }) => {
const filteredApps = useMemo(
() =>
apps.filter((app) => {
if (filterBy === "All") return true
if (!filterBy) return true
return app.subCategory.includes(filterBy)
}),
[apps, filterBy]
)

const filterItems = subCategories.map((subCategory) => ({
value: subCategory,
label: `${subCategory} (${getSubCategoryCount(subCategory)})`,
}))

return (
<div className="flex flex-col gap-7">
<div className="flex flex-row items-end justify-between border-b pb-2 sm:items-center">
<div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center">
<p className="whitespace-nowrap">{t("page-apps-filter-by")}</p>
<Select
value={filterBy}
onValueChange={(value) => {
setFilterBy(value)
trackCustomEvent({
eventCategory: "category_page",
eventAction: "filter_by",
eventName: `subcategory name ${value}`,
})
}}
>
<SelectTrigger className="min-w-28">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem
value="All"
className="cursor-pointer hover:bg-primary-low-contrast"
>
{t("page-apps-filter-all")}
</SelectItem>
{subCategories.map((subCategory) => (
<SelectItem
key={subCategory}
value={subCategory}
className="cursor-pointer hover:bg-primary-low-contrast"
>
{subCategory} ({getSubCategoryCount(subCategory)})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<p className="text-body-medium">
{t("page-apps-showing")}{" "}
<span className="text-body">
(
{filteredApps.length === apps.length
? apps.length
: `${filteredApps.length}/${apps.length}`}
)
</span>
</p>
</div>
</div>
<FilterBar
items={filterItems}
value={filterBy}
onValueChange={setFilterBy}
count={filteredApps.length}
totalCount={apps.length}
matomoEvent={{
eventCategory: "category_page",
eventAction: "filter_by",
eventName: "",
}}
/>
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
{filteredApps.map((app) => (
<div key={app.name}>
Expand Down
102 changes: 12 additions & 90 deletions app/[locale]/developers/apps/[category]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { AppWindowMac } from "lucide-react"
import Image from "next/image"
import { redirect } from "next/navigation"
import { getTranslations, setRequestLocale } from "next-intl/server"

Expand All @@ -8,24 +6,22 @@ import type { CommitHistory, Lang, PageParams } from "@/lib/types"
import { ContentHero } from "@/components/Hero"
import MainArticle from "@/components/MainArticle"
import SubpageCard from "@/components/SubpageCard"
import { LinkBox, LinkOverlay } from "@/components/ui/link-box"
import { Section } from "@/components/ui/section"
import { TagsInlineText } from "@/components/ui/tag"

import { getAppPageContributorInfo } from "@/lib/utils/contributors"
import { getMetadata } from "@/lib/utils/metadata"

import AppModalContents from "../_components/AppModalContents"
import AppModalWrapper from "../_components/AppModalWrapper"
import CategoryAppsGrid from "../_components/CategoryAppsGrid"
import HighlightsSection from "../_components/HighlightsSection"
import TagFilter from "../_components/TagFilter"
import { DEV_APP_CATEGORIES } from "../constants"
import type { DeveloperAppCategorySlug, DeveloperAppTag } from "../types"
import {
getCachedHighlightsByCategory,
getCategoryPageHighlights,
transformDeveloperAppsData,
} from "../utils"
import { transformDeveloperAppsData } from "../utils"

import DevelopersAppsCategoryJsonLD from "./page-jsonld"

Expand All @@ -36,10 +32,10 @@ const Page = async ({
searchParams,
}: {
params: PageParams & { category: DeveloperAppCategorySlug }
searchParams: { appId?: string; tag?: string }
searchParams: { appId?: string }
}) => {
const { locale, category } = params
const { appId, tag } = searchParams
const { appId } = searchParams

setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "page-developers-apps" })
Expand All @@ -54,55 +50,22 @@ const Page = async ({
new Set(allCategoryData.flatMap((app) => app.tags))
).sort()

// Filter by selected tag if present (validate it's a real tag)
const validTag =
tag && uniqueTags.includes(tag as DeveloperAppTag)
? (tag as DeveloperAppTag)
: undefined
const categoryData = validTag
? allCategoryData.filter((app) => app.tags.includes(validTag))
: allCategoryData

const activeApp = enrichedData.find((app) => app.id === appId)

// Clean up invalid searchParams by redirecting
const hasInvalidTag = tag && !validTag
const hasInvalidAppId = appId && !activeApp
if (hasInvalidTag || hasInvalidAppId) {
const params = new URLSearchParams()
if (validTag) params.set("tag", validTag)
if (activeApp) params.set("appId", activeApp.id)
const queryString = params.toString()
redirect(
`/developers/apps/${category}${queryString ? `?${queryString}` : ""}`
)
// Clean up invalid appId by redirecting
if (appId && !activeApp) {
redirect(`/developers/apps/${category}`)
}

// Prepare translations for client component
// Prepare tag labels for client component
const tagLabels = Object.fromEntries(
uniqueTags.map((tag) => [tag, t(`page-developers-apps-tag-${tag}`)])
)
const filterLabels = {
filterBy: t("page-developers-apps-filter-label"),
clearFilter: t("page-developers-apps-filter-clear"),
noTags: t("page-developers-apps-filter-no-tags"),
showing: t("page-developers-apps-filter-showing"),
}
) as Record<DeveloperAppTag, string>

// Get dynamic highlights based on stars and recent activity (cached weekly)
const highlightsByCategory = await getCachedHighlightsByCategory(enrichedData)
const highlights = getCategoryPageHighlights(highlightsByCategory, category)

// Helper to build app modal link with preserved tag param
const buildAppLink = (appId: string) => {
const params = new URLSearchParams()
params.set("appId", appId)
if (validTag) {
params.set("tag", validTag)
}
return `?${params.toString()}`
}

// Get contributor info for JSON-LD
const commitHistoryCache: CommitHistory = {}
const { contributors } = await getAppPageContributorInfo(
Expand Down Expand Up @@ -135,52 +98,11 @@ const Page = async ({
{t("page-developers-apps-applications-title")}
</h2>

<TagFilter
tags={uniqueTags}
<CategoryAppsGrid
apps={allCategoryData}
uniqueTags={uniqueTags}
tagLabels={tagLabels}
selectedTag={validTag}
category={category}
count={categoryData.length}
labels={filterLabels}
/>

<div className="grid grid-cols-fill-3 gap-x-8">
{categoryData.map((app) => (
<LinkBox
key={app.id}
className="h-fit rounded-xl p-6 hover:bg-background-highlight"
>
<LinkOverlay
href={buildAppLink(app.id)}
scroll={false}
className="flex gap-x-3 no-underline"
>
<div className="grid size-14 shrink-0 place-items-center overflow-hidden rounded-lg border">
{app.thumbnail_url ? (
<Image
src={app.thumbnail_url}
alt=""
width={58}
height={58}
/>
) : (
<AppWindowMac className="size-12" />
)}
</div>
<div className="space-y-1">
<p className="font-bold text-body">{app.name}</p>
<TagsInlineText
list={app.tags.map((tag) =>
t(`page-developers-apps-tag-${tag}`)
)}
variant="light"
className="lowercase"
/>
</div>
</LinkOverlay>
</LinkBox>
))}
</div>
</Section>

<Section id="categories" className="space-y-4">
Expand Down
84 changes: 84 additions & 0 deletions app/[locale]/developers/apps/_components/CategoryAppsGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"use client"

import { useMemo, useState } from "react"
import { AppWindowMac } from "lucide-react"
import Image from "next/image"

import FilterBar from "@/components/FilterBar"
import { LinkBox, LinkOverlay } from "@/components/ui/link-box"
import { TagsInlineText } from "@/components/ui/tag"

import type { DeveloperApp, DeveloperAppTag } from "../types"

type CategoryAppsGridProps = {
apps: DeveloperApp[]
uniqueTags: DeveloperAppTag[]
tagLabels: Record<DeveloperAppTag, string>
}

export default function CategoryAppsGrid({
apps,
uniqueTags,
tagLabels,
}: CategoryAppsGridProps) {
const [selectedTag, setSelectedTag] = useState<DeveloperAppTag>()

const filteredApps = useMemo(
() =>
selectedTag ? apps.filter((app) => app.tags.includes(selectedTag)) : apps,
[apps, selectedTag]
)

const filterItems = uniqueTags.map((tag) => ({
value: tag,
label: tagLabels[tag],
}))

return (
<>
<FilterBar
items={filterItems}
value={selectedTag}
onValueChange={(value) => setSelectedTag(value as DeveloperAppTag)}
count={filteredApps.length}
totalCount={apps.length}
/>

<div className="grid grid-cols-fill-3 gap-x-8">
{filteredApps.map((app) => (
<LinkBox
key={app.id}
className="h-fit rounded-xl p-6 hover:bg-background-highlight"
>
<LinkOverlay
href={`?appId=${app.id}`}
scroll={false}
className="flex gap-x-3 no-underline"
>
<div className="grid size-14 shrink-0 place-items-center overflow-hidden rounded-lg border">
{app.thumbnail_url ? (
<Image
src={app.thumbnail_url}
alt=""
width={58}
height={58}
/>
) : (
<AppWindowMac className="size-12" />
)}
</div>
<div className="space-y-1">
<p className="font-bold text-body">{app.name}</p>
<TagsInlineText
list={app.tags.map((tag) => tagLabels[tag])}
variant="light"
className="lowercase"
/>
</div>
</LinkOverlay>
</LinkBox>
))}
</div>
</>
)
}
Loading