mirror of
https://github.com/privacy-scaling-explorations/pse.dev.git
synced 2026-01-09 14:18:02 -05:00
add more translation
This commit is contained in:
@@ -137,7 +137,7 @@ export default async function ProjectDetailPage({ params }: PageProps) {
|
||||
<div className="flex w-full flex-col gap-5 text-base font-normal leading-relaxed">
|
||||
<Markdown>{currProject.description}</Markdown>
|
||||
</div>
|
||||
<ProjectExtraLinks project={currProject} />
|
||||
<ProjectExtraLinks project={currProject} lang={lang} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ import { LocaleTypes, cookieName, getOptions, languages } from "./settings"
|
||||
const runsOnServerSide = typeof window === "undefined"
|
||||
|
||||
//
|
||||
i18next
|
||||
export const i18n = i18next
|
||||
.use(initReactI18next)
|
||||
.use(LanguageDetector)
|
||||
.use(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createInstance } from "i18next"
|
||||
import { createInstance, init } from "i18next"
|
||||
import resourcesToBackend from "i18next-resources-to-backend"
|
||||
import { initReactI18next } from "react-i18next/initReactI18next"
|
||||
|
||||
|
||||
@@ -30,6 +30,21 @@
|
||||
"description": "The page you are looking for might have been removed, had its name changed or is temporarily unavailable."
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"build": "Build",
|
||||
"play": "Play",
|
||||
"research": "Research"
|
||||
},
|
||||
"status": {
|
||||
"archived": "Archived",
|
||||
"active": "Active"
|
||||
},
|
||||
"sortBy": "Sort by: {{option}}",
|
||||
"tryItOut": "Try it out!",
|
||||
"learnMore": "Learn more",
|
||||
"buildWithThisTool": "Build with this tool",
|
||||
"deepDiveResearch": "Dive deeper into the research",
|
||||
"searchProjectPlaceholder": "Search project title or keyword",
|
||||
"close": "Close",
|
||||
"lastUpdatedAt": "Last updated {{date}}",
|
||||
"projectLibrary": "Project Library",
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function ProjectCard({
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => router.push(`${lang}/projects/${id}`)}
|
||||
onClick={() => router.push(`/projects/${id}`)}
|
||||
className={cn(projectCardVariants({ showLinks, border, className }))}
|
||||
>
|
||||
{showBanner && (
|
||||
@@ -67,14 +67,14 @@ export default function ProjectCard({
|
||||
alt={`${name} banner`}
|
||||
width={1200}
|
||||
height={630}
|
||||
className="w-full rounded-t-lg object-cover"
|
||||
className="w-full rounded-t-lg object-cover min-h-[160px]"
|
||||
/>
|
||||
)}
|
||||
<div className="flex h-full flex-col justify-between gap-5 rounded-b-lg bg-white p-5">
|
||||
<div className="flex flex-col justify-start gap-2">
|
||||
<div className="mb-2 flex gap-2">
|
||||
{tags?.themes?.map((theme, index) => {
|
||||
const { label } = ThemesButtonMapping?.[theme]
|
||||
const { label } = ThemesButtonMapping(lang)?.[theme]
|
||||
const icon = TagsIconMapping?.[theme]
|
||||
|
||||
return (
|
||||
|
||||
@@ -33,7 +33,8 @@ type IProjectTags = {
|
||||
}
|
||||
|
||||
export function ProjectTags({ project, lang }: IProjectTags) {
|
||||
const { label, icon } = ThemesStatusMapping?.[project?.projectStatus] ?? {}
|
||||
const statusItem = ThemesStatusMapping(lang)
|
||||
const { label, icon } = statusItem?.[project?.projectStatus] ?? {}
|
||||
const { t } = useTranslation(lang, "common")
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import Link from "next/link"
|
||||
|
||||
@@ -6,35 +8,14 @@ import {
|
||||
ProjectExtraLinkType,
|
||||
ProjectInterface,
|
||||
} from "@/lib/types"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
import { LocaleTypes } from "@/app/i18n/settings"
|
||||
|
||||
import { Icons } from "../icons"
|
||||
|
||||
const ExtraLinkLabelMapping: Record<
|
||||
ProjectExtraLinkType,
|
||||
{
|
||||
label: string
|
||||
icon?: any
|
||||
}
|
||||
> = {
|
||||
buildWith: {
|
||||
label: "Build with this tool",
|
||||
icon: <Icons.hammer />,
|
||||
},
|
||||
play: {
|
||||
label: "Try it out!",
|
||||
icon: <Icons.hand />,
|
||||
},
|
||||
research: {
|
||||
label: "Dive deeper into the research",
|
||||
icon: <Icons.readme />,
|
||||
},
|
||||
learn: {
|
||||
label: "Learn more",
|
||||
},
|
||||
}
|
||||
|
||||
interface ProjectExtraLinksProps {
|
||||
project: ProjectInterface
|
||||
lang: LocaleTypes
|
||||
}
|
||||
|
||||
interface ExtraLinkItemsProps {
|
||||
@@ -42,43 +23,71 @@ interface ExtraLinkItemsProps {
|
||||
links: ActionLinkTypeLink[]
|
||||
}
|
||||
|
||||
const ExtraLinkItems = ({ id, links = [] }: ExtraLinkItemsProps) => {
|
||||
const { label, icon } = ExtraLinkLabelMapping[id]
|
||||
|
||||
if (!links.length) return null // no links hide the section
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{icon && <span className="text-anakiwa-500">{icon}</span>}
|
||||
<p className="font-sans text-xl font-medium text-tuatara-700">
|
||||
{label}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
{links.map(({ label, url }: ActionLinkTypeLink) => {
|
||||
return (
|
||||
<Link
|
||||
href={url}
|
||||
target="_blank"
|
||||
className="flex cursor-pointer items-center gap-1 overflow-hidden border-b-2 border-transparent font-sans font-normal text-tuatara-950 duration-200 ease-in-out hover:border-orange"
|
||||
>
|
||||
{label}
|
||||
<Icons.externalUrl />
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ProjectExtraLinks({ project }: ProjectExtraLinksProps) {
|
||||
export default function ProjectExtraLinks({
|
||||
project,
|
||||
lang,
|
||||
}: ProjectExtraLinksProps) {
|
||||
const { t } = useTranslation(lang, "common")
|
||||
const { extraLinks = {} } = project
|
||||
const hasExtraLinks = Object.keys(extraLinks).length > 0
|
||||
|
||||
const ExtraLinkLabelMapping: Record<
|
||||
ProjectExtraLinkType,
|
||||
{
|
||||
label: string
|
||||
icon?: any
|
||||
}
|
||||
> = {
|
||||
buildWith: {
|
||||
label: t("buildWithThisTool"),
|
||||
icon: <Icons.hammer />,
|
||||
},
|
||||
play: {
|
||||
label: t("tryItOut"),
|
||||
icon: <Icons.hand />,
|
||||
},
|
||||
research: {
|
||||
label: t("deepDiveResearch"),
|
||||
icon: <Icons.readme />,
|
||||
},
|
||||
learn: {
|
||||
label: t("learnMore"),
|
||||
},
|
||||
}
|
||||
|
||||
if (!hasExtraLinks) return null
|
||||
|
||||
const ExtraLinkItems = ({ id, links = [] }: ExtraLinkItemsProps) => {
|
||||
const { label, icon } = ExtraLinkLabelMapping[id]
|
||||
|
||||
if (!links.length) return null // no links hide the section
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{icon && <span className="text-anakiwa-500">{icon}</span>}
|
||||
<p className="font-sans text-xl font-medium text-tuatara-700">
|
||||
{label}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
{links.map(({ label, url }: ActionLinkTypeLink) => {
|
||||
return (
|
||||
<Link
|
||||
href={url}
|
||||
target="_blank"
|
||||
className="flex cursor-pointer items-center gap-1 overflow-hidden border-b-2 border-transparent font-sans font-normal text-tuatara-950 duration-200 ease-in-out hover:border-orange"
|
||||
>
|
||||
{label}
|
||||
<Icons.externalUrl />
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 py-4">
|
||||
{Object.entries(ExtraLinkLabelMapping).map(([key]) => {
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
ProjectFilter,
|
||||
useProjectFiltersState,
|
||||
} from "@/state/useProjectFiltersState"
|
||||
import i18next from "i18next"
|
||||
import { useDebounce } from "react-use"
|
||||
|
||||
import { LangProps } from "@/types/common"
|
||||
import { ProjectStatusType } from "@/lib/types"
|
||||
import { IThemeStatus, IThemesButton, LangProps } from "@/types/common"
|
||||
import { cn, queryStringToObject } from "@/lib/utils"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
import { LocaleTypes } from "@/app/i18n/settings"
|
||||
@@ -41,49 +41,45 @@ const FilterWrapper = ({ label, children }: FilterWrapperProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const ThemesButtonMapping: Record<
|
||||
string,
|
||||
{
|
||||
label: string
|
||||
icon: any
|
||||
export const ThemesButtonMapping = (lang: LocaleTypes): IThemesButton => {
|
||||
const t = i18next.getFixedT(lang, "common")
|
||||
|
||||
return {
|
||||
build: {
|
||||
label: t("tags.build"),
|
||||
icon: <Icons.hammer />,
|
||||
},
|
||||
play: {
|
||||
label: t("tags.play"),
|
||||
icon: <Icons.hand />,
|
||||
},
|
||||
research: {
|
||||
label: t("tags.research"),
|
||||
icon: <Icons.readme />,
|
||||
},
|
||||
}
|
||||
> = {
|
||||
build: {
|
||||
label: "Build",
|
||||
icon: <Icons.hammer />,
|
||||
},
|
||||
play: {
|
||||
label: "Play",
|
||||
icon: <Icons.hand />,
|
||||
},
|
||||
research: {
|
||||
label: "Research",
|
||||
icon: <Icons.readme />,
|
||||
},
|
||||
}
|
||||
|
||||
export const ThemesStatusMapping: Partial<
|
||||
Record<
|
||||
ProjectStatusType,
|
||||
{
|
||||
label: string
|
||||
icon: any
|
||||
}
|
||||
>
|
||||
> = {
|
||||
active: {
|
||||
label: "Active",
|
||||
icon: <Icons.checkActive />,
|
||||
},
|
||||
archived: {
|
||||
label: "Archived",
|
||||
icon: <Icons.archived />,
|
||||
},
|
||||
export const ThemesStatusMapping = (lang: LocaleTypes): IThemeStatus => {
|
||||
const t = i18next.getFixedT(lang, "common")
|
||||
|
||||
return {
|
||||
active: {
|
||||
label: t("status.active"),
|
||||
icon: <Icons.checkActive />,
|
||||
},
|
||||
archived: {
|
||||
label: t("status.archived"),
|
||||
icon: <Icons.archived />,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const FilterButtons = ({
|
||||
searchQuery,
|
||||
lang,
|
||||
}: {
|
||||
lang: LocaleTypes
|
||||
searchQuery?: string
|
||||
}): JSX.Element => {
|
||||
const { activeFilters, onSelectTheme } = useProjectFiltersState(
|
||||
@@ -92,25 +88,27 @@ const FilterButtons = ({
|
||||
|
||||
return (
|
||||
<div className="relative col-span-1 grid grid-cols-3 gap-2 after:absolute after:right-[-25px] after:h-11 after:w-[1px] after:content-none md:col-span-2 md:gap-4 md:after:content-['']">
|
||||
{Object.entries(ThemesButtonMapping).map(([key, { label, icon }]) => {
|
||||
const isActive = activeFilters?.themes?.includes(key)
|
||||
const variant = isActive ? "blue" : "white"
|
||||
return (
|
||||
<Button
|
||||
key={key}
|
||||
variant={variant}
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
onSelectTheme(key, searchQuery ?? "")
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
{Object.entries(ThemesButtonMapping(lang)).map(
|
||||
([key, { label, icon }]) => {
|
||||
const isActive = activeFilters?.themes?.includes(key)
|
||||
const variant = isActive ? "blue" : "white"
|
||||
return (
|
||||
<Button
|
||||
key={key}
|
||||
variant={variant}
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
onSelectTheme(key, searchQuery ?? "")
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -128,7 +126,7 @@ export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!queryString) return
|
||||
router.push(`${lang}/projects?${queryString}`)
|
||||
router.push(`/projects?${queryString}`)
|
||||
}, [queryString, router, lang])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -152,7 +150,7 @@ export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
|
||||
projects,
|
||||
})
|
||||
setSearchQuery("") // clear input
|
||||
router.push(`${lang}/projects`)
|
||||
router.push(`/projects`)
|
||||
}
|
||||
|
||||
useDebounce(
|
||||
@@ -235,7 +233,7 @@ export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
|
||||
}
|
||||
|
||||
if (type === "button") {
|
||||
const { icon, label } = ThemesButtonMapping[item]
|
||||
const { icon, label } = ThemesButtonMapping(lang)[item]
|
||||
if (!isActive) return null
|
||||
return (
|
||||
<div>
|
||||
@@ -272,14 +270,14 @@ export default function ProjectFiltersBar({ lang }: LangProps["params"]) {
|
||||
<div className="flex flex-col gap-6">
|
||||
<span className="text-lg font-medium">{t("whatDoYouWantDoToday")}</span>
|
||||
<div className="grid grid-cols-1 items-center justify-between gap-3 md:grid-cols-5 md:gap-12">
|
||||
<FilterButtons />
|
||||
<FilterButtons lang={lang} />
|
||||
<div className="col-span-1 grid grid-cols-[1fr_auto] gap-2 md:col-span-3 md:gap-3">
|
||||
<Input
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setSearchQuery(e?.target?.value)
|
||||
}
|
||||
value={searchQuery}
|
||||
placeholder="Search project title or keyword"
|
||||
placeholder={t("searchProjectPlaceholder")}
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge value={filterCount}>
|
||||
|
||||
@@ -14,17 +14,6 @@ import { Dropdown } from "../ui/dropdown"
|
||||
|
||||
const labelClass = "h-5 text-xs text-base md:h-6 text-slate-900/70 md:text-lg"
|
||||
|
||||
const projectSortItems: { label: string; value: ProjectSortBy }[] = [
|
||||
{ label: "Random", value: "random" },
|
||||
{ label: "Title: A-Z", value: "asc" },
|
||||
{ label: "Title: Z-A", value: "desc" },
|
||||
{ label: "Relevance", value: "relevance" },
|
||||
]
|
||||
|
||||
const getSortLabel = (sortBy: ProjectSortBy) => {
|
||||
return projectSortItems.find((item) => item.value === sortBy)?.label || sortBy
|
||||
}
|
||||
|
||||
export const ProjectResultBar = ({ lang }: LangProps["params"]) => {
|
||||
const { t } = useTranslation(lang, "common")
|
||||
const { activeFilters, toggleFilter, projects, sortProjectBy, sortBy } =
|
||||
@@ -41,12 +30,23 @@ export const ProjectResultBar = ({ lang }: LangProps["params"]) => {
|
||||
}
|
||||
)
|
||||
|
||||
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.relevance"), value: "relevance" },
|
||||
]
|
||||
|
||||
const activeSortOption = t("sortBy", {
|
||||
option: t(`filterOptions.${sortBy}`),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={labelClass}>{resultLabel}</span>
|
||||
<Dropdown
|
||||
label={`Sort by: ${getSortLabel(sortBy)}`}
|
||||
label={activeSortOption}
|
||||
defaultItem="random"
|
||||
items={projectSortItems}
|
||||
onChange={(sortBy) => sortProjectBy(sortBy as ProjectSortBy)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { LangProps } from "@/types/common"
|
||||
import { siteConfig } from "@/config/site"
|
||||
import { MainNav, MainNavProps } from "@/components/main-nav"
|
||||
import { useTranslation } from "@/app/i18n/client"
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ProjectStatusType } from "@/lib/types"
|
||||
import { LocaleTypes } from "@/app/i18n/settings"
|
||||
|
||||
export interface LangProps {
|
||||
@@ -5,3 +6,21 @@ export interface LangProps {
|
||||
lang: LocaleTypes
|
||||
}
|
||||
}
|
||||
|
||||
export type IThemeStatus = Partial<
|
||||
Record<
|
||||
ProjectStatusType,
|
||||
{
|
||||
label: string
|
||||
icon: any
|
||||
}
|
||||
>
|
||||
>
|
||||
|
||||
export type IThemesButton = Record<
|
||||
string,
|
||||
{
|
||||
label: string
|
||||
icon: any
|
||||
}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user