AppsGrid.tsx 3.7 KB

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