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:
18
packages/ui/src/Layout/Footer/Footer.stories.tsx
Normal file
18
packages/ui/src/Layout/Footer/Footer.stories.tsx
Normal 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",
|
||||
},
|
||||
}
|
34
packages/ui/src/Layout/Footer/Footer.tsx
Normal file
34
packages/ui/src/Layout/Footer/Footer.tsx
Normal 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>
|
||||
)
|
||||
}
|
16
packages/ui/src/Layout/Header/Header.stories.tsx
Normal file
16
packages/ui/src/Layout/Header/Header.stories.tsx
Normal 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: {},
|
||||
}
|
38
packages/ui/src/Layout/Header/Header.tsx
Normal file
38
packages/ui/src/Layout/Header/Header.tsx
Normal 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>
|
||||
)
|
||||
}
|
16
packages/ui/src/Layout/Header/Locales/Arrow.tsx
Normal file
16
packages/ui/src/Layout/Header/Locales/Arrow.tsx
Normal 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>
|
||||
)
|
||||
}
|
26
packages/ui/src/Layout/Header/Locales/LocaleFlag.tsx
Normal file
26
packages/ui/src/Layout/Header/Locales/LocaleFlag.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
86
packages/ui/src/Layout/Header/Locales/Locales.tsx
Normal file
86
packages/ui/src/Layout/Header/Locales/Locales.tsx
Normal 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>
|
||||
)
|
||||
}
|
107
packages/ui/src/Layout/Header/SwitchTheme.tsx
Normal file
107
packages/ui/src/Layout/Header/SwitchTheme.tsx
Normal 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>
|
||||
)
|
||||
}
|
24
packages/ui/src/Layout/MainLayout/MainLayout.tsx
Normal file
24
packages/ui/src/Layout/MainLayout/MainLayout.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
39
packages/ui/src/Layout/Section/RevealFade.tsx
Normal file
39
packages/ui/src/Layout/Section/RevealFade.tsx
Normal 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>
|
||||
)
|
||||
}
|
88
packages/ui/src/Layout/Section/Section.tsx
Normal file
88
packages/ui/src/Layout/Section/Section.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user