1
1
mirror of https://github.com/theoludwig/theoludwig.git synced 2025-05-29 22:37:44 +02:00

refactor: components struture

This commit is contained in:
2024-07-31 11:41:39 +02:00
parent ceeeb2f9c5
commit b5c50728de
72 changed files with 122 additions and 114 deletions

View File

@ -0,0 +1,18 @@
import type { Meta, StoryObj } from "@storybook/react"
import { Footer as FooterComponent } from "./Footer"
const meta = {
title: "Layout/Footer",
component: FooterComponent,
} satisfies Meta<typeof FooterComponent>
export default meta
type Story = StoryObj<typeof meta>
export const Footer: Story = {
args: {
version: "1.0.0",
},
}

View File

@ -0,0 +1,34 @@
import { useTranslations } from "next-intl"
import { GIT_REPO_LINK } from "@repo/utils/constants"
import { Link } from "../../Design/Link/Link"
export interface FooterProps {
version: string
}
export const Footer: React.FC<FooterProps> = (props) => {
const { version } = props
const t = useTranslations()
return (
<footer className="bg-background dark:bg-background-dark border-gray-darker dark:border-gray-darker-dark flex flex-col items-center justify-center border-t-2 p-6 text-lg">
<p>
<Link href="/">{t("meta.title")}</Link> |{" "}
{t("footer.all-rights-reserved")}
</p>
<p>
Version{" "}
<Link
href={`${GIT_REPO_LINK}/releases/tag/v${version}`}
target="_blank"
isExternal={false}
>
{version}
</Link>
</p>
</footer>
)
}

View File

@ -0,0 +1,16 @@
import type { Meta, StoryObj } from "@storybook/react"
import { Header as HeaderComponent } from "./Header"
const meta = {
title: "Layout/Header",
component: HeaderComponent,
} satisfies Meta<typeof HeaderComponent>
export default meta
type Story = StoryObj<typeof meta>
export const Header: Story = {
args: {},
}

View File

@ -0,0 +1,38 @@
import { useTranslations } from "next-intl"
import Image from "next/image"
import { Link } from "../../Design/Link/Link"
import { Locales } from "./Locales/Locales"
import { SwitchTheme } from "./SwitchTheme"
export interface HeaderProps {}
export const Header: React.FC<HeaderProps> = () => {
const t = useTranslations()
return (
<header className="bg-background dark:bg-background-dark border-gray-darker dark:border-gray-darker-dark sticky top-0 z-50 flex w-full justify-between gap-4 border-b-2 px-6 py-2">
<h1>
<Link href="/" className="flex items-center justify-center">
<Image
quality={100}
className="w-16"
src="/images/logo.webp"
width={800}
height={800}
alt={`${t("meta.title")} Logo`}
priority
/>
<strong className="ml-1 hidden sm:block sm:text-xl">
{t("meta.title")}
</strong>
</Link>
</h1>
<div className="flex items-center justify-between gap-6">
<Link href="/blog">Blog</Link>
<Locales />
<SwitchTheme />
</div>
</header>
)
}

View File

@ -0,0 +1,16 @@
export const Arrow: React.FC = () => {
return (
<svg
width="12"
height="8"
viewBox="0 0 12 8"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
className="fill-current text-black dark:text-white"
d="M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z"
/>
</svg>
)
}

View File

@ -0,0 +1,26 @@
import type { Locale } from "@repo/i18n/config"
import { useTranslations } from "next-intl"
import Image from "next/image"
export interface LocaleFlagProps {
locale: Locale
}
export const LocaleFlag: React.FC<LocaleFlagProps> = (props) => {
const { locale } = props
const t = useTranslations()
return (
<>
<Image
quality={100}
width={35}
height={35}
src={`/images/locales/${locale}.svg`}
alt={`Flag of ${t(`locales.${locale}`)}`}
/>
<p className="mx-2 text-base font-semibold">{t(`locales.${locale}`)}</p>
</>
)
}

View File

