diff --git a/public/placeholder.png b/public/placeholder.png new file mode 100644 index 00000000..e0a5ffe9 Binary files /dev/null and b/public/placeholder.png differ diff --git a/public/showcase/7770ecce-d977-40dd-bf91-b3f7114e4003.png b/public/showcase/7770ecce-d977-40dd-bf91-b3f7114e4003.png new file mode 100644 index 00000000..5fb08cf4 Binary files /dev/null and b/public/showcase/7770ecce-d977-40dd-bf91-b3f7114e4003.png differ diff --git a/showcase/showcaseConfig.json b/showcase/showcaseConfig.json new file mode 100644 index 00000000..8c087ff1 --- /dev/null +++ b/showcase/showcaseConfig.json @@ -0,0 +1,17 @@ +{ + "content": [ + { + "id": "7770ecce-d977-40dd-bf91-b3f7114e4003", + "author": "redyf", + "title": "ghostty mentioned!", + "description": "ghostty mentioned!", + "images": [ + { + "src": "/showcase/7770ecce-d977-40dd-bf91-b3f7114e4003.png", + "altText": "alt text" + } + ], + "code": "# Fonts\nfont-family = \"MonoLisa\"\nfont-size = 12\nfont-style = Bold \nfont-style-italic = Regular Italic\nfont-style-bold = Bold\nfont-style-bold-italic = Bold Italic\n\n# font-feature = -ss01, -ss02, -ss04 # Cartograph\nfont-feature = ss01\nfont-feature = ss02\nfont-feature = ss04\nfont-feature = ss07\nfont-feature = ss08\nfont-feature = ss10\nfont-feature = ss11\nfont-feature = ss13\nfont-feature = ss18\nfont-feature = zero\n# font-feature = ss01, ss02, ss04, ss07, ss08, ss10, ss11, ss13, ss18, zero # Monolisa\n# font-variation = \"wght=800\"\nfont-thicken = true\nbold-is-bright = true\n\n# adjust-cell-width =\n# adjust-cell-height =\n# adjust-font-baseline =\n# adjust-underline-position =\n# adjust-underline-thickness = 1\n# adjust-strikethrough-position = 1\n# adjust-strikethrough-thickness = 1\nadjust-cursor-thickness = 10\n\n# Cursor\n# cursor-color =\n# cursor-opacity =\n# cursor-text =\ncursor-style = block\ncursor-style-blink = false\ncursor-click-to-move = false\n\n# Mouse\nmouse-hide-while-typing = false\nmouse-scroll-multiplier = 1\n\n# Screen\nbackground-opacity = 1\nunfocused-split-opacity = 0.7\nfullscreen = false\n\n# Mappings\nkeybind = ctrl+comma=unbind\nkeybind = ctrl+alt+up=unbind\nkeybind = ctrl+page_down=unbind\nkeybind = ctrl+shift+v=paste_from_clipboard\nkeybind = shift+insert=paste_from_selection\nkeybind = ctrl+shift+a=unbind\nkeybind = shift+up=unbind\nkeybind = alt+five=unbind\nkeybind = super+ctrl+right_bracket=unbind\nkeybind = ctrl+shift+plus=increase_font_size:1\nkeybind = ctrl+shift+minus=decrease_font_size:1\nkeybind = ctrl+shift+o=unbind\nkeybind = ctrl+shift+c=copy_to_clipboard\nkeybind = ctrl+shift+q=unbind\nkeybind = ctrl+shift+n=unbind\nkeybind = ctrl+shift+page_down=unbind\nkeybind = ctrl+shift+comma=reload_config\nkeybind = shift+left=unbind\nkeybind = super+ctrl+shift+up=unbind\nkeybind = alt+eight=unbind\nkeybind = shift+page_up=unbind\nkeybind = ctrl+alt+shift+j=unbind\nkeybind = ctrl+shift+left=unbind\nkeybind = ctrl+shift+w=unbind\nkeybind = super+ctrl+shift+equal=unbind\nkeybind = shift+end=unbind\nkeybind = ctrl+shift+backspace=reset_font_size\nkeybind = alt+three=unbind\nkeybind = ctrl+shift+j=unbind\nkeybind = ctrl+enter=toggle_fullscreen\nkeybind = ctrl+page_up=unbind\nkeybind = shift+right=unbind\nkeybind = ctrl+alt+left=unbind\nkeybind = shift+page_down=unbind\nkeybind = ctrl+shift+right=unbind\nkeybind = ctrl+shift+page_up=unbind\nkeybind = alt+nine=unbind\nkeybind = ctrl+shift+t=unbind\nkeybind = shift+down=unbind\nkeybind = super+ctrl+shift+left=unbind\nkeybind = alt+two=unbind\nkeybind = ctrl+alt+down=unbind\nkeybind = super+ctrl+shift+down=unbind\nkeybind = super+ctrl+shift+right=unbind\nkeybind = ctrl+plus=unbind\nkeybind = alt+four=unbind\nkeybind = ctrl+shift+e=unbind\nkeybind = ctrl+alt+right=unbind\nkeybind = alt+f4=unbind\nkeybind = alt+one=unbind\nkeybind = ctrl+shift+enter=unbind\nkeybind = shift+home=unbind\nkeybind = super+ctrl+left_bracket=unbind\nkeybind = ctrl+shift+i=unbind\nkeybind = alt+six=unbind\nkeybind = alt+seven=unbind\n\n# Window\nwindow-padding-x = 5\nwindow-padding-y = 5\n# window-padding-balance = false\n# window-inherit-working-directory = true\n# window-inherit-font-size = true\n# window-decoration = true\n# window-theme = auto\n# window-save-state = default\n# window-new-tab-position = current\n\n# Clipboard (ask, allow or deny)\nclipboard-read = ask\nclipboard-trim-trailing-spaces = true\nclipboard-paste-protection = true\nclipboard-paste-bracketed-safe = true\ncopy-on-select = true\n\n# Close\nconfirm-close-surface = true\n\n# Shell\nshell-integration = zsh\nshell-integration-features = cursor,no-sudo,title\n\n# Advanced\n# linux-cgroup = single-instance\n\n# GTK\ngtk-single-instance = desktop\ngtk-titlebar = false\ngtk-tabs-location = top\ngtk-wide-tabs = true\n\ntheme = catppuccin-mocha\n\n# Scheme: Jabuti\n# Generated by Ghostty Base16 Converter\n# background = 292a37\n# foreground = c0cbe3\n#\n# selection-background = 3c3e51\n# selection-foreground = 292a37\n#\n# palette = 0=#292a37\n# palette = 1=#ec6a88\n# palette = 2=#3fdaa4\n# palette = 3=#e1c697\n# palette = 4=#3fc6de\n# palette = 5=#be95ff\n# palette = 6=#ff7eb6\n# palette = 7=#c0cbe3\n# palette = 8=#45475d\n# palette = 9=#ec6a88\n# palette = 10=#3fdaa4\n# palette = 11=#e1c697\n# palette = 12=#3fc6de\n# palette = 13=#be95ff\n# palette = 14=#ff7eb6\n# palette = 15=#ffffff\n# palette = 16=#efb993\n# palette = 17=#8b8da9\n# palette = 18=#343545\n# palette = 19=#3c3e51\n# palette = 20=#50526b\n# palette = \"21=#d9e0ee" + } + ] +} diff --git a/src/components/codeblock/index.tsx b/src/components/codeblock/index.tsx index 2b87a4ba..c9a929c4 100644 --- a/src/components/codeblock/index.tsx +++ b/src/components/codeblock/index.tsx @@ -6,11 +6,14 @@ import { CheckIcon, CopyIcon } from "lucide-react"; interface CodeblockProps { children?: React.ReactNode; + className?: string; } -export default function CodeBlock({ children }: CodeblockProps) { +export default function CodeBlock({ children, className }: CodeblockProps) { return ( -
+    
       {children}
       
     
diff --git a/src/lib/fetch-showcase-content.ts b/src/lib/fetch-showcase-content.ts new file mode 100644 index 00000000..d88ecb52 --- /dev/null +++ b/src/lib/fetch-showcase-content.ts @@ -0,0 +1,21 @@ +import { ShowcaseContent } from "@/types/showcase"; +import { promises as fs } from "fs"; + +export async function loadShowcaseContent(): Promise { + const docsFilePath = `showcase/showcaseConfig.json`; + try { + const data = await fs.readFile(docsFilePath, "utf8"); + const jsonData: { content: ShowcaseContent[] } = JSON.parse(data); + return jsonData.content; + } catch (err) { + if (err instanceof SyntaxError) { + throw new Error( + `Failed to parse showcase content: + +${err.message}}`, + { cause: err }, + ); + } + throw err; + } +} diff --git a/src/pages/showcase/ShowcasePage.module.css b/src/pages/showcase/ShowcasePage.module.css new file mode 100644 index 00000000..ad28fb83 --- /dev/null +++ b/src/pages/showcase/ShowcasePage.module.css @@ -0,0 +1,32 @@ +.showcasePage { + + & .header { + padding: 0 0 28px; + } + + & .list { + --gap: 24px; + --cols: 3; + + @media(max-width: 1100px) { + --cols: 2; + } + + @media(max-width: 768px) { + --cols: 1; + } + + margin: 0; + padding: 0; + list-style: none; + display: flex; + flex-wrap: wrap; + gap: var(--gap); + + &>li { + --flex-basis: calc((100% / var(--cols)) - (var(--gap) * (var(--cols) - 1)) / var(--cols)); + flex-basis: var(--flex-basis); + max-width: var(--flex-basis); + } + } +} diff --git a/src/pages/showcase/components/list-item/ListItem.module.css b/src/pages/showcase/components/list-item/ListItem.module.css new file mode 100644 index 00000000..280b3ec4 --- /dev/null +++ b/src/pages/showcase/components/list-item/ListItem.module.css @@ -0,0 +1,23 @@ +.ListItem { + border: 1px solid var(--gray-2); + border-radius: 10px; + overflow: hidden; + + & img { + width: 100%; + height: auto; + } + + & .content { + padding: 12px 12px 24px; + + & .title { + margin-bottom: 8px; + } + + & .description { + margin-bottom: 16px; + } + + } +} diff --git a/src/pages/showcase/components/list-item/list-item.tsx b/src/pages/showcase/components/list-item/list-item.tsx new file mode 100644 index 00000000..6f723bd4 --- /dev/null +++ b/src/pages/showcase/components/list-item/list-item.tsx @@ -0,0 +1,30 @@ +import s from "./ListItem.module.css"; +import Button from "@/components/button"; +import Image, { ImageProps } from "next/image"; +import { H3, P } from "@/components/text"; + +interface ListItemProps { + image: ImageProps; + title: string; + description: string; + onClick: () => void; +} +export default function ListItem({ + image, + title, + description, + onClick, +}: ListItemProps) { + return ( +
  • + +
    +

    {title}

    +

    {description}

    + +
    +
  • + ); +} diff --git a/src/pages/showcase/components/modal/Modal.module.css b/src/pages/showcase/components/modal/Modal.module.css new file mode 100644 index 00000000..b11a7a16 --- /dev/null +++ b/src/pages/showcase/components/modal/Modal.module.css @@ -0,0 +1,67 @@ +.modal { + top: 0; + left: 0; + z-index: 1; + width: 100%; + height: 100%; + display: flex; + position: fixed; + pointer-events: none; + background: color-mix(in srgb, var(--black) 60%, transparent); + + & .dialog { + pointer-events: all; + top: 20%; + overflow: hidden; + border: none; + border-radius: 10px; + width: 100%; + margin: 0 auto; + max-width: 700px; + + & .closeButton { + position: absolute; + cursor: pointer; + background: none; + border: none; + top: 12px; + right: 12px; + color: var(--black); + } + + & img { + z-index: 1; + width: 100%; + height: auto; + } + + & .content { + padding: 12px; + + & .buttonContainer { + display: flex; + padding: 0 0 12px; + gap: 24px; + + >button { + flex-basis: calc(50% - 12px) + } + } + + & .title { + margin: 0 0 4px; + } + + & .author { + color: var(--gray-5); + margin: 0 0 16px; + } + } + + & .codeBlock { + padding: 8px; + max-height: 250px; + overflow: scroll; + } + } +} diff --git a/src/pages/showcase/components/modal/index.tsx b/src/pages/showcase/components/modal/index.tsx new file mode 100644 index 00000000..91b12e6c --- /dev/null +++ b/src/pages/showcase/components/modal/index.tsx @@ -0,0 +1,70 @@ +"use client"; +import Button from "@/components/button"; +import Image from "next/image"; +import s from "./Modal.module.css"; +import { H3, H5, P } from "@/components/text"; +import { useState } from "react"; +import CodeBlock from "@/components/codeblock"; +import { CircleX } from "lucide-react"; +import { ShowcaseContent } from "@/types/showcase"; + +interface ModalProps { + onClose: () => void; + content: ShowcaseContent; +} + +export default function Modal({ onClose, content }: ModalProps) { + const [viewMode, setViewMode] = useState<"detail" | "config">("detail"); + const { code, title, description, author, images } = content; + + return ( +
    + + + {images[0].alt} +
    +
    + + +
    + {viewMode === "detail" ? ( + + ) : ( + {code} + )} +
    +
    +
    + ); +} + +interface DetailContentProps { + title: string; + author: string; + description: string; +} + +function DetailContent({ title, author, description }: DetailContentProps) { + return ( + <> +

    {title}

    +
    {author}
    +

    {description}

    + + ); +} diff --git a/src/pages/showcase/index.tsx b/src/pages/showcase/index.tsx new file mode 100644 index 00000000..9c59e6e9 --- /dev/null +++ b/src/pages/showcase/index.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { NavTreeNode } from "@/components/nav-tree"; +import { H1, P } from "@/components/text"; +import NavFooterLayout from "@/layouts/nav-footer-layout"; +import { DOCS_DIRECTORY } from "../docs/[...path]"; +import { loadDocsNavTreeData } from "@/lib/fetch-nav"; +import SectionWrapper from "@/components/section-wrapper"; +import s from "./ShowcasePage.module.css"; +import ListItem from "./components/list-item/list-item"; +import { useState } from "react"; +import Modal from "./components/modal"; +import { loadShowcaseContent } from "@/lib/fetch-showcase-content"; +import { ShowcaseContent } from "@/types/showcase"; + +interface ShowcasePageProps { + docsNavTree: NavTreeNode[]; + showcaseItems: ShowcaseContent[]; +} + +export async function getStaticProps(): Promise<{ props: ShowcasePageProps }> { + return { + props: { + showcaseItems: await loadShowcaseContent(), + docsNavTree: await loadDocsNavTreeData(DOCS_DIRECTORY, ""), + }, + }; +} + +export default function Showcase({ + docsNavTree, + showcaseItems, +}: ShowcasePageProps) { + const [modalContent, setModalContent] = useState( + null, + ); + + return ( + <> + + +
    +

    Showcase

    +

    A curated list of Ghostty configs from our community

    +
    +
      + {showcaseItems.map((showcase) => ( + setModalContent(showcase)} + /> + ))} +
    +
    +
    + {modalContent !== null && ( + setModalContent(null)} content={modalContent} /> + )} + + ); +} diff --git a/src/types/showcase.ts b/src/types/showcase.ts new file mode 100644 index 00000000..4c4ff4ce --- /dev/null +++ b/src/types/showcase.ts @@ -0,0 +1,10 @@ +import { ImageProps } from "next/image"; + +export interface ShowcaseContent { + id: string; + title: string; + author: string; + description: string; + images: Pick[]; + code: string; +}