mirror of
https://github.com/privacy-scaling-explorations/pse.dev.git
synced 2026-04-23 03:01:03 -04:00
Merge branch 'main' into blog-section
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ReactNode } from "react"
|
||||
|
||||
import { AppContent } from './ui/app-content'
|
||||
import { AppContent } from "./ui/app-content"
|
||||
|
||||
type BannerProps = {
|
||||
title: ReactNode
|
||||
@@ -14,7 +14,7 @@ const Banner = ({ title, subtitle, children }: BannerProps) => {
|
||||
<div className="py-16">
|
||||
<AppContent className="flex flex-col gap-6">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
{typeof title === 'string' ? (
|
||||
{typeof title === "string" ? (
|
||||
<h6 className="py-4 font-sans text-base font-bold uppercase tracking-[4px] text-tuatara-950">
|
||||
{title}
|
||||
</h6>
|
||||
@@ -30,6 +30,6 @@ const Banner = ({ title, subtitle, children }: BannerProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
Banner.displayName = 'Banner'
|
||||
Banner.displayName = "Banner"
|
||||
|
||||
export { Banner }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import NextLink from 'next/link'
|
||||
import NextLink from "next/link"
|
||||
|
||||
interface Props {
|
||||
path: string[]
|
||||
@@ -17,8 +17,8 @@ const Breadcrumbs = ({ path }: Props) => {
|
||||
)
|
||||
}
|
||||
return (
|
||||
<NextLink key={index} href={`/${item}`} className={'capitalize'}>
|
||||
{item === 'projects' ? 'All Projects' : item}
|
||||
<NextLink key={index} href={`/${item}`} className={"capitalize"}>
|
||||
{item === "projects" ? "All Projects" : item}
|
||||
<span className="mx-2">/</span>
|
||||
</NextLink>
|
||||
)
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { HTMLAttributes } from 'react'
|
||||
import { HTMLAttributes } from "react"
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Section = ({ children, className }: HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div className={cn('divide-y divide-tuatara-300 w-full', className)}>
|
||||
<div className={cn("divide-y divide-tuatara-300 w-full", className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Divider = {
|
||||
displayName: 'Divider',
|
||||
displayName: "Divider",
|
||||
Section,
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
SunMedium,
|
||||
X,
|
||||
type Icon as LucideIcon,
|
||||
} from 'lucide-react'
|
||||
} from "lucide-react"
|
||||
|
||||
export type Icon = LucideIcon
|
||||
|
||||
@@ -114,9 +114,9 @@ export const Icons = {
|
||||
<svg
|
||||
width={props.width}
|
||||
height={
|
||||
typeof props.height === 'number'
|
||||
typeof props.height === "number"
|
||||
? props.height
|
||||
: typeof props.width === 'number'
|
||||
: typeof props.width === "number"
|
||||
? props.width - 1
|
||||
: 18
|
||||
}
|
||||
@@ -151,9 +151,9 @@ 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'
|
||||
: typeof props.width === "number"
|
||||
? props.width - 2
|
||||
: 22
|
||||
}
|
||||
@@ -172,9 +172,9 @@ 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'
|
||||
: typeof props.width === "number"
|
||||
? props.width + 2
|
||||
: 26
|
||||
}
|
||||
@@ -209,9 +209,9 @@ export const Icons = {
|
||||
<svg
|
||||
width={props.size}
|
||||
height={
|
||||
typeof props.size === 'number'
|
||||
typeof props.size === "number"
|
||||
? props.size
|
||||
: typeof props.size === 'number'
|
||||
: typeof props.size === "number"
|
||||
? props.size + 1
|
||||
: 17
|
||||
}
|
||||
@@ -235,9 +235,9 @@ export const Icons = {
|
||||
<svg
|
||||
width={props.width}
|
||||
height={
|
||||
typeof props.height === 'number'
|
||||
typeof props.height === "number"
|
||||
? props.height
|
||||
: typeof props.width === 'number'
|
||||
: typeof props.width === "number"
|
||||
? props.width - 1
|
||||
: 19
|
||||
}
|
||||
@@ -256,9 +256,9 @@ export const Icons = {
|
||||
<svg
|
||||
width={props.width}
|
||||
height={
|
||||
typeof props.height === 'number'
|
||||
typeof props.height === "number"
|
||||
? props.height
|
||||
: typeof props.width === 'number'
|
||||
: typeof props.width === "number"
|
||||
? props.width - 1
|
||||
: 19
|
||||
}
|
||||
@@ -277,9 +277,9 @@ export const Icons = {
|
||||
<svg
|
||||
width={props.width}
|
||||
height={
|
||||
typeof props.height === 'number'
|
||||
typeof props.height === "number"
|
||||
? props.height
|
||||
: typeof props.width === 'number'
|
||||
: typeof props.width === "number"
|
||||
? props.width + 1
|
||||
: 15
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Icons } from '../icons'
|
||||
import { Icons } from "../icons"
|
||||
|
||||
export const ProjectLinkIconMap: Record<string, any> = {
|
||||
github: (
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { cn } from "@/lib/utils"
|
||||
import { LocaleTypes } from "@/app/i18n/settings"
|
||||
|
||||
import { ProjectLink } from "./project-link"
|
||||
import { ProjectLink } from "../mappings/project-link"
|
||||
|
||||
interface ProjectCardProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
|
||||
@@ -1,17 +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 { CategoryTag } from "../ui/categoryTag"
|
||||
|
||||
interface TagsProps extends HtmlHTMLAttributes<HTMLDivElement> {
|
||||
label: string
|
||||
@@ -32,7 +32,7 @@ type IProjectTags = {
|
||||
}
|
||||
|
||||
export function ProjectTags({ project, lang }: IProjectTags) {
|
||||
const { t } = useTranslation(lang, 'common')
|
||||
const { t } = useTranslation(lang, "common")
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 pt-10">
|
||||
@@ -40,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}>
|
||||
|
||||
@@ -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,19 +39,19 @@ 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"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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,11 +124,11 @@ export default function ProjectFiltersBar({ lang }: LangProps['params']) {
|
||||
const clearAllFilters = () => {
|
||||
useProjectFiltersState.setState({
|
||||
activeFilters: {},
|
||||
queryString: '',
|
||||
queryString: "",
|
||||
projects,
|
||||
})
|
||||
setSearchQuery('') // clear input
|
||||
router.push('/projects')
|
||||
setSearchQuery("") // clear input
|
||||
router.push("/projects")
|
||||
}
|
||||
|
||||
useDebounce(
|
||||
@@ -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, index) => {
|
||||
const isActive =
|
||||
activeFilters?.[key as ProjectFilter]?.includes(item)
|
||||
|
||||
if (type === 'checkbox') {
|
||||
if (type === "checkbox") {
|
||||
return (
|
||||
<Checkbox
|
||||
key={item}
|
||||
@@ -212,7 +212,7 @@ export default function ProjectFiltersBar({ lang }: LangProps['params']) {
|
||||
)
|
||||
}
|
||||
|
||||
if (type === 'button') {
|
||||
if (type === "button") {
|
||||
const { icon, label } = ThemesButtonMapping(lang)[item]
|
||||
if (!isActive) return null
|
||||
return (
|
||||
@@ -222,7 +222,7 @@ export default function ProjectFiltersBar({ lang }: LangProps['params']) {
|
||||
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,7 +257,7 @@ 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-expect-error - ProjectStatusLabelMapping is not typed
|
||||
@@ -272,24 +272,25 @@ 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)}
|
||||
>
|
||||
All
|
||||
</div>
|
||||
{ProjectCategories.map((key) => {
|
||||
if (key === ProjectCategory.RESEARCH) return null // Research category has now it's own page
|
||||
return (
|
||||
<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 +311,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 +319,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 +334,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>
|
||||
|
||||
@@ -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,8 +61,8 @@ export const ProjectList = ({ lang }: LangProps['params']) => {
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true)
|
||||
if (typeof window !== 'undefined') {
|
||||
sectionsRef.current = document.querySelectorAll('div[data-section]')
|
||||
if (typeof window !== "undefined") {
|
||||
sectionsRef.current = document.querySelectorAll("div[data-section]")
|
||||
|
||||
const handleScroll = () => {
|
||||
if (isManualScroll) return
|
||||
@@ -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) {
|
||||
@@ -169,7 +169,7 @@ export const ProjectList = ({ lang }: LangProps['params']) => {
|
||||
data-section={status}
|
||||
className="flex justify-between gap-10"
|
||||
>
|
||||
<div className={cn('flex w-full flex-col gap-10 pt-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>
|
||||
@@ -199,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) => {
|
||||
@@ -217,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,
|
||||
}
|
||||
)}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
'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)
|
||||
|
||||
@@ -25,20 +25,20 @@ export const ProjectResultBar = ({ lang }: LangProps['params']) => {
|
||||
)
|
||||
|
||||
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}`),
|
||||
})
|
||||
|
||||
|
||||
183
components/research/research-card.tsx
Normal file
183
components/research/research-card.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
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 {
|
||||
ProjectInterface,
|
||||
ProjectLinkWebsite,
|
||||
ProjectStatus,
|
||||
ProjectStatusLabelMapping,
|
||||
} from "@/lib/types"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { LocaleTypes } from "@/app/i18n/settings"
|
||||
|
||||
import { ProjectLink } from "../mappings/project-link"
|
||||
|
||||
interface ProjectCardProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof projectCardVariants> {
|
||||
project: ProjectInterface
|
||||
showLinks?: boolean // show links in the card
|
||||
showBanner?: boolean // show images in the card
|
||||
showCardTags?: boolean // show card tags in the card
|
||||
showStatus?: boolean // show status in the card
|
||||
contentClassName?: string // show status in the card
|
||||
}
|
||||
|
||||
const tagCardVariants = cva(
|
||||
"text-xs font-sans text-tuatara-950 rounded-[3px] py-[2px] px-[6px]",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: "bg-[#D8FEA8]",
|
||||
secondary: "bg-[#C2E8F5]",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
const projectCardVariants = cva(
|
||||
"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]",
|
||||
},
|
||||
border: {
|
||||
true: "border border-slate-900/20",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export const ProjectStatusColorMapping: Record<ProjectStatus, string> = {
|
||||
active: "#D8FEA8",
|
||||
inactive: "#FFB7AA",
|
||||
maintained: "#FFEC9E",
|
||||
}
|
||||
|
||||
export default function ProjectCard({
|
||||
project,
|
||||
showLinks = false,
|
||||
showBanner = false,
|
||||
border = false,
|
||||
showCardTags = true,
|
||||
showStatus = true,
|
||||
className,
|
||||
contentClassName,
|
||||
lang,
|
||||
}: ProjectCardProps & { lang: LocaleTypes }) {
|
||||
const router = useRouter()
|
||||
|
||||
const { id, image, links, name, imageAlt, projectStatus, cardTags } =
|
||||
project ?? {}
|
||||
|
||||
const { content: projectContent } = getProjectById(id, lang)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group cursor-pointer",
|
||||
projectCardVariants({ showLinks, border, className })
|
||||
)}
|
||||
onClick={() => {
|
||||
router.push(`/projects/${id}`)
|
||||
}}
|
||||
>
|
||||
{showBanner && (
|
||||
<div
|
||||
className="relative flex flex-col border-b border-black/10 cursor-pointer"
|
||||
onClick={() => {
|
||||
router.push(`/projects/${id}`)
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={`/project-banners/${image ? image : "fallback.webp"}`}
|
||||
alt={`${name} banner`}
|
||||
width={1200}
|
||||
height={630}
|
||||
className="h-[160px] w-full overflow-hidden rounded-t-lg border-none object-cover"
|
||||
/>
|
||||
{!image && (
|
||||
<span className="absolute w-full px-5 text-xl font-bold text-center text-black -translate-x-1/2 -translate-y-1/2 left-1/2 top-1/2">
|
||||
{imageAlt || name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col justify-between h-full gap-8 p-[30px] bg-white rounded-b-lg hover:bg-research-card-gradient duration-300",
|
||||
contentClassName,
|
||||
{
|
||||
"bg-white": !showBanner,
|
||||
"bg-transparent": showBanner,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col justify-start gap-2">
|
||||
<h1 className="text-2xl font-bold leading-7 duration-200 cursor-pointer text-anakiwa-700 line-clamp-2">
|
||||
{name}
|
||||
</h1>
|
||||
{projectContent?.tldr && (
|
||||
<div className="flex flex-col h-24 gap-4">
|
||||
<p className="text-slate-900/80 line-clamp-3">
|
||||
{projectContent?.tldr}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between ">
|
||||
{showLinks && (
|
||||
<div className="flex items-center justify-start gap-3">
|
||||
{Object.entries(links ?? {})?.map(([website, url], index) => {
|
||||
return (
|
||||
<ProjectLink
|
||||
key={index}
|
||||
url={url}
|
||||
website={website as ProjectLinkWebsite}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showStatus && (
|
||||
<div
|
||||
className="px-[6px] py-[2px] text-xs font-normal leading-none flex items-center justify-center rounded-[3px]"
|
||||
style={{
|
||||
backgroundColor: ProjectStatusColorMapping[projectStatus],
|
||||
}}
|
||||
>
|
||||
{ProjectStatusLabelMapping[project?.projectStatus]}
|
||||
</div>
|
||||
)}
|
||||
{showCardTags && (
|
||||
<>
|
||||
{cardTags && (
|
||||
<div className="flex items-center gap-1">
|
||||
{cardTags?.primary && (
|
||||
<div className={tagCardVariants({ variant: "primary" })}>
|
||||
{cardTags?.primary}
|
||||
</div>
|
||||
)}
|
||||
{cardTags?.secondary && (
|
||||
<div
|
||||
className={tagCardVariants({ variant: "secondary" })}
|
||||
>
|
||||
{cardTags?.secondary}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
140
components/research/research-list.tsx
Normal file
140
components/research/research-list.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
"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 { LangProps } from "@/types/common"
|
||||
import { ProjectInterface, ProjectStatus } from "@/lib/types"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
|
||||
import ResearchCard from "./research-card"
|
||||
import Link from "next/link"
|
||||
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")
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 pt-24 pb-40 text-center">
|
||||
<div className="mx-auto">
|
||||
<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")}
|
||||
</span>
|
||||
<span className="text-lg font-normal text-tuatara-950">
|
||||
{t("noResultsDescription")}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ProjectStatusOrderList = ["active", "maintained", "inactive"]
|
||||
|
||||
export const ResearchList = ({ lang }: LangProps["params"]) => {
|
||||
const { t } = useTranslation(lang, "research-page")
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
|
||||
const { researchs, searchQuery, queryString } = useProjectFiltersState(
|
||||
(state) => state
|
||||
)
|
||||
|
||||
const noItems = researchs?.length === 0
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true)
|
||||
}, [])
|
||||
|
||||
const hasActiveFilters = searchQuery !== "" || queryString !== ""
|
||||
|
||||
if (!isMounted) {
|
||||
return (
|
||||
<div className="flex flex-col gap-10 lg:px-[100px]">
|
||||
<div className="flex flex-col gap-6 overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"after:left-[100px] lg:after:left-[200px]",
|
||||
sectionTitleClass()
|
||||
)}
|
||||
>
|
||||
<div className="h-3 lg:h-4 w-[120px] lg:w-[220px] bg-gray-200 animate-pulse rounded-lg"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid items-start justify-between w-full grid-cols-1 gap-2 md:grid-cols-3 md:gap-6 ">
|
||||
<div className="min-h-[200px] border border-gray-200 bg-gray-200 animate-pulse rounded-lg overflow-hidden"></div>
|
||||
<div className="min-h-[200px] border border-gray-200 bg-gray-200 animate-pulse rounded-lg overflow-hidden"></div>
|
||||
<div className="min-h-[200px] border border-gray-200 bg-gray-200 animate-pulse rounded-lg overflow-hidden"></div>
|
||||
<div className="min-h-[200px] border border-gray-200 bg-gray-200 animate-pulse rounded-lg overflow-hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (noItems) return <NoResults lang={lang} />
|
||||
|
||||
const activeResearchs = researchs.filter(
|
||||
(research) => research.projectStatus === ProjectStatus.ACTIVE
|
||||
)
|
||||
|
||||
const pastResearchs = researchs.filter(
|
||||
(research) => research.projectStatus !== ProjectStatus.ACTIVE
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="relative grid items-start justify-between grid-cols-1">
|
||||
<div
|
||||
data-section="active-researchs"
|
||||
className="flex flex-col justify-between gap-10 lg:px-[100px]"
|
||||
>
|
||||
<div className={cn("flex w-full flex-col gap-10")}>
|
||||
{!hasActiveFilters && (
|
||||
<div className="flex flex-col gap-6 overflow-hidden">
|
||||
<h3 className={cn(sectionTitleClass())}>{t("activeResearch")}</h3>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 gap-4 md:gap-x-6 md:gap-y-10 lg:grid-cols-3">
|
||||
{activeResearchs.map((project) => {
|
||||
return (
|
||||
<ResearchCard
|
||||
key={project?.id}
|
||||
project={project}
|
||||
lang={lang}
|
||||
className="h-[180px]"
|
||||
showBanner={false}
|
||||
showLinks={false}
|
||||
showCardTags={false}
|
||||
showStatus={false}
|
||||
border
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn("flex w-full flex-col gap-10 pt-10")}>
|
||||
<div className="flex flex-col gap-6 overflow-hidden">
|
||||
<h3 className={cn(sectionTitleClass())}>{t("pastResearch")}</h3>
|
||||
</div>
|
||||
<div className="flex flex-col gap-5">
|
||||
{pastResearchs.map((project) => {
|
||||
return (
|
||||
<Link
|
||||
href={`/projects/${project?.id}`}
|
||||
key={project?.id}
|
||||
className="text-neutral-950 border-b-[2px] border-b-anakiwa-500 text-sm font-medium w-fit hover:text-anakiwa-500 duration-200"
|
||||
>
|
||||
{project.name}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react'
|
||||
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'
|
||||
import { Parser as HtmlToReactParser } from 'html-to-react'
|
||||
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 ?? [])
|
||||
@@ -53,7 +53,7 @@ export const NewsSection = ({ lang }: NewsSectionProps) => {
|
||||
const htmlToReactParser = new HtmlToReactParser()
|
||||
const announcementContent = htmlToReactParser.parse(
|
||||
convertDirtyStringToHtml(
|
||||
news?.content || news?.message_snapshots?.[0]?.message?.content || ''
|
||||
news?.content || news?.message_snapshots?.[0]?.message?.content || ""
|
||||
)
|
||||
)
|
||||
|
||||
@@ -65,7 +65,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] ">
|
||||
@@ -74,9 +74,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>
|
||||
)
|
||||
@@ -93,8 +93,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>
|
||||
@@ -111,7 +111,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>
|
||||
@@ -120,4 +120,4 @@ export const NewsSection = ({ lang }: NewsSectionProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
NewsSection.displayName = 'NewsSection'
|
||||
NewsSection.displayName = "NewsSection"
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
'use client'
|
||||
"use client"
|
||||
|
||||
import { LangProps } from '@/types/common'
|
||||
import { useTranslation } from '@/app/i18n/client'
|
||||
import { LangProps } from "@/types/common"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
|
||||
import { Icons } from '../icons'
|
||||
import { AppContent } from '../ui/app-content'
|
||||
import { Icons } from "../icons"
|
||||
import { AppContent } from "../ui/app-content"
|
||||
|
||||
type WhatWeDoContent = { title: string; description: string; icon: any }
|
||||
|
||||
export const WhatWeDo = ({ lang }: LangProps['params']) => {
|
||||
const { t } = useTranslation(lang, 'what-we-do-section')
|
||||
export const WhatWeDo = ({ lang }: LangProps["params"]) => {
|
||||
const { t } = useTranslation(lang, "what-we-do-section")
|
||||
|
||||
const content: WhatWeDoContent[] = [
|
||||
{
|
||||
title: t('privacy.title'),
|
||||
description: t('privacy.description'),
|
||||
title: t("privacy.title"),
|
||||
description: t("privacy.description"),
|
||||
icon: Icons.privacy,
|
||||
},
|
||||
{
|
||||
title: t('scaling.title'),
|
||||
description: t('scaling.description'),
|
||||
title: t("scaling.title"),
|
||||
description: t("scaling.description"),
|
||||
icon: Icons.scaling,
|
||||
},
|
||||
{
|
||||
title: t('explorations.title'),
|
||||
description: t('explorations.description'),
|
||||
title: t("explorations.title"),
|
||||
description: t("explorations.description"),
|
||||
icon: Icons.explorations,
|
||||
},
|
||||
]
|
||||
@@ -35,10 +35,10 @@ export const WhatWeDo = ({ lang }: LangProps['params']) => {
|
||||
<section className="flex flex-col gap-16 py-16 md:pb-24">
|
||||
<div className="flex flex-col text-center">
|
||||
<h6 className="py-6 font-sans text-base font-bold uppercase tracking-[4px] text-tuatara-950">
|
||||
{t('whatWeDo')}
|
||||
{t("whatWeDo")}
|
||||
</h6>
|
||||
<h3 className="font-display text-[18px] font-bold text-tuatara-950 md:text-3xl">
|
||||
{t('whatWeDoDescription')}
|
||||
{t("whatWeDoDescription")}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
'use client'
|
||||
"use client"
|
||||
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
|
||||
import { LangProps } from '@/types/common'
|
||||
import { NavItem } from '@/types/nav'
|
||||
import { siteConfig } from '@/config/site'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAppSettings } from '@/hooks/useAppSettings'
|
||||
import { useTranslation } from '@/app/i18n/client'
|
||||
import { LangProps } from "@/types/common"
|
||||
import { NavItem } from "@/types/nav"
|
||||
import { siteConfig } from "@/config/site"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useAppSettings } from "@/hooks/useAppSettings"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
|
||||
import { Icons } from './icons'
|
||||
import { AppContent } from './ui/app-content'
|
||||
import { Icons } from "./icons"
|
||||
import { AppContent } from "./ui/app-content"
|
||||
|
||||
const ItemLabel = ({
|
||||
label,
|
||||
@@ -42,11 +42,11 @@ const LinksWrapper = ({
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) => {
|
||||
return <div className={cn('flex flex-col gap-4', className)}>{children}</div>
|
||||
return <div className={cn("flex flex-col gap-4", className)}>{children}</div>
|
||||
}
|
||||
|
||||
export function SiteFooter({ lang }: LangProps['params']) {
|
||||
const { t } = useTranslation(lang, 'common')
|
||||
export function SiteFooter({ lang }: LangProps["params"]) {
|
||||
const { t } = useTranslation(lang, "common")
|
||||
|
||||
const { MAIN_NAV } = useAppSettings(lang)
|
||||
|
||||
@@ -63,7 +63,7 @@ export function SiteFooter({ lang }: LangProps['params']) {
|
||||
className="h-36 w-36"
|
||||
/>
|
||||
<span className="text-center font-sans text-sm leading-[21px] md:text-left">
|
||||
{t('footer.description')}
|
||||
{t("footer.description")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="order-2 grid grid-cols-1 justify-between gap-10 uppercase lg:grid-cols-5 lg:gap-0">
|
||||
@@ -77,7 +77,7 @@ export function SiteFooter({ lang }: LangProps['params']) {
|
||||
<Link
|
||||
key={indexKey}
|
||||
href={external ? href : `/${lang}${href}`}
|
||||
target={external ? '_blank' : undefined}
|
||||
target={external ? "_blank" : undefined}
|
||||
>
|
||||
<ItemLabel label={title} />
|
||||
</Link>
|
||||
@@ -99,7 +99,7 @@ export function SiteFooter({ lang }: LangProps['params']) {
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ItemLabel label={t('menu.jobs')} external />
|
||||
<ItemLabel label={t("menu.jobs")} external />
|
||||
</Link>
|
||||
<Link
|
||||
href={siteConfig.links.report}
|
||||
@@ -107,7 +107,7 @@ export function SiteFooter({ lang }: LangProps['params']) {
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ItemLabel label={t('menu.report')} external />
|
||||
<ItemLabel label={t("menu.report")} external />
|
||||
</Link>
|
||||
<Link
|
||||
href={siteConfig.links.firstGoodIssue}
|
||||
@@ -115,7 +115,7 @@ export function SiteFooter({ lang }: LangProps['params']) {
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ItemLabel label={t('menu.firstGoodIssue')} external />
|
||||
<ItemLabel label={t("menu.firstGoodIssue")} external />
|
||||
</Link>
|
||||
</LinksWrapper>
|
||||
<LinksWrapper>
|
||||
@@ -186,14 +186,14 @@ export function SiteFooter({ lang }: LangProps['params']) {
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ItemLabel label={t('footer.privacyPolicy')} />
|
||||
<ItemLabel label={t("footer.privacyPolicy")} />
|
||||
</Link>
|
||||
<Link
|
||||
href={siteConfig.links.termOfUse}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ItemLabel label={t('footer.termsOfUse')} />
|
||||
<ItemLabel label={t("footer.termsOfUse")} />
|
||||
</Link>
|
||||
</LinksWrapper>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React from "react"
|
||||
|
||||
interface MySvgProps {
|
||||
color: string
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface LabelProps {
|
||||
label: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SectionTitle = ({ label, className = '' }: LabelProps) => {
|
||||
const SectionTitle = ({ label, className = "" }: LabelProps) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'font-sans text-base font-bold uppercase leading-[24px] tracking-[3.36px] text-tuatara-950',
|
||||
"font-sans text-base font-bold uppercase leading-[24px] tracking-[3.36px] text-tuatara-950",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -18,11 +18,11 @@ const SectionTitle = ({ label, className = '' }: LabelProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
const MainPageTitle = ({ label, className = '' }: LabelProps) => {
|
||||
const MainPageTitle = ({ label, className = "" }: LabelProps) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'text-4xl font-bold break-words font-display text-tuatara-950 lg:text-6xl xl:text-7xl',
|
||||
"text-4xl font-bold break-words font-display text-tuatara-950 lg:text-6xl xl:text-7xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -32,7 +32,7 @@ const MainPageTitle = ({ label, className = '' }: LabelProps) => {
|
||||
}
|
||||
|
||||
const Label = {
|
||||
displayName: 'Label',
|
||||
displayName: "Label",
|
||||
PageTitle: MainPageTitle,
|
||||
Section: SectionTitle,
|
||||
}
|
||||
|
||||
@@ -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,19 +110,19 @@ 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"),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -132,18 +132,18 @@ export const WikiSideNavigation = ({
|
||||
|
||||
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,
|
||||
}
|
||||
)}
|
||||
@@ -166,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,
|
||||
}
|
||||
)}
|
||||
@@ -179,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",
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user