@ -0,0 +1,86 @@
"use client"
import { classNames } from "@repo/config-tailwind/classNames"
import type { Locale } from "@repo/i18n/config"
import { LOCALES } from "@repo/i18n/config"
import { usePathname, useRouter } from "@repo/i18n/navigation"
import { useLocale } from "next-intl"
import { useEffect, useRef } from "react"
import { useBoolean } from "@repo/react-hooks/useBoolean"
import { Arrow } from "./Arrow"
import { LocaleFlag } from "./LocaleFlag"
export interface LocalesProps {}
export const Locales: React.FC<LocalesProps> = () => {
const router = useRouter()
const pathname = usePathname()
const localeCurrent = useLocale() as Locale
const {
value: isVisibleMenu,
toggle: toggleMenu,
setFalse: hideMenu,
} = useBoolean()
const languageClickRef = useRef<HTMLButtonElement | null>(null)
useEffect(() => {
const handleClickEvent = (event: MouseEvent): void => {
if (languageClickRef.current == null || event.target == null) {
return
}
if (!languageClickRef.current.contains(event.target as Node)) {
hideMenu()
}
}
window.document.addEventListener("click", handleClickEvent)
return () => {
return window.removeEventListener("click", handleClickEvent)
}
}, [hideMenu])
if (pathname.startsWith("/blog")) {
return <></>
}
return (
<div className="flex flex-col items-center justify-center">
<button
ref={languageClickRef}
className="flex items-center"
onClick={toggleMenu}
>
<LocaleFlag locale={localeCurrent} />
<Arrow />
</button>
<ul
className={classNames(
"shadow-lightFlag dark:shadow-darkFlag bg-background dark:bg-background-dark absolute top-14 z-10 mt-4 flex w-32 list-none flex-col items-center justify-center rounded-lg p-0",
{ hidden: !isVisibleMenu },
)}
>
{LOCALES.filter((locale) => {
return locale !== localeCurrent
}).map((locale) => {
return (
<li key={locale} className="w-full">
<button
className="flex h-12 w-full items-center justify-center rounded-lg hover:bg-[#4f545c]/20"
onClick={() => {
router.replace(pathname, { locale, scroll: false })
router.refresh()
}}
>
<LocaleFlag locale={locale} />
</button>
</li>
)
})}
</ul>
</div>
)
}

View File

@ -0,0 +1,107 @@
"use client"
import { classNames } from "@repo/config-tailwind/classNames"
import { useIsMounted } from "@repo/react-hooks/useIsMounted"
import {
ThemeProvider as NextThemeProvider,
useTheme as useNextTheme,
} from "next-themes"
export const THEMES = ["light", "dark"] as const
export type Theme = (typeof THEMES)[number]
export const THEME_DEFAULT = "dark" as Theme
export interface ThemeProviderProps extends React.PropsWithChildren {}
export const ThemeProvider: React.FC<ThemeProviderProps> = (props) => {
const { children } = props
return (
<NextThemeProvider
attribute="class"
defaultTheme={THEME_DEFAULT}
enableSystem={false}
>
{children}
</NextThemeProvider>
)
}
export interface UseThemeOutput {
theme: Theme
toggleTheme: () => void
}
export const useTheme = (): UseThemeOutput => {
const { setTheme, theme: themeData } = useNextTheme()
const { isMounted } = useIsMounted()
const theme = isMounted ? (themeData as Theme) : THEME_DEFAULT
const toggleTheme: UseThemeOutput["toggleTheme"] = () => {
const newTheme = theme === "dark" ? "light" : "dark"
setTheme(newTheme)
}
return {
theme,
toggleTheme,
}
}
export interface SwitchThemeProps {}
export const SwitchTheme: React.FC<SwitchThemeProps> = () => {
const { theme, toggleTheme } = useTheme()
return (
<div className="flex items-center justify-center" onClick={toggleTheme}>
<div className="relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0">
<div className="h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out">
<div
className={classNames(
"absolute inset-y-0 left-[8px] my-auto h-[10px] w-[14px] leading-[0] transition-opacity duration-[250ms] ease-in-out",
{
"opacity-100": theme === "dark",
"opacity-0": theme === "light",
},
)}
>
<span className="relative flex size-[10px] items-center justify-center">
🌜
</span>
</div>
<div
className={classNames(
"absolute inset-y-0 right-[10px] my-auto size-[10px] leading-[0]",
{
"opacity-100": theme === "light",
"opacity-0": theme === "dark",
},
)}
>
<span className="relative flex size-[10px] items-center justify-center">
🌞
</span>
</div>
</div>
<div
className={classNames(
"absolute top-px box-border size-[22px] rounded-[50%] bg-[#fafafa] text-white transition-all duration-[250ms] ease-in-out",
{
"left-[27px]": theme === "dark",
"left-0": theme === "light",
},
)}
style={{ border: "1px solid #4d4d4d" }}
/>
<input
type="checkbox"
aria-label="Dark mode toggle"
className="absolute -m-px hidden size-px overflow-hidden border-0 p-0"
defaultChecked
/>
</div>
</div>
)
}

