Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
mastodon
GitHub Repository: mastodon/joinmastodon
Path: blob/main/components/AppsGrid.tsx
1006 views
1
import { FormattedMessage, useIntl } from "react-intl"
2
import { AppCard } from "../components/AppCard"
3
import classNames from "classnames"
4
import { useState } from "react"
5
import SelectMenu from "../components/SelectMenu"
6
import { sortBy as _sortBy } from "lodash"
7
import type { appsList } from "../data/apps"
8
import Category from "../components/Category"
9
10
export type AppsGridProps = {
11
apps: appsList
12
}
13
14
/** Renders AppCards as a grid, with sorting and filtering options */
15
export const AppsGrid = ({ apps }: AppsGridProps) => {
16
const intl = useIntl()
17
const [activeCategory, setActiveCategory] = useState("all")
18
19
//prettier-ignore
20
const categories = [
21
{ key: "all", label: intl.formatMessage({ id: "browse_apps.all", defaultMessage: "All" }) },
22
{ key: "android", label: intl.formatMessage({ id: "browse_apps.android", defaultMessage: "Android" }) },
23
{ key: "ios", label: intl.formatMessage({ id: "browse_apps.ios", defaultMessage: "iOS" }) },
24
{ key: "web", label: intl.formatMessage({ id: "browse_apps.web", defaultMessage: "Web" }) },
25
{ key: "desktop", label: intl.formatMessage({ id: "browse_apps.desktop", defaultMessage: "Desktop" }) },
26
{ key: "retro", label: intl.formatMessage({ id: "browse_apps.retro", defaultMessage: "Retro computing" }) },
27
]
28
29
/** normalizing the apps dictionary as an array */
30
const allApps = Object.entries(apps)
31
.flatMap(([category, apps]) =>
32
apps.map(({ name, icon, url, paid, released_on, hidden_from_all, open }) => ({
33
name,
34
icon,
35
url,
36
paid: paid ?? false,
37
hidden_from_all: hidden_from_all ?? false,
38
released_on: new Date(released_on) ?? null,
39
category,
40
categoryLabel: categories.find((c) => c.key === category)["label"],
41
open,
42
}))
43
)
44
45
//prettier-ignore
46
const sortOptions = [
47
{ value: "date_added", label: intl.formatMessage({ id: "sorting.recently_added", defaultMessage: "Recently Added" }) },
48
{ value: "paid", label: intl.formatMessage({ id: "sorting.free", defaultMessage: "Free" }) },
49
{ value: "category", label: intl.formatMessage({ id: "sorting.category", defaultMessage: "Category" }) },
50
{ value: "name", label: intl.formatMessage({ id: "sorting.name", defaultMessage: "Alphabetical" }) },
51
]
52
const [sortOption, setSortOption] = useState(sortOptions[0].value)
53
const filteredApps = allApps.filter(
54
({ category, hidden_from_all }) =>
55
category === activeCategory ||
56
(activeCategory === "all" && !hidden_from_all)
57
)
58
const sortedAndFilteredApps = _sortBy(filteredApps, sortOption)
59
60
return (
61
<div>
62
<div>
63
<h2 className="h4 mb-8">
64
<FormattedMessage
65
id="browse_apps.title2"
66
defaultMessage="Browse third-party apps"
67
/>
68
</h2>
69
<div className="-mx-gutter ps-gutter mb-6 overflow-x-auto">
70
<div className="flex flex-wrap gap-gutter md:flex-nowrap">
71
{categories.map((category) => (
72
<Category
73
key={category.key}
74
value={category.key}
75
currentValue={activeCategory}
76
label={category.label}
77
onChange={(e) => setActiveCategory(e.target.value)}
78
/>
79
))}
80
</div>
81
</div>
82
</div>
83
<div className="my-8">
84
<SelectMenu
85
label={
86
<FormattedMessage id="sorting.sort_by" defaultMessage="Sort" />
87
}
88
value={sortOption}
89
onChange={(v) => {
90
setSortOption(v)
91
}}
92
options={sortOptions}
93
/>
94
</div>
95
<div className="grid grid-cols-[repeat(auto-fill,minmax(250px,1fr))] gap-4">
96
{sortedAndFilteredApps.map(AppCard)}
97
</div>
98
</div>
99
)
100
}
101
export default AppsGrid
102
103