Test lint-staged

This commit is contained in:
Kalidou Diagne
2025-02-03 23:36:16 +00:00
parent f46d659e11
commit 5038805441
35 changed files with 2105 additions and 930 deletions

View File

@@ -3,10 +3,9 @@ import {
LucideProps,
Moon,
SunMedium,
Twitter,
X,
type Icon as LucideIcon,
} from "lucide-react"
} from 'lucide-react'
export type Icon = LucideIcon
@@ -101,11 +100,11 @@ export const Icons = {
<svg
width={props.width}
height={
typeof props.height === "number"
typeof props.height === 'number'
? props.height
: typeof props.width === "number"
? props.width - 1
: 18
: typeof props.width === 'number'
? props.width - 1
: 18
}
viewBox="0 0 25 24"
fill="currentColor"
@@ -138,11 +137,11 @@ export const Icons = {
xmlns="http://www.w3.org/2000/svg"
width={props.width}
height={
typeof props.height === "number"
typeof props.height === 'number'
? props.height
: typeof props.width === "number"
? props.width - 2
: 22
: typeof props.width === 'number'
? props.width - 2
: 22
}
fill="currentColor"
viewBox="0 0 24 22"
@@ -159,11 +158,11 @@ export const Icons = {
xmlns="http://www.w3.org/2000/svg"
width={props.width}
height={
typeof props.height === "number"
typeof props.height === 'number'
? props.height
: typeof props.width === "number"
? props.width + 2
: 26
: typeof props.width === 'number'
? props.width + 2
: 26
}
fill="currentColor"
viewBox="0 0 24 26"
@@ -196,11 +195,11 @@ export const Icons = {
<svg
width={props.size}
height={
typeof props.size === "number"
typeof props.size === 'number'
? props.size
: typeof props.size === "number"
? props.size + 1
: 17
: typeof props.size === 'number'
? props.size + 1
: 17
}
viewBox="0 0 16 17"
fill="none"
@@ -222,11 +221,11 @@ export const Icons = {
<svg
width={props.width}
height={
typeof props.height === "number"
typeof props.height === 'number'
? props.height
: typeof props.width === "number"
? props.width - 1
: 19
: typeof props.width === 'number'
? props.width - 1
: 19
}
viewBox="0 0 20 19"
fill="none"
@@ -243,11 +242,11 @@ export const Icons = {
<svg
width={props.width}
height={
typeof props.height === "number"
typeof props.height === 'number'
? props.height
: typeof props.width === "number"
? props.width - 1
: 19
: typeof props.width === 'number'
? props.width - 1
: 19
}
viewBox="0 0 20 19"
fill="none"
@@ -264,11 +263,11 @@ export const Icons = {
<svg
width={props.width}
height={
typeof props.height === "number"
typeof props.height === 'number'
? props.height
: typeof props.width === "number"
? props.width + 1
: 15
: typeof props.width === 'number'
? props.width + 1
: 15
}
viewBox="0 0 14 15"
fill="none"
@@ -471,6 +470,7 @@ export const Icons = {
height="14"
viewBox="0 0 14 14"
fill="currentColor"
{...props}
>
<path
d="M0.960938 9.93311V3.43311C0.960938 3.00213 1.13214 2.5888 1.43689 2.28406C1.74164 1.97931 2.15496 1.80811 2.58594 1.80811H12.3359C12.7669 1.80811 13.1802 1.97931 13.485 2.28406C13.7897 2.5888 13.9609 3.00213 13.9609 3.43311V9.93311C13.9609 10.3641 13.7897 10.7774 13.485 11.0822C13.1802 11.3869 12.7669 11.5581 12.3359 11.5581H2.58594C2.15496 11.5581 1.74164 11.3869 1.43689 11.0822C1.13214 10.7774 0.960938 10.3641 0.960938 9.93311ZM6.47781 4.32117C6.41707 4.27792 6.34559 4.25222 6.27121 4.24689C6.19683 4.24157 6.12242 4.25681 6.05613 4.29097C5.98984 4.32512 5.93423 4.37686 5.89539 4.44051C5.85655 4.50417 5.83598 4.57729 5.83594 4.65186V8.71436C5.83598 8.78893 5.85655 8.86204 5.89539 8.9257C5.93423 8.98936 5.98984 9.04109 6.05613 9.07524C6.12242 9.1094 6.19683 9.12464 6.27121 9.11932C6.34559 9.11399 6.41707 9.08829 6.47781 9.04504L9.32156 7.01379C9.37422 6.97621 9.41715 6.9266 9.44677 6.86908C9.47638 6.81156 9.49184 6.7478 9.49184 6.68311C9.49184 6.61841 9.47638 6.55465 9.44677 6.49713C9.41715 6.43961 9.37422 6.39 9.32156 6.35242L6.47781 4.32117Z"
@@ -485,6 +485,7 @@ export const Icons = {
height="16"
viewBox="0 0 17 16"
fill="none"
{...props}
>
<g clipPath="url(#clip0_7523_20424)">
<path
@@ -509,6 +510,7 @@ export const Icons = {
height="13"
viewBox="0 0 13 13"
fill="none"
{...props}
>
<path
d="M11.2031 1.20312H1.79685C1.53818 1.20312 1.32654 1.41895 1.32654 1.68274V11.2752C1.32654 11.5389 1.53818 11.7548 1.79685 11.7548H11.2031C11.4618 11.7548 11.6734 11.5389 11.6734 11.2752V1.68274C11.6734 1.41895 11.4618 1.20312 11.2031 1.20312Z"
@@ -543,6 +545,7 @@ export const Icons = {
height="12"
viewBox="0 0 12 12"
fill="none"
{...props}
>
<path
fillRule="evenodd"
@@ -705,11 +708,11 @@ export const Icons = {
/>
</svg>
),
edit: ({ size = 10, ...props }: LucideProps) => (
edit: ({ size = 16, ...props }: LucideProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
{...props}

View File

@@ -1,23 +1,19 @@
import React from "react"
import Image from "next/image"
import { useRouter } from "next/navigation"
import { VariantProps, cva } from "class-variance-authority"
import React from 'react'
import Image from 'next/image'
import { useRouter } from 'next/navigation'
import { VariantProps, cva } from 'class-variance-authority'
import { getProjectById } from "@/lib/projectsUtils"
import { getProjectById } from '@/lib/projectsUtils'
import {
ProjectInterface,
ProjectLinkWebsite,
ProjectStatus,
ProjectStatusLabelMapping,
} from "@/lib/types"
import { cn } from "@/lib/utils"
import { useTranslation } from "@/app/i18n/client"
import { LocaleTypes } from "@/app/i18n/settings"
} from '@/lib/types'
import { cn } from '@/lib/utils'
import { LocaleTypes } from '@/app/i18n/settings'
import { Icons } from "../icons"
import { CategoryTag } from "../ui/categoryTag"
import { ThemesButtonMapping } from "./project-filters-bar"
import { ProjectLink } from "./project-link"
import { ProjectLink } from './project-link'
interface ProjectCardProps
extends React.HTMLAttributes<HTMLDivElement>,
@@ -27,42 +23,36 @@ interface ProjectCardProps
showBanner?: boolean // show images in the card
}
const TagsIconMapping: Record<string, any> = {
build: <Icons.hammer height={12} width={12} />,
play: <Icons.hand height={12} width={12} />,
research: <Icons.readme height={12} width={12} />,
}
const tagCardVariants = cva(
"text-xs font-sans text-tuatara-950 rounded-[3px] py-[2px] px-[6px]",
'text-xs font-sans text-tuatara-950 rounded-[3px] py-[2px] px-[6px]',
{
variants: {
variant: {
primary: "bg-[#D8FEA8]",
secondary: "bg-[#C2E8F5]",
primary: 'bg-[#D8FEA8]',
secondary: 'bg-[#C2E8F5]',
},
},
}
)
const projectCardVariants = cva(
"flex flex-col overflow-hidden rounded-lg transition duration-200 ease-in border border-transparent",
'flex flex-col overflow-hidden rounded-lg transition duration-200 ease-in border border-transparent',
{
variants: {
showLinks: {
true: "min-h-[280px]",
false: "min-h-[200px]",
true: 'min-h-[280px]',
false: 'min-h-[200px]',
},
border: {
true: "border border-slate-900/20",
true: 'border border-slate-900/20',
},
},
}
)
export const ProjectStatusColorMapping: Record<ProjectStatus, string> = {
active: "#D8FEA8",
inactive: "#FFB7AA",
maintained: "#FFEC9E",
active: '#D8FEA8',
inactive: '#FFB7AA',
maintained: '#FFEC9E',
}
export default function ProjectCard({
@@ -73,10 +63,9 @@ export default function ProjectCard({
className,
lang,
}: ProjectCardProps & { lang: LocaleTypes }) {
const { t } = useTranslation(lang, "common")
const router = useRouter()
const { id, image, links, name, tags, imageAlt, projectStatus, cardTags } =
const { id, image, links, name, imageAlt, projectStatus, cardTags } =
project ?? {}
const { content: projectContent } = getProjectById(id, lang)
@@ -84,7 +73,7 @@ export default function ProjectCard({
return (
<div
className={cn(
"group",
'group',
projectCardVariants({ showLinks, border, className })
)}
>
@@ -96,7 +85,7 @@ export default function ProjectCard({
}}
>
<Image
src={`/project-banners/${image ? image : "fallback.webp"}`}
src={`/project-banners/${image ? image : 'fallback.webp'}`}
alt={`${name} banner`}
width={1200}
height={630}
@@ -154,12 +143,12 @@ export default function ProjectCard({
{cardTags && (
<div className="flex items-center gap-1">
{cardTags?.primary && (
<div className={tagCardVariants({ variant: "primary" })}>
<div className={tagCardVariants({ variant: 'primary' })}>
{cardTags?.primary}
</div>
)}
{cardTags?.secondary && (
<div className={tagCardVariants({ variant: "secondary" })}>
<div className={tagCardVariants({ variant: 'secondary' })}>
{cardTags?.secondary}
</div>
)}

View File

@@ -1,18 +1,17 @@
"use client"
'use client'
import { HtmlHTMLAttributes } from "react"
import Link from "next/link"
import { HtmlHTMLAttributes } from 'react'
import Link from 'next/link'
import {
FilterLabelMapping,
ProjectFilter,
} from "@/state/useProjectFiltersState"
} from '@/state/useProjectFiltersState'
import { ProjectInterface } from "@/lib/types"
import { useTranslation } from "@/app/i18n/client"
import { LocaleTypes } from "@/app/i18n/settings"
import { ProjectInterface } from '@/lib/types'
import { useTranslation } from '@/app/i18n/client'
import { LocaleTypes } from '@/app/i18n/settings'
import { CategoryTag } from "../ui/categoryTag"
import { ThemesStatusMapping } from "./project-filters-bar"
import { CategoryTag } from '../ui/categoryTag'
interface TagsProps extends HtmlHTMLAttributes<HTMLDivElement> {
label: string
@@ -33,9 +32,7 @@ type IProjectTags = {
}
export function ProjectTags({ project, lang }: IProjectTags) {
const statusItem = ThemesStatusMapping(lang)
const { label, icon } = statusItem?.[project?.projectStatus] ?? {}
const { t } = useTranslation(lang, "common")
const { t } = useTranslation(lang, 'common')
return (
<div className="flex flex-col gap-4 pt-10">
@@ -43,7 +40,7 @@ export function ProjectTags({ project, lang }: IProjectTags) {
const keyTags = project?.tags?.[key as ProjectFilter]
const hasItems = keyTags && keyTags?.length > 0
if (["themes", "builtWith"].includes(key)) return null // keys to ignore
if (['themes', 'builtWith'].includes(key)) return null // keys to ignore
return (
hasItems && (
<div data-section-id={key} key={key}>

View File

@@ -1,17 +1,17 @@
"use client"
'use client'
import React from "react"
import Link from "next/link"
import React from 'react'
import Link from 'next/link'
import {
ActionLinkTypeLink,
ProjectExtraLinkType,
ProjectInterface,
} from "@/lib/types"
import { useTranslation } from "@/app/i18n/client"
import { LocaleTypes } from "@/app/i18n/settings"
} from '@/lib/types'
import { useTranslation } from '@/app/i18n/client'
import { LocaleTypes } from '@/app/i18n/settings'
import { Icons } from "../icons"
import { Icons } from '../icons'
interface ProjectExtraLinksProps {
project: ProjectInterface
@@ -27,7 +27,7 @@ export default function ProjectExtraLinks({
project,
lang,
}: ProjectExtraLinksProps) {
const { t } = useTranslation(lang, "common")
const { t } = useTranslation(lang, 'common')
const { extraLinks = {} } = project
const hasExtraLinks = Object.keys(extraLinks).length > 0
@@ -39,26 +39,26 @@ export default function ProjectExtraLinks({
}
> = {
buildWith: {
label: t("buildWithThisTool"),
label: t('buildWithThisTool'),
icon: <Icons.hammer />,
},
play: {
label: t("tryItOut"),
label: t('tryItOut'),
icon: <Icons.hand />,
},
research: {
label: t("deepDiveResearch"),
label: t('deepDiveResearch'),
icon: <Icons.readme />,
},
learn: {
label: t("learnMore"),
label: t('learnMore'),
},
}
if (!hasExtraLinks) return null
const ExtraLinkItems = ({ id, links = [] }: ExtraLinkItemsProps) => {
const { label, icon } = ExtraLinkLabelMapping[id]
const { label } = ExtraLinkLabelMapping[id]
if (!links.length) return null // no links hide the section

View File

@@ -1,20 +1,20 @@
"use client"
'use client'
import React, { ChangeEvent, ReactNode, useEffect, useState } from "react"
import Image from "next/image"
import { useRouter, useSearchParams } from "next/navigation"
import { projects } from "@/data/projects"
import FiltersIcon from "@/public/icons/filters.svg"
import React, { ChangeEvent, ReactNode, useEffect, useState } from 'react'
import Image from 'next/image'
import { useRouter, useSearchParams } from 'next/navigation'
import { projects } from '@/data/projects'
import FiltersIcon from '@/public/icons/filters.svg'
import {
FilterLabelMapping,
FilterTypeMapping,
ProjectFilter,
useProjectFiltersState,
} from "@/state/useProjectFiltersState"
import i18next from "i18next"
import { useDebounce } from "react-use"
} from '@/state/useProjectFiltersState'
import i18next from 'i18next'
import { useDebounce } from 'react-use'
import { IThemeStatus, IThemesButton, LangProps } from "@/types/common"
import { IThemeStatus, IThemesButton, LangProps } from '@/types/common'
import {
ProjectCategories,
ProjectCategory,
@@ -22,18 +22,18 @@ import {
ProjectSections,
ProjectStatus,
ProjectStatusLabelMapping,
} from "@/lib/types"
import { cn, queryStringToObject } from "@/lib/utils"
import { useTranslation } from "@/app/i18n/client"
import { LocaleTypes } from "@/app/i18n/settings"
} from '@/lib/types'
import { cn, queryStringToObject } from '@/lib/utils'
import { useTranslation } from '@/app/i18n/client'
import { LocaleTypes } from '@/app/i18n/settings'
import { Icons } from "../icons"
import Badge from "../ui/badge"
import { Button } from "../ui/button"
import { CategoryTag } from "../ui/categoryTag"
import { Checkbox } from "../ui/checkbox"
import { Input } from "../ui/input"
import { Modal } from "../ui/modal"
import { Icons } from '../icons'
import Badge from '../ui/badge'
import { Button } from '../ui/button'
import { CategoryTag } from '../ui/categoryTag'
import { Checkbox } from '../ui/checkbox'
import { Input } from '../ui/input'
import { Modal } from '../ui/modal'
interface FilterWrapperProps {
label: string
@@ -43,7 +43,7 @@ interface FilterWrapperProps {
const FilterWrapper = ({ label, children, className }: FilterWrapperProps) => {
return (
<div className={cn("flex flex-col gap-4 py-6", className)}>
<div className={cn('flex flex-col gap-4 py-6', className)}>
<span className="text-xl font-bold">{label}</span>
{children}
</div>
@@ -51,45 +51,45 @@ const FilterWrapper = ({ label, children, className }: FilterWrapperProps) => {
}
export const ThemesButtonMapping = (lang: LocaleTypes): IThemesButton => {
const t = i18next.getFixedT(lang, "all")
const t = i18next.getFixedT(lang, 'all')
return {
build: {
label: t("tags.build"),
label: t('tags.build'),
icon: <Icons.hammer />,
},
play: {
label: t("tags.play"),
label: t('tags.play'),
icon: <Icons.hand />,
},
research: {
label: t("tags.research"),
label: t('tags.research'),
icon: <Icons.readme />,
},
}
}
export const ThemesStatusMapping = (lang: LocaleTypes): IThemeStatus => {
const t = i18next.getFixedT(lang, "common")
const t = i18next.getFixedT(lang, 'common')
return {
active: {
label: t("status.active"),
label: t('status.active'),
icon: <Icons.checkActive />,
},
inactive: {
label: t("status.inactive"),
label: t('status.inactive'),
icon: <Icons.archived />,
},
}
}
export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
const { t } = useTranslation(lang as LocaleTypes, "common")
export default function ProjectFiltersBar({ lang }: LangProps['params']) {
const { t } = useTranslation(lang as LocaleTypes, 'common')
const [showModal, setShowModal] = useState(false)
const router = useRouter()
const searchParams = useSearchParams()
const [searchQuery, setSearchQuery] = useState("")
const [searchQuery, setSearchQuery] = useState('')
const [filterCount, setFilterCount] = useState(0)
const {
@@ -124,10 +124,10 @@ export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
const clearAllFilters = () => {
useProjectFiltersState.setState({
activeFilters: {},
queryString: "",
queryString: '',
projects,
})
setSearchQuery("") // clear input
setSearchQuery('') // clear input
router.push(`/projects`)
}
@@ -152,7 +152,7 @@ export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
size="sm"
onClick={clearAllFilters}
>
{t("clearAll")}
{t('clearAll')}
</Button>
<div className="ml-auto">
<Button
@@ -160,7 +160,7 @@ export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
size="sm"
onClick={() => setShowModal(false)}
>
{t("showProjects")}
{t('showProjects')}
</Button>
</div>
</div>
@@ -171,30 +171,30 @@ export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
<div className="flex flex-col divide-y divide-tuatara-200">
{Object.entries(filters).map(([key, items]) => {
const filterLabel =
FilterLabelMapping(lang)?.[key as ProjectFilter] ?? ""
FilterLabelMapping(lang)?.[key as ProjectFilter] ?? ''
const type = FilterTypeMapping?.[key as ProjectFilter]
const hasItems = items.length > 0
const hasActiveThemeFilters =
(activeFilters?.themes ?? [])?.length > 0
if (key === "themes" && !hasActiveThemeFilters) return null
if (key === 'themes' && !hasActiveThemeFilters) return null
return (
hasItems && (
<FilterWrapper key={key} label={filterLabel}>
<div
className={cn("gap-y-2", {
"grid grid-cols-1 gap-2 md:grid-cols-3":
type === "checkbox",
"flex gap-x-4 flex-wrap": type === "button",
className={cn('gap-y-2', {
'grid grid-cols-1 gap-2 md:grid-cols-3':
type === 'checkbox',
'flex gap-x-4 flex-wrap': type === 'button',
})}
>
{items.map((item) => {
{items.map((item, index) => {
const isActive =
activeFilters?.[key as ProjectFilter]?.includes(item)
if (type === "checkbox") {
if (type === 'checkbox') {
return (
<Checkbox
key={item}
@@ -212,17 +212,17 @@ export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
)
}
if (type === "button") {
if (type === 'button') {
const { icon, label } = ThemesButtonMapping(lang)[item]
if (!isActive) return null
return (
<div>
<div key={index}>
<CategoryTag
variant="selected"
closable
onClose={() => {
toggleFilter({
tag: "themes",
tag: 'themes',
value: item,
searchQuery,
})
@@ -248,7 +248,7 @@ export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
})}
<FilterWrapper
className="hidden"
label={t("filterLabels.fundingSource")}
label={t('filterLabels.fundingSource')}
>
{ProjectSections.map((section) => {
const label = ProjectSectionLabelMapping[section]
@@ -257,10 +257,10 @@ export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
</FilterWrapper>
<FilterWrapper
className="hidden"
label={t("filterLabels.projectStatus")}
label={t('filterLabels.projectStatus')}
>
{Object.keys(ProjectStatus).map((section: any) => {
// @ts-ignore
// @ts-expect-error - ProjectStatusLabelMapping is not typed
const label = ProjectStatusLabelMapping?.[section]
return <Checkbox key={section} name={section} label={label} />
})}
@@ -272,10 +272,10 @@ export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
<ul className="flex space-x-6">
<div
className={cn(
"relative block px-2 py-1 text-sm font-medium uppercase transition-colors cursor-pointer hover:text-primary",
'relative block px-2 py-1 text-sm font-medium uppercase transition-colors cursor-pointer hover:text-primary',
currentCategory == null
? "text-sky-400 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:bg-sky-400"
: ""
? 'text-sky-400 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:bg-sky-400'
: ''
)}
onClick={() => setCurrentCategory(null)}
>
@@ -286,10 +286,10 @@ export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
<div
key={key}
className={cn(
"relative block px-2 py-1 text-sm font-medium uppercase transition-colors cursor-pointer hover:text-primary",
'relative block px-2 py-1 text-sm font-medium uppercase transition-colors cursor-pointer hover:text-primary',
currentCategory === key
? "text-sky-400 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:bg-sky-400"
: ""
? 'text-sky-400 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:bg-sky-400'
: ''
)}
onClick={() => setCurrentCategory(key as ProjectCategory)}
>
@@ -310,7 +310,7 @@ export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
})
}}
value={searchQuery}
placeholder={t("searchProjectPlaceholder")}
placeholder={t('searchProjectPlaceholder')}
/>
<div className="flex items-center gap-3">
<Badge value={filterCount}>
@@ -318,12 +318,12 @@ export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
onClick={() => setShowModal(true)}
variant="white"
className={cn({
"border-2 border-anakiwa-950": filterCount > 0,
'border-2 border-anakiwa-950': filterCount > 0,
})}
>
<div className="flex items-center gap-2">
<Image src={FiltersIcon} alt="filter icon" />
<span className="hidden md:block">{t("filters")}</span>
<span className="hidden md:block">{t('filters')}</span>
</div>
</Button>
</Badge>
@@ -333,7 +333,7 @@ export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
className="hidden bg-transparent cursor-pointer opacity-85 text-primary hover:opacity-100 disabled:pointer-events-none disabled:opacity-50 md:block"
>
<div className="flex items-center gap-2 border-b-2 border-black">
<span className="text-sm font-medium">{t("clearAll")}</span>
<span className="text-sm font-medium">{t('clearAll')}</span>
</div>
</button>
</div>

View File

@@ -1,12 +1,12 @@
"use client"
'use client'
import React, { useCallback, useEffect, useRef, useState } from "react"
import Image from "next/image"
import NoResultIcon from "@/public/icons/no-result.svg"
import { useProjectFiltersState } from "@/state/useProjectFiltersState"
import { cva } from "class-variance-authority"
import React, { useCallback, useEffect, useRef, useState } from 'react'
import Image from 'next/image'
import NoResultIcon from '@/public/icons/no-result.svg'
import { useProjectFiltersState } from '@/state/useProjectFiltersState'
import { cva } from 'class-variance-authority'
import { LangProps } from "@/types/common"
import { LangProps } from '@/types/common'
import {
ProjectInterface,
ProjectSection,
@@ -14,18 +14,18 @@ import {
ProjectSections,
ProjectStatus,
ProjectStatusDescriptionMapping,
} from "@/lib/types"
import { cn } from "@/lib/utils"
import { useTranslation } from "@/app/i18n/client"
} from '@/lib/types'
import { cn } from '@/lib/utils'
import { useTranslation } from '@/app/i18n/client'
import ProjectCard from "./project-card"
import ProjectCard from './project-card'
const sectionTitleClass = cva(
"relative font-sans text-base font-bold uppercase tracking-[3.36px] text-anakiwa-950 after:ml-8 after:absolute after:top-1/2 after:h-[1px] after:w-full after:translate-y-1/2 after:bg-anakiwa-300 after:content-['']"
)
const NoResults = ({ lang }: LangProps["params"]) => {
const { t } = useTranslation(lang, "common")
const NoResults = ({ lang }: LangProps['params']) => {
const { t } = useTranslation(lang, 'common')
return (
<div className="flex flex-col gap-2 pt-24 pb-40 text-center">
@@ -33,21 +33,21 @@ const NoResults = ({ lang }: LangProps["params"]) => {
<Image className="h-9 w-9" src={NoResultIcon} alt="no result icon" />
</div>
<span className="text-2xl font-bold font-display text-tuatara-950">
{t("noResults")}
{t('noResults')}
</span>
<span className="text-lg font-normal text-tuatara-950">
{t("noResultsDescription")}
{t('noResultsDescription')}
</span>
</div>
)
}
const ProjectStatusOrderList = ["active", "maintained", "inactive"]
const ProjectStatusOrderList = ['active', 'maintained', 'inactive']
export const ProjectList = ({ lang }: LangProps["params"]) => {
const { t } = useTranslation(lang, "resources-page")
export const ProjectList = ({ lang }: LangProps['params']) => {
const { t } = useTranslation(lang, 'resources-page')
const SCROLL_OFFSET = -400
const [activeId, setActiveId] = useState("")
const [activeId, setActiveId] = useState('')
const [isManualScroll, setIsManualScroll] = useState(false)
const [isMounted, setIsMounted] = useState(false)
@@ -61,7 +61,7 @@ export const ProjectList = ({ lang }: LangProps["params"]) => {
useEffect(() => {
setIsMounted(true)
if (typeof window !== "undefined") {
if (typeof window !== 'undefined') {
sectionsRef.current = document.querySelectorAll(`div[data-section]`)
const handleScroll = () => {
@@ -70,18 +70,18 @@ export const ProjectList = ({ lang }: LangProps["params"]) => {
sectionsRef.current?.forEach((section: any) => {
const sectionTop = section.offsetTop - SCROLL_OFFSET
if (window.scrollY >= sectionTop && window.scrollY > 0) {
setActiveId(section.getAttribute("id"))
setActiveId(section.getAttribute('id'))
}
})
}
window.addEventListener("scroll", handleScroll)
return () => window.removeEventListener("scroll", handleScroll)
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}
}, [SCROLL_OFFSET, isManualScroll])
const scrollToId = useCallback((id: string) => {
if (typeof window !== "undefined") {
if (typeof window !== 'undefined') {
const element = document.getElementById(id)
const top = element?.offsetTop ?? 0
@@ -89,7 +89,7 @@ export const ProjectList = ({ lang }: LangProps["params"]) => {
setActiveId(id) // active clicked id
setIsManualScroll(true) // tell the window event listener to ignore this scrolling
window?.scrollTo({
behavior: "smooth",
behavior: 'smooth',
top: (top ?? 0) - SCROLL_OFFSET,
})
}
@@ -98,7 +98,7 @@ export const ProjectList = ({ lang }: LangProps["params"]) => {
}
}, [])
const hasActiveFilters = searchQuery !== "" || queryString !== ""
const hasActiveFilters = searchQuery !== '' || queryString !== ''
// loading state skeleton
if (!isMounted) {
@@ -122,13 +122,16 @@ export const ProjectList = ({ lang }: LangProps["params"]) => {
if (noItems) return <NoResults lang={lang} />
const projectsGroupByStatus = projects.reduce((acc, project) => {
acc[project.projectStatus] = [
...(acc[project.projectStatus] || []),
project,
]
return acc
}, {} as Record<ProjectStatus, ProjectInterface[]>)
const projectsGroupByStatus = projects.reduce(
(acc, project) => {
acc[project.projectStatus] = [
...(acc[project.projectStatus] || []),
project,
]
return acc
},
{} as Record<ProjectStatus, ProjectInterface[]>
)
// show all projects without sections if there are active filters
if (hasActiveFilters) {
@@ -151,7 +154,7 @@ export const ProjectList = ({ lang }: LangProps["params"]) => {
return (
<div className="relative grid items-start justify-between grid-cols-1">
<div className="flex flex-col">
{ProjectStatusOrderList.map((status) => {
{ProjectStatusOrderList.map((status, index) => {
const projects = projectsGroupByStatus[status as ProjectStatus] ?? []
const description =
ProjectStatusDescriptionMapping?.[status as ProjectStatus]
@@ -161,8 +164,12 @@ export const ProjectList = ({ lang }: LangProps["params"]) => {
if (!hasProjects) return null // no projects for this status, hide the section
return (
<div data-section={status} className="flex justify-between gap-10">
<div className={cn("flex w-full flex-col gap-10 pt-10")}>
<div
key={index}
data-section={status}
className="flex justify-between gap-10"
>
<div className={cn('flex w-full flex-col gap-10 pt-10')}>
{!hasActiveFilters && (
<div className="flex flex-col gap-6 overflow-hidden">
<h3 className={cn(sectionTitleClass())}>{status}</h3>
@@ -192,7 +199,7 @@ export const ProjectList = ({ lang }: LangProps["params"]) => {
<div id="sidebar" className="sticky hidden p-8 top-20 bg-white/30">
<div className="flex flex-col gap-4">
<h6 className="text-lg font-bold font-display text-tuatara-700">
{t("onThisPage")}
{t('onThisPage')}
</h6>
<ul className="font-sans text-black text-normal">
{ProjectSections.map((id: ProjectSection) => {
@@ -210,9 +217,9 @@ export const ProjectList = ({ lang }: LangProps["params"]) => {
}}
data-id={id}
className={cn(
"flex h-8 cursor-pointer items-center border-l-2 border-l-anakiwa-200 px-3 duration-200",
'flex h-8 cursor-pointer items-center border-l-2 border-l-anakiwa-200 px-3 duration-200',
{
"border-l-anakiwa-500 text-anakiwa-500 font-medium":
'border-l-anakiwa-500 text-anakiwa-500 font-medium':
active,
}
)}

View File

@@ -1,44 +1,44 @@
"use client"
'use client'
import {
DEFAULT_PROJECT_SORT_BY,
ProjectFilter,
ProjectSortBy,
useProjectFiltersState,
} from "@/state/useProjectFiltersState"
} from '@/state/useProjectFiltersState'
import { LangProps } from "@/types/common"
import { useTranslation } from "@/app/i18n/client"
import { LangProps } from '@/types/common'
import { useTranslation } from '@/app/i18n/client'
import { CategoryTag } from "../ui/categoryTag"
import { Dropdown } from "../ui/dropdown"
import { CategoryTag } from '../ui/categoryTag'
import { Dropdown } from '../ui/dropdown'
const labelClass = "h-5 text-xs text-base md:h-6 text-slate-900/70 md:text-sm"
const labelClass = 'h-5 text-xs text-base md:h-6 text-slate-900/70 md:text-sm'
export const ProjectResultBar = ({ lang }: LangProps["params"]) => {
const { t } = useTranslation(lang, "common")
export const ProjectResultBar = ({ lang }: LangProps['params']) => {
const { t } = useTranslation(lang, 'common')
const { activeFilters, toggleFilter, projects, sortProjectBy, sortBy } =
useProjectFiltersState((state) => state)
const haveActiveFilters = Object.entries(activeFilters).some(
([_key, values]) => values?.length > 0
([, values]) => values?.length > 0
)
const resultLabel = t(
haveActiveFilters ? "showingProjectsWith" : "showingProjects",
haveActiveFilters ? 'showingProjectsWith' : 'showingProjects',
{
count: projects?.length,
}
)
const projectSortItems: { label: string; value: ProjectSortBy }[] = [
{ label: t("filterOptions.random"), value: "random" },
{ label: t("filterOptions.asc"), value: "asc" },
{ label: t("filterOptions.desc"), value: "desc" },
{ label: t('filterOptions.random'), value: 'random' },
{ label: t('filterOptions.asc'), value: 'asc' },
{ label: t('filterOptions.desc'), value: 'desc' },
// { label: t("filterOptions.relevance"), value: "relevance" },
]
const activeSortOption = t("sortBy", {
const activeSortOption = t('sortBy', {
option: t(`filterOptions.${sortBy}`),
})

View File

@@ -1,16 +1,16 @@
import { useEffect, useState } from "react"
import Link from "next/link"
import React from 'react'
import { useEffect, useState } from 'react'
import Link from 'next/link'
import { siteConfig } from "@/config/site"
import { AnnounceInterface } from "@/lib/types"
import { convertDirtyStringToHtml } from "@/lib/utils"
import { useTranslation } from "@/app/i18n/client"
import { LocaleTypes } from "@/app/i18n/settings"
import { siteConfig } from '@/config/site'
import { AnnounceInterface } from '@/lib/types'
import { convertDirtyStringToHtml } from '@/lib/utils'
import { useTranslation } from '@/app/i18n/client'
import { LocaleTypes } from '@/app/i18n/settings'
import { Icons } from "../icons"
import { AppContent } from "../ui/app-content"
const HtmlToReactParser = require("html-to-react").Parser
import { Icons } from '../icons'
import { AppContent } from '../ui/app-content'
import { Parser as HtmlToReactParser } from 'html-to-react'
interface NewsSectionProps {
lang: LocaleTypes
@@ -28,7 +28,7 @@ const ContentPlaceholder = () => (
)
export const NewsSection = ({ lang }: NewsSectionProps) => {
const { t } = useTranslation(lang, "news-section")
const { t } = useTranslation(lang, 'news-section')
const [newsItems, setNewsItems] = useState<AnnounceInterface[]>([])
const [loading, setLoading] = useState(true)
@@ -36,7 +36,7 @@ export const NewsSection = ({ lang }: NewsSectionProps) => {
useEffect(() => {
const getDiscordAnnouncements = async () => {
setLoading(true)
await fetch("/api/news")
await fetch('/api/news')
.then((res) => res.json())
.then(({ announcements }: { announcements: AnnounceInterface[] }) => {
setNewsItems(announcements ?? [])
@@ -49,6 +49,7 @@ export const NewsSection = ({ lang }: NewsSectionProps) => {
const [news] = newsItems ?? []
// @ts-expect-error - HtmlToReactParser is not typed
const htmlToReactParser = new HtmlToReactParser()
const announcementContent = htmlToReactParser.parse(
convertDirtyStringToHtml(news?.content)
@@ -62,7 +63,7 @@ export const NewsSection = ({ lang }: NewsSectionProps) => {
<div className="bg-white py-16">
<div className="flex flex-col gap-10 ">
<h3 className="text-base font-bold font-sans text-center uppercase tracking-[3.36px]">
{t("recentUpdates")}
{t('recentUpdates')}
</h3>
<AppContent className="mx-auto flex max-w-[978px] flex-col gap-4">
<div className="flex gap-6 flex-col border border-tuatara-950 bg-anakiwa-100 p-6 rounded-[8px] ">
@@ -71,9 +72,9 @@ export const NewsSection = ({ lang }: NewsSectionProps) => {
news?.timestamp && (
<span className="text-anakiwa-600 text-lg font-bold font-display">
{new Intl.DateTimeFormat(lang, {
month: "long",
day: "numeric",
year: "numeric",
month: 'long',
day: 'numeric',
year: 'numeric',
}).format(new Date(news?.timestamp))}
</span>
)
@@ -90,8 +91,8 @@ export const NewsSection = ({ lang }: NewsSectionProps) => {
>
<Icons.twitter size={24} className="text-anakiwa-500" />
<span className="flex text-anakiwa-900 underline font-medium leading-[24px]">
{t("repostOnSocial", {
socialName: "X",
{t('repostOnSocial', {
socialName: 'X',
})}
</span>
</Link>
@@ -108,7 +109,7 @@ export const NewsSection = ({ lang }: NewsSectionProps) => {
passHref
>
<Icons.discord className="text-anakiwa-400" />
<span>{t("seeAllUpdates")}</span>
<span>{t('seeAllUpdates')}</span>
<Icons.externalUrl className="text-tuatara-950" />
</Link>
</AppContent>
@@ -117,4 +118,4 @@ export const NewsSection = ({ lang }: NewsSectionProps) => {
)
}
NewsSection.displayName = "NewsSection"
NewsSection.displayName = 'NewsSection'

View File

@@ -1,4 +1,4 @@
import React from "react"
import React from 'react'
interface MySvgProps {
color: string
@@ -33,7 +33,7 @@ export const Global = ({ color }: MySvgProps) => {
viewBox="0 0 25 24"
xmlns="http://www.w3.org/2000/svg"
>
<g id="global-line" clip-path="url(#clip0_3593_4165)">
<g id="global-line" clipPath="url(#clip0_3593_4165)">
<path
id="Vector"
d="M12.5205 22C6.99751 22 2.52051 17.523 2.52051 12C2.52051 6.477 6.99751 2 12.5205 2C18.0435 2 22.5205 6.477 22.5205 12C22.5205 17.523 18.0435 22 12.5205 22ZM10.2305 19.667C9.24392 17.5743 8.67238 15.3102 8.54751 13H4.58251C4.77709 14.5389 5.41443 15.9882 6.41708 17.1717C7.41972 18.3552 8.74452 19.2221 10.2305 19.667ZM10.5505 13C10.7015 15.439 11.3985 17.73 12.5205 19.752C13.6728 17.6766 14.346 15.3695 14.4905 13H10.5505ZM20.4585 13H16.4935C16.3686 15.3102 15.7971 17.5743 14.8105 19.667C16.2965 19.2221 17.6213 18.3552 18.6239 17.1717C19.6266 15.9882 20.2639 14.5389 20.4585 13ZM4.58251 11H8.54751C8.67238 8.68979 9.24392 6.42569 10.2305 4.333C8.74452 4.77788 7.41972 5.64475 6.41708 6.8283C5.41443 8.01184 4.77709 9.4611 4.58251 11ZM10.5515 11H14.4895C14.3453 8.6306 13.6725 6.32353 12.5205 4.248C11.3682 6.32345 10.6951 8.63052 10.5505 11H10.5515ZM14.8105 4.333C15.7971 6.42569 16.3686 8.68979 16.4935 11H20.4585C20.2639 9.4611 19.6266 8.01184 18.6239 6.8283C17.6213 5.64475 16.2965 4.77788 14.8105 4.333Z"

View File

@@ -1,9 +1,9 @@
import React from "react"
import ReactMarkdown, { Components } from "react-markdown"
import remarkGfm from "remark-gfm"
import React from 'react'
import ReactMarkdown, { Components } from 'react-markdown'
import remarkGfm from 'remark-gfm'
const generateSectionId = (text: string) => {
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-")
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-')
}
export const createMarkdownElement = (
@@ -15,7 +15,7 @@ export const createMarkdownElement = (
ref: (node: HTMLElement | null) => {
if (node && node.textContent) {
node.setAttribute(
"data-section-id",
'data-section-id',
generateSectionId(node.textContent)
)
}
@@ -32,57 +32,57 @@ const Table = (props: any) => {
// Styling for HTML attributes for markdown component
const REACT_MARKDOWN_CONFIG: Components = {
a: ({ node, ...props }) =>
createMarkdownElement("a", {
className: "text-anakiwa-500 hover:text-orange duration-200",
target: "_blank",
a: ({ ...props }) =>
createMarkdownElement('a', {
className: 'text-anakiwa-500 hover:text-orange duration-200',
target: '_blank',
...props,
}),
h1: ({ node, ...props }) =>
createMarkdownElement("h1", {
className: "text-neutral-800 text-4xl md:text-5xl font-bold",
h1: ({ ...props }) =>
createMarkdownElement('h1', {
className: 'text-neutral-800 text-4xl md:text-5xl font-bold',
...props,
}),
h2: ({ node, ...props }) =>
createMarkdownElement("h2", {
className: "text-neutral-800 text-4xl",
h2: ({ ...props }) =>
createMarkdownElement('h2', {
className: 'text-neutral-800 text-4xl',
...props,
}),
h3: ({ node, ...props }) =>
createMarkdownElement("h3", {
className: "text-neutral-800 text-3xl",
h3: ({ ...props }) =>
createMarkdownElement('h3', {
className: 'text-neutral-800 text-3xl',
...props,
}),
h4: ({ node, ...props }) =>
createMarkdownElement("h4", {
className: "text-neutral-800 text-xl",
h4: ({ ...props }) =>
createMarkdownElement('h4', {
className: 'text-neutral-800 text-xl',
...props,
}),
h5: ({ node, ...props }) =>
createMarkdownElement("h5", {
className: "text-neutral-800 text-lg font-bold",
h5: ({ ...props }) =>
createMarkdownElement('h5', {
className: 'text-neutral-800 text-lg font-bold',
...props,
}),
h6: ({ node, ...props }) =>
createMarkdownElement("h6", {
className: "text-neutral-800 text-md font-bold",
h6: ({ ...props }) =>
createMarkdownElement('h6', {
className: 'text-neutral-800 text-md font-bold',
...props,
}),
p: ({ node, ...props }) =>
createMarkdownElement("p", {
className: "text-tuatara-700 font-sans text-base font-normal",
p: ({ ...props }) =>
createMarkdownElement('p', {
className: 'text-tuatara-700 font-sans text-base font-normal',
...props,
}),
ul: ({ node, ordered, ...props }) =>
createMarkdownElement(ordered ? "ol" : "ul", {
ul: ({ ordered, ...props }) =>
createMarkdownElement(ordered ? 'ol' : 'ul', {
className:
"ml-6 list-disc text-tuatara-700 font-sans text-base font-normal",
'ml-6 list-disc text-tuatara-700 font-sans text-base font-normal',
...props,
}),
ol: ({ node, ordered, ...props }) =>
createMarkdownElement(ordered ? "ol" : "ul", {
ol: ({ ordered, ...props }) =>
createMarkdownElement(ordered ? 'ol' : 'ul', {
className:
"ml-6 list-disc text-tuatara-700 font-sans text-base font-normal",
'ml-6 list-disc text-tuatara-700 font-sans text-base font-normal',
...props,
}),
table: Table,

View File

@@ -1,12 +1,12 @@
"use client"
'use client'
import { useEffect, useRef, useState } from "react"
import { useEffect, useRef, useState } from 'react'
import { ProjectExtraLinkType } from "@/lib/types"
import { cn } from "@/lib/utils"
import { useTranslation } from "@/app/i18n/client"
import { ProjectExtraLinkType } from '@/lib/types'
import { cn } from '@/lib/utils'
import { useTranslation } from '@/app/i18n/client'
import { Icons } from "./icons"
import { Icons } from './icons'
interface Section {
level: number
@@ -23,11 +23,11 @@ interface WikiSideNavigationProps {
export const WikiSideNavigation = ({
className,
lang = "en",
content = "",
lang = 'en',
content = '',
project,
}: WikiSideNavigationProps) => {
const { t } = useTranslation(lang, "common")
const { t } = useTranslation(lang, 'common')
const [sections, setSections] = useState<Section[]>([])
const [activeSection, setActiveSection] = useState<string | null>(null)
const observerRef = useRef<IntersectionObserver | null>(null)
@@ -45,7 +45,7 @@ export const WikiSideNavigation = ({
extractedSections.push({
level: match[1].length,
text,
id: text.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
id: text.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
})
}
}
@@ -61,14 +61,14 @@ export const WikiSideNavigation = ({
useEffect(() => {
const observerOptions = {
root: null,
rootMargin: "-20% 0px -80% 0px",
rootMargin: '-20% 0px -80% 0px',
threshold: 0,
}
observerRef.current = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveSection(entry.target.getAttribute("data-section-id"))
setActiveSection(entry.target.getAttribute('data-section-id'))
}
})
}, observerOptions)
@@ -96,7 +96,7 @@ export const WikiSideNavigation = ({
window.scrollTo({
top: offsetPosition,
behavior: "smooth",
behavior: 'smooth',
})
setActiveSection(sectionId)
}
@@ -110,41 +110,40 @@ export const WikiSideNavigation = ({
}
> = {
buildWith: {
label: t("buildWith"),
label: t('buildWith'),
icon: <Icons.hammer />,
},
play: {
label: t("tryItOut"),
label: t('tryItOut'),
icon: <Icons.hand />,
},
research: {
label: t("deepDiveResearch"),
label: t('deepDiveResearch'),
icon: <Icons.readme />,
},
learn: {
label: t("learnMore"),
label: t('learnMore'),
},
}
const { extraLinks = {} } = project
const hasExtraLinks = Object.keys(extraLinks).length > 0
if (sections.length === 0 || content.length === 0) return null
return (
<div className="sticky overflow-hidden top-20">
<aside className={cn("flex flex-col", className)}>
<aside className={cn('flex flex-col', className)}>
<h6 className="text-lg font-bold font-display text-tuatara-700">
{t("contents")}
{t('contents')}
</h6>
<ul className="pt-4 font-sans text-black text-normal">
{sections.map((section, index) => (
<li
key={index}
className={cn(
"flex h-8 items-center border-l-2 border-l-anakiwa-200 px-3 duration-200 cursor-pointer ",
'flex h-8 items-center border-l-2 border-l-anakiwa-200 px-3 duration-200 cursor-pointer ',
{
"border-l-anakiwa-500 text-anakiwa-500 font-medium":
'border-l-anakiwa-500 text-anakiwa-500 font-medium':
activeSection === section.id,
}
)}
@@ -159,7 +158,7 @@ export const WikiSideNavigation = ({
))}
{Object.entries(ExtraLinkLabelMapping).map(([key]) => {
const links = extraLinks[key as ProjectExtraLinkType] ?? []
// @ts-ignore
// @ts-expect-error - ExtraLinkLabelMapping is not typed
const { label } = ExtraLinkLabelMapping?.[key as any] ?? {}
if (!links.length) return null // no links hide the section
return (
@@ -167,9 +166,9 @@ export const WikiSideNavigation = ({
key={key}
onClick={() => scrollToSection(key)}
className={cn(
"flex h-8 items-center border-l-2 border-l-anakiwa-200 px-3 duration-200 cursor-pointer",
'flex h-8 items-center border-l-2 border-l-anakiwa-200 px-3 duration-200 cursor-pointer',
{
"border-l-anakiwa-500 text-anakiwa-500 font-medium":
'border-l-anakiwa-500 text-anakiwa-500 font-medium':
activeSection === key,
}
)}
@@ -180,12 +179,12 @@ export const WikiSideNavigation = ({
})}
<li
key="edit"
onClick={() => scrollToSection("edit-this-page")}
onClick={() => scrollToSection('edit-this-page')}
className={cn(
"flex h-8 items-center border-l-2 border-l-anakiwa-200 px-3 duration-200 cursor-pointer",
'flex h-8 items-center border-l-2 border-l-anakiwa-200 px-3 duration-200 cursor-pointer',
{
"border-l-anakiwa-500 text-anakiwa-500 font-medium":
activeSection === "edit-this-page",
'border-l-anakiwa-500 text-anakiwa-500 font-medium':
activeSection === 'edit-this-page',
}
)}
>