View File

@ -0,0 +1,24 @@
import { classNames } from "@repo/config-tailwind/classNames"
export interface MainLayoutProps
extends React.ComponentPropsWithoutRef<"main"> {
className?: string
center?: boolean
}
export const MainLayout: React.FC<MainLayoutProps> = (props) => {
const { className, center = false, ...rest } = props
return (
<main
className={classNames(
"min-h-[calc(100vh-188px)] md:mx-auto md:max-w-4xl lg:max-w-7xl",
{
"flex flex-col items-center justify-center text-center": center,
},
className,
)}
{...rest}
/>
)
}

View File

@ -0,0 +1,39 @@
"use client"
import { useEffect, useRef } from "react"
export interface RevealFadeProps extends React.PropsWithChildren {}
export const RevealFade: React.FC<RevealFadeProps> = (props) => {
const { children } = props
const htmlElement = useRef<HTMLDivElement | null>(null)
const className =
"opacity-100 visible translate-y-0 transition-all duration-700 ease-in-out"
useEffect(() => {
const observer = new window.IntersectionObserver(
(entries, observer) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.className = className
observer.unobserve(entry.target)
}
}
},
{
root: null,
rootMargin: "0px",
threshold: 0.28,
},
)
observer.observe(htmlElement.current as HTMLDivElement)
}, [])
return (
<div ref={htmlElement} className="invisible -translate-y-7 opacity-0">
{children}
</div>
)
}

View File

@ -0,0 +1,88 @@
import { classNames } from "@repo/config-tailwind/classNames"
import type { TypographyProps } from "../../Design/Typography/Typography"
import { Typography } from "../../Design/Typography/Typography"
export * from "./RevealFade"
export interface SectionProps
extends React.ComponentPropsWithoutRef<"section"> {
verticalSpacing?: boolean
horizontalSpacing?: boolean
}
export const Section: React.FC<SectionProps> = (props) => {
const {
className,
verticalSpacing = false,
horizontalSpacing = false,
...rest
} = props
return (
<section
className={classNames(
{
"my-12": verticalSpacing,
"mx-6": horizontalSpacing,
},
className,
)}
{...rest}
/>
)
}
export interface SectionTitleProps extends TypographyProps<"h2"> {}
export const SectionTitle: React.FC<SectionTitleProps> = (props) => {
const { className, ...rest } = props
return (
<Typography
as="h2"
variant="h2"
className={classNames("mb-4 text-center", className)}
{...rest}
/>
)
}
export interface SectionDescriptionProps extends TypographyProps<"p"> {}
export const SectionDescription: React.FC<SectionDescriptionProps> = (
props,
) => {
const { className, ...rest } = props
return (
<Typography
as="p"
variant="text1"
className={classNames("mb-4 text-center", className)}
{...rest}
/>
)
}
export interface SectionContentProps
extends React.ComponentPropsWithoutRef<"div"> {
shadowContainer?: boolean
}
export const SectionContent: React.FC<SectionContentProps> = (props) => {
const { className, shadowContainer = false, ...rest } = props
return (
<div
className={classNames(
"size-full max-w-full break-words px-6 py-4 sm:px-16",
{
"shadow-light dark:shadow-dark max-w-full rounded-2xl border border-solid border-black":
shadowContainer,
},
className,
)}
{...rest}
/>
)
}