23 |
50 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/src/components/app/SidebarShell.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Footer from "./Footer.astro";
3 | import SidebarNavbar from "./SidebarNavbar.astro";
4 | import SidebarTags from "./SidebarTags.astro";
5 | import Banner from "./header/Banner.astro";
6 |
7 | const { showSidebar = true } = Astro.props;
8 | ---
9 |
10 |
11 |
12 |
13 | {showSidebar &&
}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/components/app/SidebarTags.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import themeConfig from "@util/themeConfig";
3 | const tags = themeConfig.directoryData.tags;
4 | const emoji = themeConfig.layout.emoji;
5 | ---
6 |
7 |
34 |
--------------------------------------------------------------------------------
/src/components/app/header/Banner.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Dot from "./Dot.astro";
3 | import { Icon } from "astro-icon/components";
4 | import config from "@util/themeConfig";
5 |
6 |
7 | const getHref = () => {
8 | const link = config.header.banner?.link;
9 | if (link) {
10 | return link;
11 | }
12 |
13 | return "#";
14 | };
15 |
16 | const href = getHref();
17 | ---
18 |
19 | {
20 | config.header.banner.show ?
21 |
: <>>
33 | }
--------------------------------------------------------------------------------
/src/components/app/header/ColorSelector.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { ThemeSwitch } from "astro-color-scheme";
3 | import { Icon } from "astro-icon/components";
4 | import themeConfig from "@util/themeConfig";
5 |
6 | const config = themeConfig.ui.icons;
7 | ---
8 |
9 |
10 |
14 | Toggle Theme
15 |
16 |
17 |
18 |
19 |
20 |
62 |
--------------------------------------------------------------------------------
/src/components/app/header/Dot.astro:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/blog/Grid.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getCollection } from "astro:content";
3 | import PureGrid from "../directory/PureGrid.astro";
4 |
5 | const allListings = await getCollection("blog");
6 | const blogListings = allListings.map((value) => ({
7 | ...value,
8 | id: `blog/${value.id}`,
9 | }));
10 | ---
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/directory/FeaturedTag.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import configData from "@util/themeConfig";
3 | ---
4 |
5 |
8 | {configData.directoryUI.featured.labelForCard}
9 |
10 |
--------------------------------------------------------------------------------
/src/components/directory/Grid.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getListings } from "../../lib/getListings";
3 | import PureGrid from "./PureGrid.astro";
4 |
5 | const allListings = (await getListings()).sort(
6 | (a, b) => Number(b.data.featured) - Number(a.data.featured),
7 | );
8 | ---
9 |
10 |
11 |
12 |
53 |
--------------------------------------------------------------------------------
/src/components/directory/PureGrid.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import DirectoryCard from "./cards/index.astro";
3 | import config from "@util/themeConfig";
4 |
5 | const { listings } = Astro.props;
6 | const type = config.directoryUI.grid.type;
7 | const grid = type == "rectangle-card-grid" || type == "small-card-grid";
8 | ---
9 |
10 |
16 | {
17 | Array.isArray(listings) &&
18 | listings.map((e: any) => )
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/components/directory/Search.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Icon } from "astro-icon/components";
3 | import { getCollection } from "astro:content";
4 | import formatString from "../../util/formatString";
5 | import UiTagGrid from "../ui/tags/Grid.vue";
6 | import UiTagSelect from "../ui/tags/Select.vue";
7 | import config from "@util/themeConfig";
8 |
9 | const searchPlaceholder = await getSearchPlaceholder();
10 |
11 | async function getSearchPlaceholder() {
12 | if (
13 | config.directoryData.search?.placeholder &&
14 | config.directoryData.search.placeholder.includes("{0}")
15 | ) {
16 | const count = (await getCollection("directory")).length;
17 | return formatString(
18 | config.directoryData.search?.placeholder ?? "Search among {0} listings",
19 | count,
20 | );
21 | }
22 |
23 | return config.directoryData.search?.placeholder ?? "Search";
24 | }
25 | ---
26 |
27 |
28 |
29 |
30 | {
31 | config.directoryUI.search.icon ? (
32 |
33 |
38 |
39 | ) : (
40 | ""
41 | )
42 | }
43 |
48 |
49 | ⌘K
53 |
54 |
55 |
56 | {
57 | () => {
58 | const sidebar = config.layout.sidebar;
59 | if (sidebar) {
60 | return <>>
61 | }
62 | if (config.directoryUI.search.tags.display === "select") {
63 | return (
64 |
65 |
66 |
67 | );
68 | }
69 | if (config.directoryUI.search.tags.display === "show-all") {
70 | return
;
71 | }
72 | }
73 | }
74 |
75 |
76 |
101 |
--------------------------------------------------------------------------------
/src/components/directory/cards/BulletCard.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Icon from "node_modules/astro-icon/components/Icon.astro";
3 | import { Image } from "astro:assets";
4 | const { myItem, href } = Astro.props;
5 | ---
6 |
7 |
8 |
9 | {
10 | myItem.image ? (
11 |
17 | ) : (
18 |
22 | )
23 | }
24 |
25 | {myItem.title}
26 |
27 |
31 |
34 | {myItem.description}
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/components/directory/cards/RectangleCard.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import UiTag from "@components/ui/tags/Tag.astro";
3 | import DirectoryFeaturedTag from "../FeaturedTag.astro";
4 | import { Image } from "astro:assets";
5 | const { myItem, href } = Astro.props;
6 | ---
7 |
8 |
10 | { myItem.featured ? : <>> }
11 | { myItem.image ? :
14 | {myItem.title }
15 |
}
16 |
17 |
18 | { myItem?.title }
19 |
20 |
21 | { myItem.description }
22 |
23 |
24 | { myItem.tags?.map(
25 | (e: string) => )
26 | }
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/components/directory/cards/SmallHorizontalCard.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Image } from "astro:assets";
3 | import DirectoryFeaturedTag from "../FeaturedTag.astro";
4 | const { myItem, href } = Astro.props;
5 | const type = "small-horizontal";
6 | ---
7 |
8 |
12 | { myItem.featured ? : <>> }
13 |
14 | {
15 | myItem.image ? (
16 |
22 | ) : (
23 |
24 | )
25 | }
26 |
27 |
28 |
29 |
30 |
31 | {myItem.title}
32 |
33 |
34 |
35 |
36 | {myItem.description}
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/components/directory/cards/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import config from "@util/themeConfig";
3 | import BulletCard from "./BulletCard.astro";
4 | import RectangleCard from "./RectangleCard.astro";
5 | import SmallHorizontalCard from "./SmallHorizontalCard.astro";
6 |
7 | const { item } = Astro.props;
8 |
9 | const myItem = {
10 | ...item,
11 | ...item.data,
12 | };
13 |
14 | const href = config.directoryData?.source?.linksOutbound
15 | ? myItem.link
16 | : `/${myItem.id}`;
17 |
18 | const type = config.directoryUI.grid.type;
19 | ---
20 |
21 |
26 | {type == "icon-list" && }
27 | {
28 | type == "rectangle-card-grid" && (
29 |
30 | )
31 | }
32 | {
33 | type == "small-card-grid" && (
34 |
35 | )
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/src/components/directory/index.ts:
--------------------------------------------------------------------------------
1 | import Grid from "./Grid.astro";
2 | import Search from "./Search.astro";
3 |
4 | export { Grid, Search };
5 |
--------------------------------------------------------------------------------
/src/components/hero/SimpleLeftHero.astro:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/components/hero/ThemeHero.astro:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/hero/TwoColumnHero.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Image } from "astro:assets";
3 | const { image } = Astro.props;
4 | ---
5 |
6 |
7 |
10 |
18 |
19 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/components/listings/TitleHeader.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Icon } from "astro-icon/components";
3 | import Image from "astro/components/Image.astro";
4 |
5 | const { title, link, linkText, description, image } = Astro.props;
6 | ---
7 |
8 |
9 |
28 |
{description}
29 |
30 |
--------------------------------------------------------------------------------
/src/components/ui/ListCard.astro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/masterkram/minted-directory-astro/f71c8ae3fcff741285107415701fb6d1390f55b6/src/components/ui/ListCard.astro
--------------------------------------------------------------------------------
/src/components/ui/ListItem.astro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/masterkram/minted-directory-astro/f71c8ae3fcff741285107415701fb6d1390f55b6/src/components/ui/ListItem.astro
--------------------------------------------------------------------------------
/src/components/ui/tags/Grid.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
32 | {{ tag.name }}
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/components/ui/tags/Select.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
35 | removeTag(myTag)"
36 | class="absolute text-gray-500 opacity-0 transition-all group-hover:opacity-100 hover:bg-gray-100 flex items-center justify-center -top-4 left-0 bg-white rounded-full h-6 w-6 border dark:bg-gray-700 dark:border-gray-600 dark:hover:bg-gray-800">
37 |
38 |
39 | {{ myTag }}
40 |
41 |
43 |
44 | Select a tag
45 |
46 | {{ tag.name }}
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/components/ui/tags/Tag.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import type Tag from "../../../types/Tag";
3 | import themeConfig from "@util/themeConfig";
4 | const config = themeConfig.directoryData.tags;
5 |
6 | const { tag } = Astro.props;
7 |
8 | const configTag = config?.find((element: any) => element.key === tag);
9 |
10 | const tagClass = configTag?.color ? `${configTag.color}-tag` : "gray-tag";
11 | ---
12 |
13 |
14 | {configTag?.name || tag}
15 |
16 |
--------------------------------------------------------------------------------
/src/config/settings.toml:
--------------------------------------------------------------------------------
1 | # available directory themes:
2 | # spearmint, peppermint, hemingway, brookmint
3 | theme = 'brookmint'
4 |
5 | [general]
6 | title = "Meditation Apps"
7 | logo = ""
8 | iconLogo = "tabler:coffee"
9 |
10 | [general.seo]
11 | name = "Cafe Directory"
12 | description = "Find the best nuxt starter kits."
13 | url = "https://nuxtstarters.com"
14 |
15 | [directoryData.source]
16 | name = "json" # default, mock, sheets, json
17 | links = "normal"
18 |
19 | [directoryData.tagPages]
20 | title = "{0} Apps"
21 |
22 | [directoryData.search]
23 | placeholder = "Search among {0} listings of this directory :)"
24 |
25 | [[directoryData.tags]]
26 | key = "breathing"
27 | name = "Breathing"
28 | color = "blue"
29 | emoji = "👃"
30 | description = "Breathing techniques are a popular feature in meditation apps."
31 |
32 | [[directoryData.tags]]
33 | key = "sleep"
34 | name = "Sleep"
35 | color = "green"
36 | emoji = "💤"
37 | description = "Some apps include music that slowly goes away to help you sleep better and rest."
38 |
39 | [[directoryData.tags]]
40 | key = "meditation"
41 | name = "Meditation"
42 | color = "purple"
43 | emoji = "🧘♂️"
44 | description = "A meditation for your life. It's a great way to relax and clear your mind. It's also a great way to improve your sleep."
45 |
46 | [[directoryData.tags]]
47 | key = "yoga"
48 | name = "Yoga"
49 | color = "purple"
50 | emoji = "🧘♂️"
51 | description = "A yoga for your life."
52 |
53 | [[directoryData.tags]]
54 | key = "timer"
55 | name = "Timer"
56 | color = "green"
57 | emoji = "⏰"
58 | description = "Timers can be used to track how much you meditate."
59 |
60 | [header.banner]
61 | show = true
62 | text = "Follow mark_bruderer on twitter."
63 | link = "https://x.com/intent/follow?screen_name=mark_bruderer"
64 | brandText = "AI Agent Libraries"
65 |
66 | [header.navbar]
67 | colorModeSelector = true
68 |
69 | [[header.navbar.links]]
70 | name = "Blog"
71 | href = "/blog"
72 |
73 | [[header.navbar.links]]
74 | name = "Analytics"
75 | href = "https://us.posthog.com/shared/vHlGCcSqPjGH4r_QijaXSETtmN9J-g"
76 | target = "_blank"
77 |
78 | [header.actionButton]
79 | text = "Submit an app"
80 | href = "https://tally.so"
81 |
82 | [footer]
83 | description = "Best directory for my niche."
84 |
85 | [footer.socials.x]
86 | link = "https://x.com/mark_bruderer"
87 |
88 | [footer.socials.youtube]
89 | link = "https://www.youtube.com/@mark_hacks"
90 |
--------------------------------------------------------------------------------
/src/config/themes/brookmint.toml:
--------------------------------------------------------------------------------
1 | [layout]
2 | sidebar = true
3 | emoji = true
4 |
5 | [listings]
6 | pageHeader = 'title'
7 |
8 | [directoryUI.search]
9 | placeholder = "Search among {0} agent frameworks"
10 | showCount = true
11 | icon = "tabler:search"
12 |
13 | [directoryUI.search.tags]
14 | display = "show-all"
15 | intersection = false
16 |
17 | [directoryUI.grid]
18 | list = false
19 | type = "small-card-grid"
20 |
21 | [directoryUI.grid.emptyState]
22 | text = "Seems that this entry is missing from the archives."
23 | type = "button" # options: button, simple, link
24 | icon = "tabler:exclamation-mark"
25 |
26 | [directoryUI.grid.card]
27 | image = true
28 |
29 | [directoryUI.grid.submit]
30 | show = true
31 | first = false
32 | title = "Submit a template"
33 | description = "Submit a template to show off a good project to other people."
34 | hideable = true
35 |
36 | [directoryUI.featured]
37 | showOnAllPages = true
38 | showOnSide = true
39 | icon = "tabler:star"
40 | labelForCard = "Featured ✨"
41 |
42 | [ui.icons]
43 | dark = "tabler:moon"
44 | light = "tabler:sun"
45 | youtube = "tabler:brand-youtube"
46 | x = "tabler:brand-twitter"
47 | instagram = "tabler:brand-instagram"
48 | facebook = "tabler:brand-facebook"
49 | github = "tabler:brand-github"
50 |
--------------------------------------------------------------------------------
/src/config/themes/hemingway.toml:
--------------------------------------------------------------------------------
1 | [layout]
2 | sidebar = false
3 | emoji = false
4 |
5 | [listings]
6 | pageHeader = 'title' # none, title
7 |
8 | [directoryUI.search]
9 | placeholder = "Search among {0} agent frameworks"
10 | showCount = true
11 | icon = "tabler:bow"
12 |
13 | [directoryUI.search.tags]
14 | display = "select" # options: none,select,show-all
15 | intersection = false
16 |
17 | [directoryUI.grid]
18 | list = false
19 | type = "icon-list" # options: icon-list, small-card-grid, rectangle-card-grid
20 |
21 | [directoryUI.grid.emptyState]
22 | text = "Seems that this entry is missing from the archives."
23 | type = "button" # options: button, simple, link
24 | icon = "tabler:exclamation-mark"
25 |
26 | [directoryUI.grid.card]
27 | image = true
28 |
29 | [directoryUI.grid.submit]
30 | show = true
31 | first = false
32 | title = "Submit a template"
33 | description = "Submit a template to show off a good project to other people."
34 | hideable = true
35 |
36 | [directoryUI.featured]
37 | showOnAllPages = true
38 | showOnSide = true
39 | icon = "tabler:star"
40 | labelForCard = "Featured ✨"
41 |
42 | [ui.icons]
43 | dark = "tabler:moon"
44 | light = "tabler:sun"
45 | youtube = "tabler:brand-youtube"
46 | x = "tabler:brand-twitter"
47 | instagram = "tabler:brand-instagram"
48 | facebook = "tabler:brand-facebook"
49 | github = "tabler:brand-github"
--------------------------------------------------------------------------------
/src/config/themes/peppermint.toml:
--------------------------------------------------------------------------------
1 | [layout]
2 | sidebar = false
3 | emoji = false
4 |
5 | [listings]
6 | pageHeader = 'title' # none, title
7 |
8 | [directoryUI.search]
9 | placeholder = "Search among {0} agent frameworks"
10 | showCount = true
11 | icon = "tabler:bow"
12 |
13 | [directoryUI.search.tags]
14 | display = "select" # options: none,select,show-all
15 | intersection = false
16 |
17 | [directoryUI.grid]
18 | list = false
19 | type = "rectangle-card-grid" # options: icon-list, small-card-grid, rectangle-card-grid
20 |
21 | [directoryUI.grid.emptyState]
22 | text = "Seems that this entry is missing from the archives."
23 | type = "button" # options: button, simple, link
24 | icon = "tabler:exclamation-mark"
25 |
26 | [directoryUI.grid.card]
27 | image = true
28 |
29 | [directoryUI.grid.submit]
30 | show = true
31 | first = false
32 | title = "Submit a template"
33 | description = "Submit a template to show off a good project to other people."
34 | hideable = true
35 |
36 | [directoryUI.featured]
37 | showOnAllPages = true
38 | showOnSide = true
39 | icon = "tabler:star"
40 | labelForCard = "Featured ✨"
41 |
42 | [ui.icons]
43 | dark = "tabler:moon"
44 | light = "tabler:sun"
45 | youtube = "tabler:brand-youtube"
46 | x = "tabler:brand-twitter"
47 | instagram = "tabler:brand-instagram"
48 | facebook = "tabler:brand-facebook"
49 | github = "tabler:brand-github"
--------------------------------------------------------------------------------
/src/config/themes/spearmint.toml:
--------------------------------------------------------------------------------
1 | [layout]
2 | sidebar = false
3 | emoji = false
4 |
5 | [listings]
6 | pageHeader = 'none' # none, title
7 |
8 | [directoryUI.search]
9 | placeholder = "Search among {0} agent frameworks"
10 | showCount = true
11 | icon = "tabler:bow"
12 |
13 | [directoryUI.search.tags]
14 | display = "show-all" # options: none,select,show-all
15 | intersection = false
16 |
17 | [directoryUI.grid]
18 | list = false
19 | type = "small-card-grid" # options: icon-list, small-card-grid, rectangle-card-grid
20 |
21 | [directoryUI.grid.emptyState]
22 | text = "Seems that this entry is missing from the archives."
23 | type = "button" # options: button, simple, link
24 | icon = "tabler:exclamation-mark"
25 |
26 | [directoryUI.grid.card]
27 | image = true
28 |
29 | [directoryUI.grid.submit]
30 | show = true
31 | first = false
32 | title = "Submit a template"
33 | description = "Submit a template to show off a good project to other people."
34 | hideable = true
35 |
36 | [directoryUI.featured]
37 | showOnAllPages = true
38 | showOnSide = true
39 | icon = "tabler:star"
40 | labelForCard = "Featured ✨"
41 |
42 | [ui.icons]
43 | dark = "tabler:moon"
44 | light = "tabler:sun"
45 | youtube = "tabler:brand-youtube"
46 | x = "tabler:brand-twitter"
47 | instagram = "tabler:brand-instagram"
48 | facebook = "tabler:brand-facebook"
49 | github = "tabler:brand-github"
--------------------------------------------------------------------------------
/src/content.config.ts:
--------------------------------------------------------------------------------
1 | import { z, defineCollection } from "astro:content";
2 | import { glob } from 'astro/loaders';
3 | import { createDirectoryCollection } from "@lib/loaders";
4 |
5 | const directory = createDirectoryCollection();
6 |
7 | const pages = defineCollection({
8 | loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: "./src/data/pages" }),
9 | schema: ({ image }) => z.object({
10 | image: image().optional(),
11 | title: z.string().optional(),
12 | tags: z.array(z.string()).optional(),
13 | }),
14 | });
15 |
16 | const blog = defineCollection({
17 | loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: "./src/data/blog" }),
18 | schema: ({image}) => z.object({
19 | title: z.string().optional(),
20 | tags: z.array(z.string()).optional(),
21 | image: image().optional(),
22 | }),
23 | });
24 |
25 | export const collections = {
26 | directory,
27 | pages,
28 | blog,
29 | };
30 |
--------------------------------------------------------------------------------
/src/data/blog/test.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: blog article demo
3 | description: a description.
4 | ---
5 |
6 | # ngmi
--------------------------------------------------------------------------------
/src/data/directory/directory.csv:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/masterkram/minted-directory-astro/f71c8ae3fcff741285107415701fb6d1390f55b6/src/data/directory/directory.csv
--------------------------------------------------------------------------------
/src/data/directory/directory.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "calm",
4 | "title": "Calm",
5 | "image": "../images/calm.png",
6 | "featured": true,
7 | "tags": [
8 | "breathing",
9 | "sleep",
10 | "meditation"
11 | ],
12 | "description": "Offers guided meditations, sleep stories, and calming music to help users relax and improve sleep.",
13 | "link": "https://minteddirectory.com"
14 | },
15 | {
16 | "id": "headspace",
17 | "title": "Squirtle",
18 | "image": "../images/007.png",
19 | "tags": [
20 | "breathing",
21 | "sleep",
22 | "meditation"
23 | ],
24 | "description": "Provides a variety of guided meditations, sleep content, and mindfulness exercises for daily use.",
25 | "link": "https://minteddirectory.com"
26 | },
27 | {
28 | "id": "insight-timer",
29 | "title": "Insight Timer",
30 | "image": "",
31 | "description": "Features a vast library of guided meditations, talks, and music tracks from teachers worldwide.",
32 | "link": "https://minteddirectory.com"
33 | },
34 | {
35 | "id": "buddhify",
36 | "title": "Buddhify",
37 | "image": "",
38 | "description": "Customizable meditation sessions for various situations, with a one-time payment model.",
39 | "link": "https://minteddirectory.com"
40 | },
41 | {
42 | "id": "smiling-mind",
43 | "title": "Smiling Mind",
44 | "image": "",
45 | "description": "A non-profit app designed by psychologists, offering mindfulness programs for different age groups and settings.",
46 | "link": "https://minteddirectory.com"
47 | },
48 | {
49 | "id": "meditopia",
50 | "title": "Meditopia",
51 | "tags": [
52 | "breathing",
53 | "meditation"
54 | ],
55 | "image": "",
56 | "description": "Focuses on sleep and stress reduction with guided meditations in multiple languages.",
57 | "link": "https://minteddirectory.com"
58 | },
59 | {
60 | "id": "happier",
61 | "title": "10% Happier",
62 | "tags": [
63 | "yoga",
64 | "meditation"
65 | ],
66 | "image": "",
67 | "description": "Teaches meditation through videos, guided sessions, and a personal coaching feature.",
68 | "link": "https://minteddirectory.com"
69 | },
70 | {
71 | "id": "simple-habit",
72 | "title": "Simple Habit",
73 | "image": "",
74 | "description": "Offers short, 5-minute meditations for busy individuals, with sessions led by mindfulness experts.",
75 | "link": "https://minteddirectory.com"
76 | },
77 | {
78 | "id": "aura",
79 | "title": "Aura",
80 | "image": "",
81 | "description": "Personalized mindfulness meditations, stories, and coaching to reduce stress and anxiety.",
82 | "link": "https://minteddirectory.com"
83 | },
84 | {
85 | "id": "breath",
86 | "title": "Breethe",
87 | "image": "",
88 | "description": "Provides guided meditations, inspirational talks, and supportive community features.",
89 | "link": "https://minteddirectory.com"
90 | },
91 | {
92 | "id": "mylife",
93 | "title": "MyLife Meditation",
94 | "image": "",
95 | "description": "Offers mood tracking and personalized meditation recommendations based on user emotions.",
96 | "link": "https://minteddirectory.com"
97 | },
98 | {
99 | "id": "waking-up",
100 | "title": "Waking Up",
101 | "image": "",
102 | "description": "Created by neuroscientist Sam Harris, offering guided meditations and philosophical insights.",
103 | "link": "https://minteddirectory.com"
104 | },
105 | {
106 | "id": "medito",
107 | "title": "Medito",
108 | "image": "",
109 | "description": "A free meditation app with a variety of guided sessions and customizable features.",
110 | "link": "https://minteddirectory.com"
111 | },
112 | {
113 | "id": "healthy-minds-program",
114 | "title": "Healthy Minds Program",
115 | "image": "",
116 | "description": "Focuses on increasing awareness and well-being through guided meditations and podcast-style content.",
117 | "link": "https://minteddirectory.com"
118 | },
119 | {
120 | "id": "sattva",
121 | "title": "Sattva",
122 | "image": "",
123 | "description": "Offers guided meditations, chants, and mantras, with a focus on ancient Vedic principles.",
124 | "link": "https://minteddirectory.com"
125 | },
126 | {
127 | "id": "mindfullness",
128 | "title": "Mindfulness.com",
129 | "image": "",
130 | "description": "Provides a range of mindfulness meditations and courses for beginners and experienced practitioners.",
131 | "link": "https://minteddirectory.com"
132 | },
133 | {
134 | "id": "oak",
135 | "title": "Oak",
136 | "image": "",
137 | "description": "A free meditation and breathing app with customizable sessions and progress tracking.",
138 | "link": "https://minteddirectory.com"
139 | },
140 | {
141 | "id": "stop-breathe-think",
142 | "title": "Stop, Breathe & Think",
143 | "image": "",
144 | "description": "Offers mindfulness and meditation activities tailored to user emotions and experiences.",
145 | "link": "https://minteddirectory.com"
146 | },
147 | {
148 | "id": "mindfullness-app",
149 | "title": "The Mindfulness App",
150 | "image": "",
151 | "description": "Provides guided and silent meditations with customizable session lengths and reminders.",
152 | "link": "https://minteddirectory.com"
153 | },
154 | {
155 | "id": "zenfie",
156 | "title": "Zenfie",
157 | "image": "",
158 | "description": "Offers a variety of meditation techniques, including breathwork, sound baths, and guided journeys.",
159 | "link": "https://minteddirectory.com"
160 | }
161 | ]
--------------------------------------------------------------------------------
/src/data/directory/starter.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Cafe Condesa
3 | description: This is an example listing to get you started.
4 | tags:
5 | - visual
6 | link: https://test.com
7 | ---
8 |
9 | Welcome
10 |
11 | ```py
12 | print("hello world")
13 | ```
--------------------------------------------------------------------------------
/src/data/directory/starter2.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Markdown Demo
3 | description: This is an example listing to get you started.
4 | tags:
5 | - visual
6 | link: https://test.com
7 | ---
8 |
9 | ## h2 Heading
10 | ### h3 Heading
11 | #### h4 Heading
12 | ##### h5 Heading
13 | ###### h6 Heading
14 |
15 |
16 | ## Horizontal Rules
17 |
18 | ___
19 |
20 | ---
21 |
22 | ***
23 |
24 |
25 | ## Typographic replacements
26 |
27 | Enable typographer option to see result.
28 |
29 | (c) (C) (r) (R) (tm) (TM) (p) (P) +-
30 |
31 | test.. test... test..... test?..... test!....
32 |
33 | !!!!!! ???? ,, -- ---
34 |
35 | "Smartypants, double quotes" and 'single quotes'
36 |
37 |
38 | ## Emphasis
39 |
40 | **This is bold text**
41 |
42 | __This is bold text__
43 |
44 | *This is italic text*
45 |
46 | _This is italic text_
47 |
48 | ~~Strikethrough~~
49 |
50 |
51 | ## Blockquotes
52 |
53 |
54 | > Blockquotes can also be nested...
55 | >> ...by using additional greater-than signs right next to each other...
56 | > > > ...or with spaces between arrows.
57 |
58 |
59 | ## Lists
60 |
61 | Unordered
62 |
63 | + Create a list by starting a line with `+`, `-`, or `*`
64 | + Sub-lists are made by indenting 2 spaces:
65 | - Marker character change forces new list start:
66 | * Ac tristique libero volutpat at
67 | + Facilisis in pretium nisl aliquet
68 | - Nulla volutpat aliquam velit
69 | + Very easy!
70 |
71 | Ordered
72 |
73 | 1. Lorem ipsum dolor sit amet
74 | 2. Consectetur adipiscing elit
75 | 3. Integer molestie lorem at massa
76 |
77 |
78 | 1. You can use sequential numbers...
79 | 1. ...or keep all the numbers as `1.`
80 |
81 | Start numbering with offset:
82 |
83 | 57. foo
84 | 1. bar
85 |
86 |
87 | ## Code
88 |
89 | Inline `code`
90 |
91 | Indented code
92 |
93 | // Some comments
94 | line 1 of code
95 | line 2 of code
96 | line 3 of code
97 |
98 |
99 | Block code "fences"
100 |
101 | ```
102 | Sample text here...
103 | ```
104 |
105 | Syntax highlighting
106 |
107 | ``` js
108 | var foo = function (bar) {
109 | return bar++;
110 | };
111 |
112 | console.log(foo(5));
113 | ```
114 |
115 | ## Tables
116 |
117 | | Option | Description |
118 | | ------ | ----------- |
119 | | data | path to data files to supply the data that will be passed into templates. |
120 | | engine | engine to be used for processing templates. Handlebars is the default. |
121 | | ext | extension to be used for dest files. |
122 |
123 | Right aligned columns
124 |
125 | | Option | Description |
126 | | ------:| -----------:|
127 | | data | path to data files to supply the data that will be passed into templates. |
128 | | engine | engine to be used for processing templates. Handlebars is the default. |
129 | | ext | extension to be used for dest files. |
130 |
131 |
132 | ## Links
133 |
134 | [link text](http://dev.nodeca.com)
135 |
136 | [link with title](http://nodeca.github.io/pica/demo/ "title text!")
137 |
138 | Autoconverted link https://github.com/nodeca/pica (enable linkify to see)
139 |
140 |
141 | ## Images
142 |
143 | 
144 | 
145 |
146 | Like links, Images also have a footnote style syntax
147 |
148 | ![Alt text][id]
149 |
150 | With a reference later in the document defining the URL location:
151 |
152 | [id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat"
153 |
154 |
155 | ## Plugins
156 |
157 | The killer feature of `markdown-it` is very effective support of
158 | [syntax plugins](https://www.npmjs.org/browse/keyword/markdown-it-plugin).
159 |
160 |
161 | ### [Emojies](https://github.com/markdown-it/markdown-it-emoji)
162 |
163 | > Classic markup: :wink: :cry: :laughing: :yum:
164 | >
165 | > Shortcuts (emoticons): :-) :-( 8-) ;)
166 |
167 | see [how to change output](https://github.com/markdown-it/markdown-it-emoji#change-output) with twemoji.
168 |
169 |
170 | ### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup)
171 |
172 | - 19^th^
173 | - H~2~O
174 |
175 |
176 | ### [\
](https://github.com/markdown-it/markdown-it-ins)
177 |
178 | ++Inserted text++
179 |
180 |
181 | ### [\](https://github.com/markdown-it/markdown-it-mark)
182 |
183 | ==Marked text==
184 |
185 |
186 | ### [Footnotes](https://github.com/markdown-it/markdown-it-footnote)
187 |
188 | Footnote 1 link[^first].
189 |
190 | Footnote 2 link[^second].
191 |
192 | Inline footnote^[Text of inline footnote] definition.
193 |
194 | Duplicated footnote reference[^second].
195 |
196 | [^first]: Footnote **can have markup**
197 |
198 | and multiple paragraphs.
199 |
200 | [^second]: Footnote text.
201 |
202 |
203 | ### [Definition lists](https://github.com/markdown-it/markdown-it-deflist)
204 |
205 | Term 1
206 |
207 | : Definition 1
208 | with lazy continuation.
209 |
210 | Term 2 with *inline markup*
211 |
212 | : Definition 2
213 |
214 | { some code, part of Definition 2 }
215 |
216 | Third paragraph of definition 2.
217 |
218 | _Compact style:_
219 |
220 | Term 1
221 | ~ Definition 1
222 |
223 | Term 2
224 | ~ Definition 2a
225 | ~ Definition 2b
226 |
227 |
228 | ### [Abbreviations](https://github.com/markdown-it/markdown-it-abbr)
229 |
230 | This is HTML abbreviation example.
231 |
232 | It converts "HTML", but keep intact partial entries like "xxxHTMLyyy" and so on.
233 |
234 | *[HTML]: Hyper Text Markup Language
235 |
236 | ### [Custom containers](https://github.com/markdown-it/markdown-it-container)
237 |
238 | ::: warning
239 | *here be dragons*
240 | :::
241 |
--------------------------------------------------------------------------------
/src/data/directory/starter3.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Example Listing
3 | description: This is an example listing to get you started.
4 | tags:
5 | - visual
6 | link: https://test.com
7 | ---
8 |
9 | Welcome
10 |
11 | ```py
12 | print("hello world")
13 | ```
--------------------------------------------------------------------------------
/src/data/images/007.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/masterkram/minted-directory-astro/f71c8ae3fcff741285107415701fb6d1390f55b6/src/data/images/007.png
--------------------------------------------------------------------------------
/src/data/images/calm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/masterkram/minted-directory-astro/f71c8ae3fcff741285107415701fb6d1390f55b6/src/data/images/calm.png
--------------------------------------------------------------------------------
/src/data/pages/blog.mdx:
--------------------------------------------------------------------------------
1 | import Grid from '../../components/blog/Grid.astro'
2 | import SimpleLeftHero from '../../components/hero/SimpleLeftHero.astro'
3 | import Search from '../../components/directory/Search.astro'
4 |
5 |
6 | The Blog
7 | Here posts will be written with content related to the keywords of the directory to boost the ranking on search engines.
8 |
9 |
10 | ---
11 |
12 |
--------------------------------------------------------------------------------
/src/data/pages/index.mdx:
--------------------------------------------------------------------------------
1 | import Grid from '@/components/directory/Grid.astro'
2 | import SimpleLeftHero from '@/components/hero/SimpleLeftHero.astro'
3 | import Search from '@/components/directory/Search.astro'
4 |
5 | # Minted Directory
6 |
7 | Welcome to this fully customizable directory.
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
--------------------------------------------------------------------------------
/src/layouts/Article.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import "@fontsource/gabarito";
3 | import BaseLayout from "./BaseLayout.astro";
4 | import AppProse from "../components/app/Prose.astro";
5 | const { frontmatter, slug } = Astro.props;
6 | import themeConfig from "@util/themeConfig";
7 | import AppShell from "@components/app/AppShell.astro";
8 | const siteTitle = themeConfig.general.title;
9 | const title = frontmatter?.title || siteTitle;
10 | const description = frontmatter?.description;
11 | ---
12 |
13 |
14 |
15 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/layouts/BaseLayout.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import "@fontsource-variable/gabarito";
3 | import themeConfig from "@util/themeConfig";
4 | import "../styles/global.css";
5 | import Posthog from "@components/analytics/Posthog.astro";
6 | import { ClientRouter } from "astro:transitions";
7 | import { getOGImage } from "@util/getOGImage";
8 |
9 | const { title, slug } = Astro.props;
10 | const siteTitle = themeConfig.general.title;
11 |
12 | const calculatedTitle = title || siteTitle;
13 | ---
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {calculatedTitle}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/layouts/Card.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import AppShell from "@components/app/AppShell.astro";
3 | import Prose from "../components/app/Prose.astro";
4 | import BaseLayout from "./BaseLayout.astro";
5 | ---
6 |
7 |
8 |
9 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/layouts/Landing.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import config from "@util/themeConfig";
3 | import Sidebar from "./Sidebar.astro";
4 | import Wide from "./Wide.astro";
5 |
6 | const sidebar = config.layout.sidebar;
7 | ---
8 |
9 | {
10 | sidebar ? (
11 |
12 |
13 |
14 | ) : (
15 |
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/src/layouts/Listing.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import BaseLayout from "./BaseLayout.astro";
3 | import AppProse from "../components/app/Prose.astro";
4 | const { frontmatter, slug } = Astro.props;
5 | import themeConfig from "@util/themeConfig";
6 | import { Icon } from "astro-icon/components";
7 | import AppShell from "@components/app/AppShell.astro";
8 | import config from "@util/themeConfig";
9 | import SidebarShell from "@components/app/SidebarShell.astro";
10 | import TitleHeader from "@components/listings/TitleHeader.astro";
11 |
12 | const siteTitle = themeConfig.general.title;
13 | const title = frontmatter?.title || siteTitle;
14 | const link = frontmatter?.link;
15 | const description = frontmatter?.description;
16 | const linkText = link?.replace(/^https?:\/\//, "");
17 | const pageHeader = themeConfig.listings.pageHeader;
18 | const sidebar = config.layout.sidebar;
19 | ---
20 |
21 |
22 | {
23 | sidebar ? (
24 |
25 |
26 | {pageHeader == "title" && (
27 |
34 | )}
35 |
36 |
37 |
38 |
39 |
40 | ) : (
41 |
42 |
43 | {pageHeader == "title" && (
44 |
45 |
59 |
{description}
60 |
61 | )}
62 |
63 |
64 |
65 |
66 |
67 | )
68 | }
69 |
70 |
--------------------------------------------------------------------------------
/src/layouts/Sidebar.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import SidebarShell from "@components/app/SidebarShell.astro";
3 | import Prose from "../components/app/Prose.astro";
4 | import BaseLayout from "./BaseLayout.astro";
5 | ---
6 |
7 |
8 |
9 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/layouts/Thin.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import AppShell from "@components/app/AppShell.astro";
3 | import Prose from "../components/app/Prose.astro";
4 | import BaseLayout from "./BaseLayout.astro";
5 | ---
6 |
7 |
8 |
9 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/layouts/Wide.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import AppShell from "@components/app/AppShell.astro";
3 | import Prose from "../components/app/Prose.astro";
4 | import BaseLayout from "./BaseLayout.astro";
5 | ---
6 |
7 |
8 |
9 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/lib/getListings.ts:
--------------------------------------------------------------------------------
1 | import { getCollection } from "astro:content";
2 |
3 | export async function getListings() {
4 | return await getCollection("directory");
5 | }
6 |
--------------------------------------------------------------------------------
/src/lib/getRootPages.ts:
--------------------------------------------------------------------------------
1 | import type { AllContent } from "../types/content";
2 | import { getCollection } from "astro:content";
3 |
4 | export async function getBlogPages() {
5 | const allPosts = await getCollection("blog");
6 | return allPosts.map((entry) => ({params: {slug: entry.id }, props: { entry }}));
7 | }
8 |
9 | export async function getRootPages(remapIndex: boolean = true) {
10 | const allListings = await getCollection("directory");
11 | const allPages = await getCollection("pages");
12 |
13 | // Combine listings and pages
14 | const combinedEntries: Array = allListings.concat(allPages as never);
15 |
16 | // Return paths based on slugs
17 | return combinedEntries.map((entry) => {
18 | let mySlug: string = entry.id;
19 |
20 | if (mySlug === "index" && remapIndex) {
21 | mySlug = "/";
22 | }
23 |
24 | return {
25 | params: { slug: mySlug },
26 | props: { entry },
27 | };
28 | });
29 | }
30 |
--------------------------------------------------------------------------------
/src/lib/loaders/index.ts:
--------------------------------------------------------------------------------
1 | import configData from "@util/themeConfig";
2 | import { defineCollection } from "astro:content";
3 | import { sheetLoad } from "./sheets";
4 | import { directorySchema } from "@validation/directory";
5 | import { z } from "zod";
6 | import { mockLoader } from "@ascorbic/mock-loader";
7 | import { glob, file } from "astro/loaders";
8 | import { airtableLoader } from "@ascorbic/airtable-loader";
9 | import { notionLoader } from "notion-astro-loader";
10 |
11 | export function createDirectoryCollection() {
12 | const source = configData.directoryData.source.name;
13 |
14 | if (source === 'sheets') {
15 | return defineCollection({
16 | loader: sheetLoad(),
17 | schema: directorySchema(z.string().url())
18 | });
19 | }
20 | if (source === 'mock') {
21 | return defineCollection({
22 | loader: mockLoader({schema: directorySchema(z.string().url()), entryCount: 10},),
23 | schema: directorySchema(z.undefined())
24 | });
25 | }
26 | if (source === 'json') {
27 | return defineCollection({
28 | loader: file('src/data/directory/directory.json'),
29 | schema: ({ image }) => directorySchema(image())
30 | });
31 | }
32 | if (source === 'csv') {
33 | return defineCollection({
34 | loader: file('src/data/directory/directory.csv'),
35 | schema: ({ image }) => directorySchema(image())
36 | });
37 | }
38 | if (source === 'airtable') {
39 | const airtableConfig = configData.directoryData.source.airtable;
40 | if (!airtableConfig?.base || !airtableConfig.name) {
41 | throw Error('You need to configure a airtable base id and table name to be able to connect with airtable data.')
42 | }
43 |
44 | return defineCollection({
45 | loader: airtableLoader({
46 | base: airtableConfig.base,
47 | table: airtableConfig.name,
48 | }),
49 | schema: directorySchema(z.string().url())
50 | });
51 | }
52 | if (source === 'notion') {
53 | const notionToken = import.meta.env.NOTION_TOKEN;
54 | const databaseId = configData.directoryData.source.notion?.databaseId;
55 |
56 | if (!notionToken || !databaseId) {
57 | throw Error('You need to add a notion token in the .env as NOTION_TOKEN file and your databaseId in the settings.toml to use notion')
58 | }
59 |
60 | return defineCollection({
61 | loader: notionLoader({
62 | auth: notionToken,
63 | database_id: databaseId
64 | }),
65 | schema: directorySchema(z.string().url())
66 | });
67 | }
68 |
69 | return defineCollection({
70 | loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: "./src/data/directory" }),
71 | schema: ({ image }) => directorySchema(image())
72 | });
73 | }
--------------------------------------------------------------------------------
/src/lib/loaders/sheets.ts:
--------------------------------------------------------------------------------
1 | import { sheetLoader } from "astro-sheet-loader";
2 | import configData from "@util/themeConfig";
3 |
4 | export const sheetLoad = () => {
5 | try {
6 | return sheetLoader({document: configData!.directoryData!.source!.sheets!.key});
7 | } catch(error) {
8 | console.log("google sheets key needs to be defined to use sheets as a data source.");
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/src/pages/[...slug].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getCollection } from "astro:content";
3 | import Wide from "@layouts/Wide.astro";
4 | import Listing from "@layouts/Listing.astro";
5 | import { render } from "astro:content";
6 | import Sidebar from "@layouts/Sidebar.astro";
7 | import Landing from "@layouts/Landing.astro";
8 | import { getRootPages } from "@lib/getRootPages";
9 |
10 | // Fetch all listings and pages
11 | const { slug } = Astro.params;
12 | const { entry } = Astro.props;
13 |
14 | const { Content } = await render(entry);
15 |
16 | export async function getStaticPaths() {
17 | return await getRootPages();
18 | }
19 |
20 | const landingView = !slug || slug === "blog";
21 | ---
22 |
23 | {
24 | landingView ? (
25 |
26 |
27 |
28 | ) : (
29 |
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/src/pages/blog/[slug].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { getCollection } from "astro:content";
3 | import { render } from "astro:content";
4 | import Article from "@layouts/Article.astro";
5 |
6 | const { entry } = Astro.props;
7 | const { Content } = await render(entry);
8 |
9 | export async function getStaticPaths() {
10 | const allListings = await getCollection("blog");
11 |
12 | return allListings.map((entry) => {
13 | let mySlug: string = entry.id;
14 | return {
15 | params: { slug: mySlug },
16 | props: { entry },
17 | };
18 | });
19 | }
20 | ---
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/pages/og/[...slug].png.ts:
--------------------------------------------------------------------------------
1 | import { getCollection, getEntry } from 'astro:content';
2 | import fs from 'fs';
3 | import path from 'path';
4 | import { ImageResponse } from '@vercel/og';
5 | import { getRootPages } from '@lib/getRootPages';
6 | import config from '@util/themeConfig';
7 | import { type AllContent } from '../../types/content';
8 |
9 | const boldFontPath = 'node_modules/@fontsource/gabarito/files/gabarito-latin-700-normal.woff' as const;
10 | const regularFontPath = 'node_modules/@fontsource/gabarito/files/gabarito-latin-400-normal.woff' as const;
11 |
12 | interface Props {
13 | params: { slug: string };
14 | }
15 |
16 | function getPostCoverPath(entry: AllContent) {
17 | if (!entry.data.image) {
18 | return '/default-listing-image.png';
19 | }
20 | return entry.data.image.src;
21 | }
22 |
23 | function getPostCoverImage(entry: AllContent) {
24 | const imagePath = getPostCoverPath(entry);
25 | if (process.env.NODE_ENV === 'development') {
26 | return path.resolve(imagePath.replace(/\?.*/, '').replace('/@fs', ''));
27 | }
28 | return path.resolve(imagePath.replace('/', 'dist/'));
29 | }
30 |
31 | export async function GET({ params }: Props) {
32 | const title = config.general.title;
33 | console.log(params);
34 | const { slug } = params;
35 |
36 | let entry: AllContent | undefined;
37 | const allListings = (await getCollection("directory")).map(e => e.id);
38 | if (allListings.includes(slug)){
39 | entry = await getEntry('directory', slug);
40 | } else {
41 | entry = await getEntry('pages', slug);
42 | }
43 |
44 | if (!entry) {
45 | throw new Error("Unable to find " + slug);
46 | }
47 |
48 | // using custom font files
49 | const GabartitoSansBold = fs.readFileSync(path.resolve(boldFontPath));
50 | const GabaritoSansRegular = fs.readFileSync(
51 | path.resolve(regularFontPath),
52 | );
53 | let postCover;
54 | try {
55 | postCover = fs.readFileSync(getPostCoverImage(entry));
56 | } catch (error) {
57 | postCover = null;
58 | }
59 |
60 | const image = postCover ? {
61 | type: 'img',
62 | props: {
63 | src: postCover.buffer,
64 | },
65 | } : {
66 | type: 'div',
67 | props: {
68 | tw: 'bg-gray-200 rounded-full'
69 | },
70 | };
71 |
72 | const html = {
73 | type: 'div',
74 | props: {
75 | children: [
76 | {
77 | type: 'div',
78 | props: {
79 | tw: 'w-[200px] h-[200px] flex rounded-3xl overflow-hidden',
80 | children: [
81 | image
82 | ],
83 | },
84 | },
85 | {
86 | type: 'div',
87 | props: {
88 | tw: 'pl-10 shrink flex flex-col max-w-xl',
89 | children: [
90 | {
91 | type: 'div',
92 | props: {
93 | tw: 'text-zinc-800',
94 | style: {
95 | fontSize: '48px',
96 | fontFamily: 'Gabarito Bold',
97 | },
98 | children: entry.data.title,
99 | },
100 | },
101 | {
102 | type: 'div',
103 | props: {
104 | tw: 'text-zinc-500 mt-2',
105 | style: {
106 | fontSize: '18px',
107 | fontFamily: 'Gabarito Regular',
108 | },
109 | children: entry.collection === 'directory' ? entry.data.description : entry.data.title,
110 | },
111 | },
112 | ],
113 | },
114 | },
115 | {
116 | type: 'div',
117 | props: {
118 | tw: 'absolute right-[40px] bottom-[40px] flex items-center',
119 | children: [
120 | {
121 | type: 'div',
122 | props: {
123 | tw: 'text-gray-900 text-4xl',
124 | style: {
125 | fontFamily: 'Gabarito Bold',
126 | },
127 | children: `${title}`,
128 | },
129 | },
130 | ],
131 | },
132 | },
133 | ],
134 | tw: 'w-full h-full flex items-center justify-center relative px-22',
135 | style: {
136 | background: '#fff',
137 | fontFamily: 'Gabarito Regular',
138 | },
139 | },
140 | };
141 |
142 | return new ImageResponse(html, {
143 | width: 1200,
144 | height: 600,
145 | fonts: [
146 | {
147 | name: 'Gabarito Bold',
148 | data: GabartitoSansBold.buffer,
149 | style: 'normal',
150 | },
151 | {
152 | name: 'Gabarito Regular',
153 | data: GabaritoSansRegular.buffer,
154 | style: 'normal',
155 | },
156 | ],
157 | });
158 | }
159 |
160 | export async function getStaticPaths() {
161 | return await getRootPages(false);
162 | }
--------------------------------------------------------------------------------
/src/pages/og/blog/[...slug].png.ts:
--------------------------------------------------------------------------------
1 | import { getEntry, type CollectionEntry } from 'astro:content';
2 | import fs from 'fs';
3 | import path from 'path';
4 | import { ImageResponse } from '@vercel/og';
5 | import { getBlogPages, getRootPages } from '@lib/getRootPages';
6 | import config from '@util/themeConfig';
7 | import type { AllContent } from '../../../types/content';
8 |
9 | const boldFontPath = 'node_modules/@fontsource/gabarito/files/gabarito-latin-700-normal.woff' as const;
10 | const regularFontPath = 'node_modules/@fontsource/gabarito/files/gabarito-latin-400-normal.woff' as const;
11 |
12 | interface Props {
13 | params: { slug: string };
14 | }
15 |
16 | function getPostCoverPath(entry: CollectionEntry<'blog'>) {
17 | if (!entry.data.image) {
18 | return '/default-blog-image.png';
19 | }
20 | return entry.data.image.src;
21 | }
22 |
23 | function getPostCoverImage(entry: CollectionEntry<'blog'>) {
24 | const imagePath = getPostCoverPath(entry);
25 | if (process.env.NODE_ENV === 'development') {
26 | return path.resolve(imagePath.replace(/\?.*/, '').replace('/@fs', ''));
27 | }
28 | return path.resolve(imagePath.replace('/', 'dist/'));
29 | }
30 |
31 | export async function GET({ params }: Props) {
32 | const title = config.general.title;
33 | const { slug } = params;
34 | const entry = await getEntry('blog', slug);
35 |
36 | if (!entry) {
37 | throw new Error("Unable to find " + slug);
38 | }
39 |
40 | // using custom font files
41 | const GabartitoSansBold = fs.readFileSync(path.resolve(boldFontPath));
42 | const GabaritoSansRegular = fs.readFileSync(
43 | path.resolve(regularFontPath),
44 | );
45 | let postCover;
46 | try {
47 | postCover = fs.readFileSync(getPostCoverImage(entry));
48 | } catch (error) {
49 | postCover = null;
50 | }
51 |
52 | const image = postCover ? {
53 | type: 'img',
54 | props: {
55 | src: postCover.buffer,
56 | },
57 | } : {
58 | type: 'div',
59 | props: {
60 | tw: 'bg-gray-200 rounded-full'
61 | },
62 | };
63 |
64 | const html = {
65 | type: 'div',
66 | props: {
67 | children: [
68 | {
69 | type: 'div',
70 | props: {
71 | tw: 'w-[200px] h-[200px] flex rounded-3xl overflow-hidden',
72 | children: [
73 | image
74 | ],
75 | },
76 | },
77 | {
78 | type: 'div',
79 | props: {
80 | tw: 'pl-10 shrink flex flex-col max-w-xl',
81 | children: [
82 | {
83 | type: 'div',
84 | props: {
85 | tw: 'text-zinc-800',
86 | style: {
87 | fontSize: '48px',
88 | fontFamily: 'Gabarito Bold',
89 | },
90 | children: entry.data.title,
91 | },
92 | },
93 | {
94 | type: 'div',
95 | props: {
96 | tw: 'text-zinc-500 mt-2',
97 | style: {
98 | fontSize: '18px',
99 | fontFamily: 'Gabarito Regular',
100 | },
101 | children: entry.data.title,
102 | },
103 | },
104 | ],
105 | },
106 | },
107 | {
108 | type: 'div',
109 | props: {
110 | tw: 'absolute right-[40px] bottom-[40px] flex items-center',
111 | children: [
112 | {
113 | type: 'div',
114 | props: {
115 | tw: 'text-gray-900 text-4xl',
116 | style: {
117 | fontFamily: 'Gabarito Bold',
118 | },
119 | children: `${title}`,
120 | },
121 | },
122 | ],
123 | },
124 | },
125 | ],
126 | tw: 'w-full h-full flex items-center justify-center relative px-22',
127 | style: {
128 | background: '#fff',
129 | fontFamily: 'Gabarito Regular',
130 | },
131 | },
132 | };
133 |
134 | return new ImageResponse(html, {
135 | width: 1200,
136 | height: 600,
137 | fonts: [
138 | {
139 | name: 'Gabarito Bold',
140 | data: GabartitoSansBold.buffer,
141 | style: 'normal',
142 | },
143 | {
144 | name: 'Gabarito Regular',
145 | data: GabaritoSansRegular.buffer,
146 | style: 'normal',
147 | },
148 | ],
149 | });
150 | }
151 |
152 | export async function getStaticPaths() {
153 | return await getBlogPages();
154 | }
--------------------------------------------------------------------------------
/src/pages/tags/[slug].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import PureGrid from "../../components/directory/PureGrid.astro";
3 | import { getCollection } from "astro:content";
4 | import formatString from "../../util/formatString";
5 | import type Tag from "../../types/Tag";
6 | import config from "@util/themeConfig";
7 | import Sidebar from "@layouts/Sidebar.astro";
8 |
9 | export async function getStaticPaths() {
10 | const tags: Array = config.directoryData.tags;
11 |
12 | const result = tags.map((e) => ({
13 | params: { slug: e.key },
14 | props: { color: e.color, title: e.key },
15 | }));
16 |
17 | return result;
18 | }
19 |
20 | const { title } = Astro.props;
21 |
22 | const allListings = await getCollection("directory");
23 |
24 | const tag = config.directoryData.tags.find(
25 | (e: Tag) => title && e.key === title,
26 | );
27 |
28 | const calculatedTitle = tag?.name;
29 | const calculatedDescription = tag?.description;
30 | const tagListings: any[] = allListings.filter((listing: any) => {
31 | return (
32 | listing?.data?.tags instanceof Array && listing.data.tags.includes(title)
33 | );
34 | });
35 |
36 | function toTitleCase(str: string) {
37 | return str.replace(
38 | /\w\S*/g,
39 | (text) => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase(),
40 | );
41 | }
42 | ---
43 |
44 |
45 |
46 |
47 | {
48 | toTitleCase(
49 | formatString(config.directoryData.tagPages.title, calculatedTitle),
50 | )
51 | }
52 |
53 |
54 | {calculatedDescription}
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import { atom } from "nanostores";
2 |
3 | export const search = atom("");
4 |
5 | export const tags = atom([]);
6 |
--------------------------------------------------------------------------------
/src/styles/global.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @plugin "@tailwindcss/typography";
3 |
4 | @custom-variant dark (&:where(.dark, .dark *));
5 |
6 | @theme {
7 | --font-*: initial;
8 | --font-sans: 'Gabarito Variable', sans-serif;
9 | --font-mono: 'Source Code Pro' monospace;
10 |
11 | --color-gray-50: var(--color-neutral-50);
12 | --color-gray-100: var(--color-neutral-100);
13 | --color-gray-200: var(--color-neutral-200);
14 | --color-gray-300: var(--color-neutral-300);
15 | --color-gray-400: var(--color-neutral-400);
16 | --color-gray-500: var(--color-neutral-500);
17 | --color-gray-600: var(--color-neutral-600);
18 | --color-gray-700: var(--color-neutral-700);
19 | --color-gray-800: var(--color-neutral-800);
20 | --color-gray-900: var(--color-neutral-900);
21 | --color-gray-950: var(--color-neutral-950);
22 |
23 | --color-primary-50: var(--color-sky-50);
24 | --color-primary-100: var(--color-sky-100);
25 | --color-primary-200: var(--color-sky-200);
26 | --color-primary-300: var(--color-sky-300);
27 | --color-primary-400: var(--color-sky-400);
28 | --color-primary-500: var(--color-sky-500);
29 | --color-primary-600: var(--color-sky-600);
30 | --color-primary-700: var(--color-sky-700);
31 | --color-primary-800: var(--color-sky-800);
32 | --color-primary-900: var(--color-sky-900);
33 | --color-primary-950: var(--color-sky-950);
34 | }
35 |
36 |
37 | .tag {
38 | @apply inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset;
39 | }
40 |
41 | .gray-tag {
42 | @apply bg-gray-50 text-gray-600 ring-gray-500/10 bg-gray-400/10 dark:bg-gray-400/10 dark:text-gray-400 dark:ring-gray-400/20;
43 | }
44 |
45 | .green-tag {
46 | @apply bg-green-50 text-green-700 ring-green-600/20 dark:bg-green-500/10 dark:text-green-400 dark:ring-green-500/20;
47 | }
48 |
49 | .blue-tag {
50 | @apply bg-blue-50 text-blue-700 ring-blue-600/20 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/30;
51 | }
52 |
53 | .indigo-tag {
54 | @apply bg-blue-50 text-indigo-700 ring-indigo-600/20 dark:bg-indigo-400/10 dark:text-indigo-400 dark:ring-indigo-400/30
55 | }
56 |
57 |
58 | .highlight span {
59 | @apply bg-gray-800 text-white px-1 -rotate-6;
60 | }
61 |
62 | .dashedcue span {
63 | @apply underline underline-offset-4 decoration-primary-600 decoration-dashed;
64 | }
--------------------------------------------------------------------------------
/src/types/Tag.ts:
--------------------------------------------------------------------------------
1 | export default interface Tag {
2 | key?: string;
3 | name: string;
4 | color?: string; // color is optional
5 | }
6 |
--------------------------------------------------------------------------------
/src/types/ThemeConfig.ts:
--------------------------------------------------------------------------------
1 | import type Tag from "./Tag";
2 |
3 | export interface ThemeConfig {
4 | general: {
5 | title: string;
6 | logo: string;
7 | iconLogo: string;
8 | seo: {
9 | name: string;
10 | description: string;
11 | url: string;
12 | };
13 | };
14 | directory: {
15 | data: {
16 | source: "markdown" | "json";
17 | };
18 | search: {
19 | placeholder: string;
20 | showCount: boolean;
21 | icon: string;
22 | tags: {
23 | display: "none" | "select" | "show-all"; // Based on options
24 | intersection: boolean;
25 | };
26 | };
27 | grid: {
28 | list: boolean;
29 | emptyState: {
30 | text: string;
31 | type: "button" | "simple" | "link"; // Based on options
32 | icon: string;
33 | };
34 | card: {
35 | image: boolean;
36 | border: "dashed" | "shadow" | "outline"; // Based on options
37 | links: "site" | "outbound";
38 | };
39 | submit: {
40 | show: boolean;
41 | first: boolean;
42 | title: string;
43 | description: string;
44 | hideable: boolean;
45 | };
46 | };
47 | featured: {
48 | showOnAllPages: boolean;
49 | showOnSide: boolean;
50 | icon: string;
51 | labelForCard: string;
52 | };
53 | tags: Array;
54 | tagPages: {
55 | title: string;
56 | };
57 | };
58 | header: {
59 | banner: {
60 | show: boolean;
61 | text: string;
62 | link: string;
63 | brandText: string;
64 | };
65 | navbar: {
66 | colorModeSelector: boolean;
67 | links: [{ href: string; name: string; target?: string }];
68 | };
69 | actionButton: {
70 | text: string;
71 | href: string;
72 | };
73 | };
74 | footer: {
75 | description: string;
76 | socials: {
77 | github: {
78 | link: string;
79 | icon: string;
80 | };
81 | facebook: {
82 | link: string;
83 | icon: string;
84 | };
85 | instagram: {
86 | link: string;
87 | icon: string;
88 | };
89 | x: {
90 | link: string;
91 | icon: string;
92 | };
93 | youtube: {
94 | link: string;
95 | icon: string;
96 | };
97 | };
98 | };
99 | ui: {
100 | icons: {
101 | dark: string;
102 | light: string;
103 | };
104 | };
105 | }
106 |
--------------------------------------------------------------------------------
/src/types/content.ts:
--------------------------------------------------------------------------------
1 | import type { CollectionEntry } from "astro:content";
2 |
3 | export type AllContent = CollectionEntry<'directory'> | CollectionEntry<'pages'>;
--------------------------------------------------------------------------------
/src/types/global.d.ts:
--------------------------------------------------------------------------------
1 | import { ThemeConfig } from "./theme-config";
2 |
3 | declare global {
4 | var themeConfig: ThemeConfig;
5 | }
6 |
--------------------------------------------------------------------------------
/src/util/formatString.ts:
--------------------------------------------------------------------------------
1 | const formatString = (template: string, ...args: any) => {
2 | return template.replace(
3 | /{([0-9]+)}/g,
4 | function (match: string, index: number) {
5 | return typeof args[index] === "undefined" ? match : args[index];
6 | }
7 | );
8 | };
9 |
10 | export default formatString;
11 |
--------------------------------------------------------------------------------
/src/util/getOGImage.ts:
--------------------------------------------------------------------------------
1 | import themeConfig from "./themeConfig"
2 |
3 | function getBasePath(): string {
4 | if (process.env.NODE_ENV === 'development') {
5 | return 'http://localhost:4321';
6 | }
7 |
8 | return themeConfig.general.seo.url;
9 | }
10 |
11 | export function getOGImage(slug: string) {
12 | let basePath: string = getBasePath();
13 | return `${basePath}/og/${slug}.png`;
14 | }
--------------------------------------------------------------------------------
/src/util/themeConfig.ts:
--------------------------------------------------------------------------------
1 | import { settingsSchema, themeSchema, themeSettingsSchema, type SettingsSchema } from "@validation/settings";
2 | import configData from "../config/settings.toml";
3 | import peppermint from "../config/themes/peppermint.toml";
4 | import spearmint from "../config/themes/spearmint.toml";
5 | import brookmint from "../config/themes/brookmint.toml";
6 | import hemingway from "../config/themes/hemingway.toml";
7 |
8 | function getConfig(data: unknown) {
9 | try {
10 | return themeSettingsSchema.parse(data);
11 | } catch (error) {
12 | return null;
13 | }
14 | }
15 |
16 | const themes = {
17 | peppermint: themeSchema.parse(peppermint),
18 | spearmint: themeSchema.parse(spearmint),
19 | brookmint: themeSchema.parse(brookmint),
20 | hemingway: themeSchema.parse(hemingway),
21 | };
22 |
23 | const data = getConfig(configData);
24 | let settings: SettingsSchema;
25 |
26 | if (data) {
27 | const selectedTheme = configData.theme || "peppermint";
28 | const themeConfig = themes[selectedTheme as keyof typeof themes];
29 |
30 | settings = {
31 | ...themeConfig,
32 | ...data,
33 | };
34 | } else {
35 | settings = settingsSchema.parse(configData);
36 | }
37 |
38 | export default settings;
--------------------------------------------------------------------------------
/src/validation/data_source.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | const sourceSchema = z.enum(["mock", "sheets", "json", "csv", "airtable", "notion"]);
4 |
5 | const dataSchema = z.object({
6 | source: sourceSchema.default("mock"),
7 | mock: z.object({
8 | entryCount: z.number().default(10),
9 | }).optional(),
10 | sheets: z.object({
11 | key: z.string().min(1, "Key is required for sheets"),
12 | }).optional(),
13 | airtable: z.object({
14 | base: z.string().min(1, "Base ID is required for Airtable"),
15 | name: z.string().min(1, "Table Name is required for Airtable"),
16 | }).optional(),
17 | notion: z.object({
18 | databaseId: z.string().min(1, "Database ID is required for Notion"),
19 | }).optional(),
20 | }).superRefine((data, ctx) => {
21 | if (data.source === "sheets" && !data.sheets?.key) {
22 | ctx.addIssue({
23 | path: ["sheets", "key"],
24 | message: "Key is required when source is 'sheets'",
25 | code: z.ZodIssueCode.custom,
26 | });
27 | }
28 | if (data.source === "airtable" && (!data.airtable?.base || !data.airtable?.name)) {
29 | ctx.addIssue({
30 | path: ["airtable"],
31 | message: "Base ID and Table Name are required when source is 'airtable'",
32 | code: z.ZodIssueCode.custom,
33 | });
34 | }
35 | if (data.source === "notion" && !data.notion?.databaseId) {
36 | ctx.addIssue({
37 | path: ["notion"],
38 | message: "Database ID is required when source is 'notion'",
39 | code: z.ZodIssueCode.custom,
40 | });
41 | }
42 | });
43 |
44 | type DataSettingsSchema = z.infer;
45 |
46 | export { dataSchema, type DataSettingsSchema };
--------------------------------------------------------------------------------
/src/validation/directory.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const directorySchema = (imageSchema: z.ZodTypeAny) =>
4 | z.object({
5 | title: z.string().optional(),
6 | description: z.string().optional(),
7 | tags: z.array(z.string()).optional(),
8 | icon: z.string().optional(),
9 | image: imageSchema.optional(),
10 | link: z.string().url().optional(),
11 | featured: z.boolean().default(false),
12 | });
--------------------------------------------------------------------------------
/src/validation/settings.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | const layoutSchema = z.object({
4 | sidebar: z.boolean().default(false),
5 | emoji: z.boolean().default(false),
6 | })
7 |
8 | const generalSchema = z.object({
9 | title: z.string(),
10 | logo: z.string(),
11 | iconLogo: z.string(),
12 | seo: z.object({
13 | name: z.string(),
14 | description: z.string(),
15 | url: z.string().url(),
16 | }),
17 | });
18 |
19 | const headerSchema = z.object({
20 | banner: z.object({
21 | show: z.boolean(),
22 | text: z.string(),
23 | link: z.string().url(),
24 | brandText: z.string(),
25 | }),
26 | navbar: z.object({
27 | colorModeSelector: z.boolean().optional().default(false),
28 | links: z.array(
29 | z.object({
30 | name: z.string(),
31 | href: z.string(),
32 | target: z.string().optional(),
33 | })
34 | ),
35 | }),
36 | actionButton: z.object({
37 | text: z.string(),
38 | href: z.string().url(),
39 | }),
40 | });
41 |
42 | const footerSchema = z.object({
43 | description: z.string(),
44 | socials: z.object({
45 | github: z.object({ link: z.string() }).optional(),
46 | facebook: z.object({ link: z.string() }).optional(),
47 | instagram: z.object({ link: z.string() }).optional(),
48 | x: z.object({ link: z.string() }).optional(),
49 | youtube: z.object({ link: z.string() }).optional(),
50 | }),
51 | });
52 |
53 | const uiSchema = z.object({
54 | icons: z.object({
55 | dark: z.string(),
56 | light: z.string(),
57 | instagram: z.string(),
58 | youtube: z.string(),
59 | facebook: z.string(),
60 | x: z.string(),
61 | }),
62 | });
63 |
64 | const directoryData = z.object({
65 | source: z.object({
66 | name: z.string(),
67 | linksOutbound: z.boolean().default(false),
68 | sheets: z
69 | .object({
70 | key: z.string(),
71 | })
72 | .optional(),
73 | airtable: z.object({
74 | base: z.string(),
75 | name: z.string(),
76 | })
77 | .optional(),
78 | notion: z.object({
79 | databaseId: z.string(),
80 | })
81 | .optional(),
82 | }),
83 | tagPages: z.object({
84 | title: z.string(),
85 | }),
86 | search: z.object({
87 | placeholder: z.string(),
88 | }),
89 | tags: z.array(
90 | z.object({
91 | key: z.string(),
92 | name: z.string(),
93 | color: z.string().optional(),
94 | emoji: z.string().optional(),
95 | description: z.string().optional(),
96 | })
97 | ),
98 | });
99 |
100 | const directoryUI = z.object({
101 | grid: z.object({
102 | type: z.enum(["icon-list", "rectangle-card-grid", "small-card-grid"]),
103 | emptyState: z.object({
104 | text: z.string(),
105 | type: z.enum(["button", "simple", "link"]),
106 | icon: z.string(),
107 | }),
108 | card: z.object({
109 | image: z.boolean(),
110 | }),
111 | submit: z.object({
112 | show: z.boolean(),
113 | first: z.boolean(),
114 | title: z.string(),
115 | description: z.string(),
116 | hideable: z.boolean(),
117 | }),
118 | }),
119 | search: z.object({
120 | showCount: z.boolean(),
121 | icon: z.string(),
122 | tags: z.object({
123 | display: z.enum(["none", "select", "show-all"]),
124 | intersection: z.boolean(),
125 | }),
126 | }),
127 | featured: z.object({
128 | showOnAllPages: z.boolean(),
129 | showOnSide: z.boolean(),
130 | icon: z.string(),
131 | labelForCard: z.string(),
132 | }),
133 | });
134 |
135 | const listingsSchema = z.object({
136 | pageHeader: z.enum(["none", "title"]),
137 | });
138 |
139 | // todo separate directory ui config from directory data config.
140 | const themeSettingsSchema = z.object({
141 | theme: z.string(),
142 | general: generalSchema,
143 | header: headerSchema,
144 | directoryData: directoryData,
145 | footer: footerSchema,
146 | });
147 |
148 | const themeSchema = z.object({
149 | listings: listingsSchema,
150 | directoryUI: directoryUI,
151 | ui: uiSchema,
152 | layout: layoutSchema,
153 | });
154 |
155 | const settingsSchema = z.object({
156 | general: generalSchema,
157 | listings: listingsSchema,
158 | directoryData: directoryData,
159 | directoryUI: directoryUI,
160 | header: headerSchema,
161 | footer: footerSchema,
162 | ui: uiSchema,
163 | layout: layoutSchema,
164 | });
165 |
166 | type SettingsSchema = z.infer;
167 |
168 | export { settingsSchema, themeSettingsSchema, themeSchema, type SettingsSchema };
--------------------------------------------------------------------------------
/tailwind.config.mjs:
--------------------------------------------------------------------------------
1 | const colors = require('tailwindcss/colors')
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | export default {
5 | darkMode: "class",
6 | content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
7 | theme: {
8 | extend: {
9 | fontFamily: {
10 | sans: ['Gabarito'],
11 | },
12 | colors: {
13 | primary: colors.sky,
14 | gray: colors.zinc,
15 | }
16 | },
17 | },
18 | plugins: [
19 | require('@tailwindcss/typography'),
20 | ],
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "types": ["./src/types"],
6 | "baseUrl": ".",
7 | "paths": {
8 | "@*": ["./src/*"]
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------