boilerplate

This commit is contained in:
Brooke Christiansen 2025-02-03 19:16:00 -08:00
commit d20ac09a66
62 changed files with 7957 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

12
.prettierrc Normal file
View File

@ -0,0 +1,12 @@
{
"tabWidth": 2,
"semi": true,
"trailingComma": "all",
"plugins": [
"prettier-plugin-astro",
"prettier-plugin-tailwindcss",
"prettier-plugin-organize-imports",
"@ianvs/prettier-plugin-sort-imports",
"prettier-plugin-merge"
]
}

4
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

48
README.md Normal file
View File

@ -0,0 +1,48 @@
# Astro Starter Kit: Basics
```sh
npm create astro@latest -- --template basics
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
│ └── favicon.svg
├── src/
│ ├── layouts/
│ │ └── Layout.astro
│ └── pages/
│ └── index.astro
└── package.json
```
To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/).
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

16
astro.config.mjs Normal file
View File

@ -0,0 +1,16 @@
// @ts-check
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import tailwind from "@astrojs/tailwind";
// https://astro.build/config
export default defineConfig({
integrations: [
react(),
tailwind({
applyBaseStyles: false,
}),
],
});

21
components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.mjs",
"css": "src/styles/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

43
package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "astro-template-directory-website",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/react": "^4.1.1",
"@astrojs/tailwind": "^5.1.3",
"@nanostores/react": "^0.8.4",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.6",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"astro": "^5.0.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.468.0",
"nanostores": "^0.11.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^2.5.5",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.7.2",
"vaul": "^1.1.2"
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-merge": "^0.7.2",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-tailwindcss": "^0.6.9"
}
}

5763
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

9
public/favicon.svg Normal file
View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/preview.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

3
public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
User-agent: *
Allow: /
Sitemap: https://onlineslidemaker.com/sitemap.xml

9
public/sitemap.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://onlineslidemaker.com</loc>
<lastmod>2024-12-24</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
</urlset>

1
src/assets/astro.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

View File

@ -0,0 +1,51 @@
import dataWebsites from "@/data/websites.json";
import { cn } from "@/lib/utils";
import { filteredTags } from "@/store";
import { useStore } from "@nanostores/react";
import { X } from "lucide-react";
import { useMemo } from "react";
import { Button } from "./ui/button";
export default function ListTags() {
const selectedTags: string[] = useStore(filteredTags);
const tags = useMemo(() => {
const tags = new Set<string>();
dataWebsites.forEach((website) => {
website.tags.forEach((tag) => tags.add(tag));
});
return Array.from(tags).sort((a, b) => a.localeCompare(b));
}, []);
return (
<div
className={cn(
"container mx-auto p-4 md:px-0 md:py-8",
"flex flex-wrap items-center justify-center gap-1",
)}
>
{tags.map((tag) => {
const selected = selectedTags.includes(tag);
return (
<Button
key={tag}
size="sm"
variant={selected ? "default" : "outline"}
onClick={() =>
filteredTags.set(
selected
? selectedTags.filter((e) => e !== tag)
: [...selectedTags, tag],
)
}
className={cn(
"flex cursor-pointer items-center gap-2 transition-all",
)}
>
{tag} {selected && <X size={12} />}
</Button>
);
})}
</div>
);
}

View File

@ -0,0 +1,78 @@
import { Badge } from "@/components/ui/badge";
import dataWebsites from "@/data/websites.json";
import { cn } from "@/lib/utils";
import { filteredTags, searchKeyword } from "@/store";
import { useStore } from "@nanostores/react";
import { useMemo } from "react";
export default function ListWebsites() {
const search = useStore(searchKeyword);
const tags = useStore(filteredTags);
const filteredWebsites = useMemo(() => {
if (!search && tags.length === 0) return dataWebsites;
return dataWebsites.filter((website) => {
if (
tags.length > 0 &&
!tags.every((tag) => (website.tags as string[]).includes(tag))
) {
return false;
}
if (!website.title.toLowerCase().includes(search)) return false;
return true;
});
}, [search, tags]);
return (
<div
className={cn(
"container mx-auto px-4 md:px-0",
"grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-3 xl:grid-cols-4",
)}
>
{filteredWebsites.map((website) => (
<a
key={website.url}
className={cn(
"rounded bg-background p-4 shadow",
"flex flex-col gap-4",
)}
href={website.url}
target="_blank"
>
<div className="flex gap-2">
<div className="h-12 w-12 bg-muted p-2">
<img
src={
website.favicon ||
"https://placehold.co/400?text=No%20Picture"
}
alt={website.title}
className="aspect-square w-full rounded object-cover"
/>
</div>
<p className="flex-1 text-sm font-semibold">{website.title}</p>
</div>
<div className="flex flex-1 flex-col justify-between gap-2">
<div className="flex flex-col gap-1">
<p className="line-clamp-3 text-xs text-muted-foreground">
{website.description}
</p>
<div className="flex flex-wrap gap-1">
{website.tags.map((tag) => (
<Badge className="px-1 py-0">{tag}</Badge>
))}
</div>
</div>
<div>
<span className="text-xs text-muted-foreground">
Last reviewed at{" "}
<span className="font-medium">{website.lastReviewAt}</span>
</span>
</div>
</div>
</a>
))}
</div>
);
}

View File

@ -0,0 +1,86 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
export default function ModalSubmitNew({
children,
}: {
children: React.ReactNode;
}) {
const title = "Submit a new website";
const description = "Request us to list your website on this website";
return (
<>
<Drawer>
<DrawerTrigger className="block md:hidden">{children}</DrawerTrigger>
<DrawerContent className="mx-2 max-h-[90%] min-h-[70%] px-4">
<DrawerHeader className="px-0 text-left">
<DrawerTitle>{title}</DrawerTitle>
<DrawerDescription>{description}</DrawerDescription>
</DrawerHeader>
<div>
<Label htmlFor="inputUrl">Website URL</Label>
<Input id="inputUrl" name="url" placeholder="https://example.com" />
</div>
<div>
<Label htmlFor="comment">Comment</Label>
<Textarea id="comment" placeholder="Leave some notes here ..." />
</div>
<DrawerFooter className="my-2 space-y-2 px-0">
<DrawerClose asChild>
<Button size="lg" variant="secondary">
Cancel
</Button>
</DrawerClose>
<Button size="lg">Submit</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
<Dialog>
<DialogTrigger className="hidden md:block">{children}</DialogTrigger>
<DialogContent>
<DialogHeader className="text-left">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<div>
<Label htmlFor="inputUrl">Website URL</Label>
<Input id="inputUrl" name="url" placeholder="https://example.com" />
</div>
<div>
<Label htmlFor="comment">Comment</Label>
<Textarea id="comment" placeholder="Leave some notes here ..." />
</div>
<DialogFooter className="flex items-center pt-1">
<DialogClose asChild>
<Button variant="secondary">Cancel</Button>
</DialogClose>
<Button>Submit</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View File

@ -0,0 +1,55 @@
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Moon, Sun } from "lucide-react";
import * as React from "react";
export function ModeToggle() {
const [theme, setThemeState] = React.useState<
"theme-light" | "dark" | "system"
>("theme-light");
React.useEffect(() => {
const isDarkMode = document.documentElement.classList.contains("dark");
setThemeState(isDarkMode ? "dark" : "theme-light");
}, []);
React.useEffect(() => {
const isDark =
theme === "dark" ||
(theme === "system" &&
window.matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.classList[isDark ? "add" : "remove"]("dark");
}, [theme]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="rounded-full border-none focus-visible:ring-0"
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setThemeState("theme-light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setThemeState("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setThemeState("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

135
src/components/Navbar.tsx Normal file
View File

@ -0,0 +1,135 @@
import { ModeToggle } from "@/components/ModeToggle";
import { Button, buttonVariants } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { CirclePlus, Home } from "lucide-react";
import ModalSubmitNew from "./ModalSubmitNew";
function NavIcon({
icon,
tooltip,
href,
target,
onClick,
}: {
icon: React.ReactNode;
tooltip: string;
href?: string;
target?: string;
onClick?: () => void;
}) {
return (
<Tooltip>
<TooltipTrigger asChild>
<a
href={href}
target={target}
onClick={onClick}
className={cn(
buttonVariants({
variant: "ghost",
size: "icon",
}),
"rounded-full text-foreground transition-all group-hover:scale-110",
)}
>
{icon}
</a>
</TooltipTrigger>
<TooltipContent>
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
);
}
export default function Navbar() {
return (
<TooltipProvider>
<div
className={cn(
"group pointer-events-none mb-4 flex h-full max-h-14",
"fixed inset-x-0 bottom-4 z-20 mx-auto md:top-4",
)}
>
<div className="fixed inset-x-0 bottom-0 h-16 w-full bg-background to-transparent backdrop-blur-lg [-webkit-mask-image:linear-gradient(to_top,black,transparent)] dark:bg-background md:top-0 md:[-webkit-mask-image:linear-gradient(to_bottom,black,transparent)]"></div>
<div
className={cn(
"relative mx-auto flex h-full min-h-full items-center gap-2 rounded-full px-2",
"pointer-events-auto transition-all",
"bg-background [box-shadow:0_0_0_1px_rgba(0,0,0,.03),0_2px_4px_rgba(0,0,0,.05),0_12px_24px_rgba(0,0,0,.05)] dark:[border:1px_solid_rgba(255,255,255,.1)] dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset]",
)}
>
<NavIcon icon={<Home size={24} />} tooltip="Home" href="/" />
<NavIcon
icon={
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<title>GitHub</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
}
tooltip="Source code of this website theme"
href="https://github.com/lukenguyen-me/magicui"
target="_blank"
/>
<NavIcon
icon={
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<title>Astro</title>
<path d="M8.358 20.162c-1.186-1.07-1.532-3.316-1.038-4.944.856 1.026 2.043 1.352 3.272 1.535 1.897.283 3.76.177 5.522-.678.202-.098.388-.229.608-.36.166.473.209.95.151 1.437-.14 1.185-.738 2.1-1.688 2.794-.38.277-.782.525-1.175.787-1.205.804-1.531 1.747-1.078 3.119l.044.148a3.158 3.158 0 0 1-1.407-1.188 3.31 3.31 0 0 1-.544-1.815c-.004-.32-.004-.642-.048-.958-.106-.769-.472-1.113-1.161-1.133-.707-.02-1.267.411-1.415 1.09-.012.053-.028.104-.045.165h.002zm-5.961-4.445s3.24-1.575 6.49-1.575l2.451-7.565c.092-.366.36-.614.662-.614.302 0 .57.248.662.614l2.45 7.565c3.85 0 6.491 1.575 6.491 1.575L16.088.727C15.93.285 15.663 0 15.303 0H8.697c-.36 0-.615.285-.784.727l-5.516 14.99z" />
</svg>
}
tooltip="This template on Astro theme"
href="https://astro.build"
target="_blank"
/>
<Separator orientation="vertical" className="my-2 h-full" />
<ModalSubmitNew>
<NavIcon icon={<CirclePlus />} tooltip="Submit a new website" />
</ModalSubmitNew>
<NavIcon
icon={
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<title>Buy Me A Coffee</title>
<path d="M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.067 2.017.144l.228.031c.267.04.533.088.798.145.392.085.895.113 1.07.542.055.137.08.288.111.431l.319 1.484a.237.237 0 01-.199.284h-.003c-.037.006-.075.01-.112.015a36.704 36.704 0 01-4.743.295 37.059 37.059 0 01-4.699-.304c-.14-.017-.293-.042-.417-.06-.326-.048-.649-.108-.973-.161-.393-.065-.768-.032-1.123.161-.29.16-.527.404-.675.701-.154.316-.199.66-.267 1-.069.34-.176.707-.135 1.056.087.753.613 1.365 1.37 1.502a39.69 39.69 0 0011.343.376.483.483 0 01.535.53l-.071.697-1.018 9.907c-.041.41-.047.832-.125 1.237-.122.637-.553 1.028-1.182 1.171-.577.131-1.165.2-1.756.205-.656.004-1.31-.025-1.966-.022-.699.004-1.556-.06-2.095-.58-.475-.458-.54-1.174-.605-1.793l-.731-7.013-.322-3.094c-.037-.351-.286-.695-.678-.678-.336.015-.718.3-.678.679l.228 2.185.949 9.112c.147 1.344 1.174 2.068 2.446 2.272.742.12 1.503.144 2.257.156.966.016 1.942.053 2.892-.122 1.408-.258 2.465-1.198 2.616-2.657.34-3.332.683-6.663 1.024-9.995l.215-2.087a.484.484 0 01.39-.426c.402-.078.787-.212 1.074-.518.455-.488.546-1.124.385-1.766zm-1.478.772c-.145.137-.363.201-.578.233-2.416.359-4.866.54-7.308.46-1.748-.06-3.477-.254-5.207-.498-.17-.024-.353-.055-.47-.18-.22-.236-.111-.71-.054-.995.052-.26.152-.609.463-.646.484-.057 1.046.148 1.526.22.577.088 1.156.159 1.737.212 2.48.226 5.002.19 7.472-.14.45-.06.899-.13 1.345-.21.399-.072.84-.206 1.08.206.166.281.188.657.162.974a.544.544 0 01-.169.364zm-6.159 3.9c-.862.37-1.84.788-3.109.788a5.884 5.884 0 01-1.569-.217l.877 9.004c.065.78.717 1.38 1.5 1.38 0 0 1.243.065 1.658.065.447 0 1.786-.065 1.786-.065.783 0 1.434-.6 1.499-1.38l.94-9.95a3.996 3.996 0 00-1.322-.238c-.826 0-1.491.284-2.26.613z" />
</svg>
}
tooltip="Buy me a coffee"
href="https://buymeacoffee.com/lukenguyen_me"
target="_blank"
/>
<Separator orientation="vertical" className="h-full py-2" />
<Tooltip>
<TooltipTrigger asChild>
<ModeToggle />
</TooltipTrigger>
<TooltipContent>
<p>Theme</p>
</TooltipContent>
</Tooltip>
</div>
</div>
</TooltipProvider>
);
}

77
src/components/Search.tsx Normal file
View File

@ -0,0 +1,77 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { searchKeyword } from "@/store";
import { Search as SearchIcon, XCircle } from "lucide-react";
import { useEffect, useState } from "react";
export default function Search({ className }: { className?: string }) {
const [search, setSearch] = useState("");
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const isDark = document.documentElement.classList.contains("dark");
setIsDark(isDark);
}, []);
const onClickSearch = () => {
onSearch();
};
const onClear = () => {
if (search !== "") setSearch("");
searchKeyword.set("");
};
const onSearch = () => {
searchKeyword.set(search);
};
return (
<div
className={cn(
"flex w-full items-center justify-center gap-2 md:gap-4",
className,
)}
>
<div className={cn("relative flex w-full")}>
<Input
placeholder="Enter something..."
value={search}
className="h-12 w-full bg-background pl-12 text-sm shadow-lg md:h-16 md:text-lg"
onChange={(e) => setSearch(e.target.value)}
onKeyUp={(event) => {
if (event.key === "Enter" || event.keyCode === 13) {
onSearch();
}
}}
/>
<a
onClick={onClear}
className={cn(
"absolute right-4 top-1 z-10 translate-y-[50%] md:top-2",
"cursor-pointer text-muted-foreground opacity-0 transition-all",
search !== "" && "opacity-100",
)}
>
<XCircle className="size-5 md:size-6" />
</a>
<SearchIcon
className={cn(
"absolute left-4 top-1 translate-y-[50%] md:top-2",
"size-5 cursor-pointer text-muted-foreground/20 md:size-6",
)}
/>
</div>
<Button
variant={isDark ? "secondary" : "default"}
className={cn(
"h-12 w-32 text-sm font-light shadow-lg md:h-16 md:text-lg",
)}
onClick={() => onClickSearch()}
>
Search
</Button>
</div>
);
}

View File

@ -0,0 +1,23 @@
import { cn } from "@/lib/utils";
export default function Spinner({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={cn(
"lucide lucide-loader-circle h-5 w-5 animate-spin",
className,
)}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
);
}

View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,120 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -0,0 +1,116 @@
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

View File

@ -0,0 +1,199 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,24 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -0,0 +1,29 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

View File

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }

View File

@ -0,0 +1,30 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

481
src/data/websites.json Normal file
View File

@ -0,0 +1,481 @@
[
{
"description": "Create online presentations for any event. Effortlessly design, customize, and share visually appealing slides to captivate your audience.",
"favicon": "https://static.figma.com/app/icon/1/favicon.ico",
"image": "https://cdn.sanity.io/images/599r6htc/regionalized/b07a7378e4af476668b732a6e70deec32851c5dc-2400x1260.png?w=1200&q=70&fit=max&auto=format",
"title": "Free Online Presentation Maker & Creator | Figma",
"url": "https://www.figma.com/presentation-maker/",
"tags": [
"most popular",
"general",
"design",
"freemium",
"collaborative"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "The only presentation software you'll ever need. Easily create beautiful slides online with free custom presentation templates and a massive media library.",
"favicon": "https://static.canva.com/static/images/android-192x192-2.png",
"image": "https://content-management-files.canva.com/9014ee8f-b284-4ce3-a031-a642d72f0a28/ogimage_presentations.jpg",
"title": "Presentations and slides for any occasion | Canva",
"url": "https://www.canva.com/presentations/",
"tags": [
"most popular",
"general",
"design",
"freemium",
"collaborative"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Simply focus on your content — Pithy Point takes care of the rest.",
"favicon": "https://pithypoint.com/favicon.ico",
"image": "https://pithypoint.com/featured_banner.png",
"title": "Pithy Point - Just Your Data, Instantly Presented",
"url": "https://pithypoint.com",
"tags": [
"niche",
"content",
"freemium"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Welcome to Prezi, the presentation software that uses motion, zoom, and spatial relationships to bring your ideas to life and make you a great presenter.",
"favicon": "https://assets.prezicdn.net/assets-versioned/coverservice-versioned/4557-fdf0f36/common/img/favicon.ico?v=2",
"image": "https://assets.prezicdn.net/assets-versioned/coverservice-versioned/4557-fdf0f36/coverservice/webflow/images/Hero-06-2x.png",
"title": "Presentations and videos with engaging visuals for hybrid teams | Prezi Present",
"url": "https://prezi.com/",
"tags": [
"most popular",
"design",
"niche",
"free trial"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Create stunning presentations with Google Slides. Discover slide templates for every use case, use AI to generate unique visualizations, and more.",
"favicon": "https://workspace.google.com/static/favicon.ico?cache=4926369",
"image": "https://lh3.googleusercontent.com/uY22fbFpWwOsMxx41dBoZy4BgFPSdkh4LNH0vRzd2c-qDzYiD4Tflc2DXEOk-BFtk7_fBFAs2xVrqXoFY2Ll0ba5Wh-zQ0rWkf5ZOg=w1600-rj-e365",
"title": "Google Slides: Presentation Slideshow Maker | Google Workspace",
"url": "https://workspace.google.com/products/slides/",
"tags": [
"most popular",
"general",
"design",
"freemium",
"collaborative"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Discover the best Google Slides themes and PowerPoint templates you can use in your presentations - 100% Free for any use.",
"favicon": "https://slidesgo.com/favicons/favicon.ico",
"image": "https://slidesgo.com/og-image.jpg",
"title": "Free Google Slides themes and Powerpoint templates | Slidesgo",
"url": "https://slidesgo.com/",
"tags": [
"general",
"design",
"freemium"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Create free presentations from professionally designed templates or from scratch. Adobe Express makes it fun and easy to customize.",
"favicon": "https://www.adobe.com/express/icons/cc-express.svg",
"image": "https://www.adobe.com/express/media_1e794b77d86a3cc74eea0c8a53c0efa4ce7102411.jpeg#width=1200&height=630",
"title": "Free Presentation Maker: Design Presentations Online | Adobe Express",
"url": "https://www.adobe.com/express/create/presentation",
"tags": [
"most popular",
"general",
"design",
"freemium",
"collaborative"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Smallppt leads the AI-powered presentation, dedeicated to quality content, efficiency, smart, visually appealing design, customization, and multilingual.",
"favicon": "https://smallppt.com/favicon.ico",
"image": "",
"title": "Smallppt: Quickly Generate Professional AI Presentations",
"url": "https://smallppt.com/",
"tags": [
"ai",
"general",
"design",
"freemium"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Create professional presentations, interactive infographics, beautiful design and engaging videos, all in one place. Start using Visme today.",
"favicon": "https://www.visme.co/android-chrome-192x192.png",
"image": "https://www.visme.co/wp-content/uploads/2020/02/Visme-Content-Maker.jpg",
"title": "Visme: AI Presentation maker, Infographics, and One pager templates",
"url": "https://www.visme.co/",
"tags": [
"general",
"design",
"freemium"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Interact with your audience using real-time voting. No installations or downloads required - and it's free!",
"favicon": "https://www.mentimeter.com/_next/static/media/android-chrome-192x192.170a12e6.png",
"image": "https://static.mentimeter.com/static/images/mentimeter-og.png",
"title": "Interactive presentation software",
"url": "https://www.mentimeter.com/",
"tags": [
"general",
"design",
"niche",
"interactive",
"freemium"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Build an impressive presentation with our AI presentation maker, Magic Design for Presentations. Simply type a prompt and discover inspiring ideas.",
"favicon": "https://slidesgo.ai/favicon.ico",
"image": null,
"title": "slidesgo.ai: Create presentations with AI, generate slides in seconds.",
"url": "https://slidesgo.ai",
"tags": [
"ai",
"general",
"design",
"freemium"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Use AI to transform ideas into captivating presentations in seconds. Ideal for businesses, educators, and personal projects. Start now - it's free!",
"favicon": "https://cdn.prod.website-files.com/63ca9a05fdc83042565f605c/63fedbd8e101652ba74f55c5_p_ai_32.png",
"image": "https://cdn.presentations.ai/images/image.png",
"title": "Presentations.AI - ChatGPT for Presentations",
"url": "https://presentations.ai/",
"tags": [
"ai",
"general",
"design",
"free trial"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Pitch is the complete pitching platform that enables any team to quickly create sleek presentations that get results. Sign up for free. ",
"favicon": "https://pitch.com/favicon.png",
"image": "https://res.cloudinary.com/pitch-software/image/upload/f_auto/website/social-image-png.jpg",
"title": "Presentation software for fast-moving teams | Pitch",
"url": "https://pitch.com/",
"tags": [
"niche",
"design",
"pitch focus",
"freemium"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Make videos in minutes with Powtoon. Use our library of styles, characters, backgrounds and video, or upload your own! Free. Easy. Awesome. Sign up today!",
"favicon": "https://static.powtoon.co/images/favicon.ico",
"image": "https://static.powtoon.co/images/powtoon_thumb.jpg",
"title": "Video Maker | Make Videos and Animations Online | Powtoon",
"url": "https://powtoon.com/",
"tags": [
"general",
"design",
"paid only"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Make custom slideshow for your webpage in minutes. Create and embed your html slider with unique look & feel that just perfect fits with your web page.",
"favicon": "https://comslider.com/images/favicon.ico",
"image": "https://www.comslider.com/images/comslider_logo_128.png",
"title": "comSlider : Online Slideshow Creator",
"url": "https://comslider.com/",
"tags": [
"general",
"design",
"paid only"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Create stunning presentations using simple text prompts with SlidesAI. Free, customizable AI PPT & slides generator for students, educators & businesses. Try now!",
"favicon": "https://www.slidesai.io/favicon.ico",
"image": "https://cdn-slidesai.s3.ap-south-1.amazonaws.com/wp-content/uploads/2023/05/10221632/og-image.png",
"title": "SlidesAI: Free AI Presentation Maker | Generate Slides with AI",
"url": "https://slidesai.io/",
"tags": [
"ai",
"general",
"design",
"freemium"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "MagicSlides.app is AI-Powered Text To Presentation app that creates presentation slides from any piece of text.",
"favicon": "https://magicslides.app/assets/magicslides_logo_circle.svg",
"image": "https://www.magicslides.app/assets/magicslides-open-graph-image1.png",
"title": "AI Presentation Maker - Presentation from Topic, YouTube, PDF, URL with AI",
"url": "https://magicslides.app/",
"tags": [
"ai",
"general",
"design",
"paid only"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Create professional animated videos for your marketing campaigns in minutes. Make explainer videos, product presentation videos, promo videos and more.",
"favicon": "https://wideoblog.wpenginepowered.com/wp-content/uploads/2017/09/favicon.png",
"image": "https://wideostaging.wpenginepowered.com/wp-content/uploads/2017/12/logo.png",
"title": "Create Professional Animated Videos and Presentations - Wideo",
"url": "https://wideo.co/",
"tags": [
"general",
"design",
"freemium",
"free trial"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": null,
"favicon": "https://www.slidemake.com/logo.svg",
"image": null,
"title": "SlideMake - AI Slideshow Maker and Generator | Create and Download Powerpoint Presentations for Free",
"url": "https://www.slidemake.com/",
"tags": [
"ai",
"general",
"design",
"paid only"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Make a slideshow presentation in minutes with PicMonkey's slideshow presentation maker tools. We'll show you how to use our graphics, templates, and more to make winning designs.",
"favicon": "https://cdn-fastly.picmonkey.com/core/picmonkey_128.png",
"image": null,
"title": "Slideshow Maker | Create Your Own Slideshow Online | PicMonkey",
"url": "https://www.picmonkey.com/design/slideshow-maker",
"tags": [
"general",
"design",
"free trial"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "A lightweight, feature-rich online office suite that helps you create impressive, impactful presentations and demos.",
"favicon": "https://online.visual-paradigm.com/favicon-32x32.png",
"image": null,
"title": "Online Presentation Maker",
"url": "https://online.visual-paradigm.com/presentation-software/",
"tags": [
"general",
"design",
"freemium"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Made with Gamma. A new medium for presenting ideas, powered by AI.",
"favicon": "https://gamma.app/favicons/favicon-192.svg",
"image": "https://cdn.gamma.app/_app_static/images/og-image-d666e6.jpg",
"title": "Presentations and Slide Decks with AI | Gamma",
"url": "https://gamma.app/",
"tags": [
"general",
"design",
"freemium"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Transform your presentations by editing fully customizable slides from professionally designed premade templates. Sign up now with Piktochart for free.",
"favicon": "https://piktochart.com/wp-content/themes/piktochart/img/favicon.png",
"image": "https://piktochart.com/wp-content/uploads/2023/11/online-presentation-maker.png",
"title": "Make Impactful Slides with a Free Presentation Maker | Piktochart",
"url": "https://piktochart.com/presentation-maker/",
"tags": [
"ai",
"general",
"design",
"freemium"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "The best AI tool for generating powerpoint slides. Create your own presentations with AI in 3 minutes. Bestppt provides a large number of beautiful PPT templates to choose from. At the same time, Bestppt provides AI chatbot and ai image functions.",
"favicon": "https://bestppt.ai/favicon.ico",
"image": "https://bestppt.ai/favicon.ico",
"title": "AI Presentation - Al Generate Presentations -Al Chatbot | Bestppt.ai",
"url": "https://bestppt.ai/",
"tags": [
"ai",
"general",
"design",
"paid only"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Beautiful.ai is the best AI-powered presentation software for teams. Stay on brand, level up and automate presentation design, and collaborate from anywhere.",
"favicon": "https://cdn.prod.website-files.com/59deb588800ae30001ec19c9/5d4891e0e260e3c1bc37b100_beautiful%20ai%20favicon%20%20blue%20square.png",
"image": "https://cdn.prod.website-files.com/59deb588800ae30001ec19c9/63d98cc57279ba775caae473_B%20icon_black%20circle.png",
"title": "AI Presentation Maker | Make it Beautiful with Beautiful.ai",
"url": "https://www.beautiful.ai/",
"tags": [
"ai",
"general",
"design",
"free trial"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Make stunning presentations in minutes with Venngage Presentation Maker. Easy-to-use, 100+ professional templates, Powerpoint export, completely free to try!",
"favicon": "https://cdn.venngage.com/assets/img/favicon.png",
"image": "https://cdn.venngage.com/landing/homepage-landing/og-image-homepage.png",
"title": "Online Presentation Maker | Design Professional & Engaging Presentations - Venngage",
"url": "https://venngage.com/features/presentation-maker",
"tags": [
"general",
"design",
"freemium"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Experience the ease of creating professional presentations with PoweredTemplate's AI Presentation Maker. Customize your slides effortlessly and download them for free. Perfect for business meetings, educational purposes, and creative projects. Try our AI-powered tool today!",
"favicon": "https://i.poweredtemplates.com/assets/favicons/favicon.ico",
"image": "https://ai.poweredtemplate.com/assets/images/ai/snippet-big.jpg",
"title": "Free AI Presentation Maker - Create Stunning Presentations Online | PoweredTemplate",
"url": "https://ai.poweredtemplate.com/",
"tags": [
"ai",
"general",
"design",
"freemium"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Miro's free presentation maker helps you create powerful and engaging presentations. Get started with a range of ready-made presentation slides.",
"favicon": "https://static-website.miro.com/miro-site-lp-build-assets/assets/favicon.ico",
"image": "https://static-website.miro.com/miro-site-lp-build-assets/assets/images/miro.png",
"title": "Free Presentation Maker for Every Team | Miro",
"url": "https://miro.com/presentations/",
"tags": [
"most popular",
"general",
"interactive",
"freemium"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "SlidesGPT is an AI PowerPoint Maker, also known as AI PPT Maker or AI presentation maker, that also generates Google slides and PDFs using ChatGPT API",
"favicon": "https://slidesgpt.com/classic/assets/img/favicon.png",
"image": "https://slidesgpt.com/assets/img/social-min.png",
"title": "SlidesGPT: AI PowerPoint Maker & AI PPT Maker using ChatGPT API",
"url": "https://slidesgpt.com/",
"tags": [
"ai",
"general",
"design",
"freemium"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Try out our AI Presentations Maker for free. Generate presentations in minutes and customize them with our editor and download the result. Start now!",
"favicon": "https://content.wepik.com/media/images/favicons/favicon-32x32.png?v=2",
"image": "https://content.wepik.com/media/content-landing/ogimages/features/ogimage-ai-presentation.jpg",
"title": "AI Presentation Maker Generate Presentations Free | Wepik",
"url": "https://wepik.com/ai-presentations",
"tags": [],
"lastReviewAt": "December 20, 2024"
},
{
"description": "With the use of the most advanced AI-based technology platform in the world, explore the presentation design world of the future. Create professional PowerPoint presentations like pitch decks etc to streamline your workflow. It is ideal for company owners and entrepreneurs looking to wow their investors. Boost AI's potential to enhance your presentation skills. Try this Free Online Presentation Generator tool now.",
"favicon": "https://www.slideteam.net/media/favicon/default/favicon.ico",
"image": null,
"title": "Free online AI Presentation Maker | Generate Presentations in Seconds",
"url": "https://www.slideteam.net/Free-Online-AI-Presentation-Maker",
"tags": [
"ai",
"general",
"design",
"paid only"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Decktopus is an AI presentation maker, that will create amazing presentations in seconds. You only need to type the presentation title and your presentation is ready.",
"favicon": "https://cdn.prod.website-files.com/622217130a9cad1a33ea9b0a/6225e7f6ebda710d65459b7c_decktopus_symbol_2.png",
"image": "https://cdn.prod.website-files.com/622217130a9cad1a33ea9b0a/63fdc4d8ff46f93c1fbc0985_p3.jpg",
"title": "Decktopus AI",
"url": "https://www.decktopus.com/",
"tags": [
"ai",
"general",
"design",
"paid only"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Zoho Show is a free online presentation software that lets you create, collaborate, publish, and broadcast presentations from any device, quick and easy.",
"favicon": "https://www.zohowebstatic.com/sites/zweb/images/favicon.ico",
"image": "https://www.zohowebstatic.com/sites/zweb/images/ogimage/show-logo.png",
"title": "Online Presentation Software | Create & Edit Slides Online - Zoho Show",
"url": "https://www.zoho.com/show/",
"tags": [
"general",
"design",
"freemium",
"collaborative"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Create and make your presentations stand out with Picmaker's free online presentation maker. Browse our range of presentation templates and slides for free.",
"favicon": "https://www.picmaker.com/assets/images/picmakerhome/pic-fav.svg",
"image": "https://www.picmaker.com/assets/images/presentationmaker/presentation-maker-og.png",
"title": " Free Online Presentation Maker | 99+ Templates | Slides - Picmaker ",
"url": "https://www.picmaker.com/presentation-maker",
"tags": [
"general",
"design",
"paid only"
],
"lastReviewAt": "December 20, 2024"
},
{
"description": "Craft captivating presentations with ease using our powerful online presentation maker. Design, enhance, and deliver impactful slides that engage your audience.",
"favicon": "https://www.marq.com/wp-content/uploads/fbrfg/favicon-32x32.png",
"image": "https://www.marq.com/wp-content/uploads/2023/08/Marq_Website_Feature-01.png",
"title": "Free Presentation Maker Online & Slide Design | Marq",
"url": "https://www.marq.com/create/tools/free-presentation-maker-online",
"tags": [
"general",
"design",
"freemium"
],
"lastReviewAt": "December 20, 2024"
}
]

78
src/layouts/Layout.astro Normal file
View File

@ -0,0 +1,78 @@
---
import { cn } from "@/lib/utils";
const title =
"Placeholder title of your website";
const description =
"Placeholder description of your website";
const url = "https://placeholder.com";
const image = "/public/preview.webp";
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="index, follow" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>
{title}
</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:url" content={url} />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={image} />
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "WebSite",
"url": "https://placeholder.com",
"name": "Placeholder title of your website",
"description": "Placeholder title of your website"
}
</script>
<!-- Script for light/dark mode -->
<script is:inline>
const getThemePreference = () => {
if (
typeof localStorage !== "undefined" &&
localStorage.getItem("theme")
) {
return localStorage.getItem("theme");
}
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
};
const isDark = getThemePreference() === "dark";
document.documentElement.classList[isDark ? "add" : "remove"]("dark");
if (typeof localStorage !== "undefined") {
const observer = new MutationObserver(() => {
const isDark = document.documentElement.classList.contains("dark");
localStorage.setItem("theme", isDark ? "dark" : "light");
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
}
</script>
</head>
<body class={cn("bg-background font-sans text-primary")}>
<slot />
</body>
</html>

6
src/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

112
src/pages/index.astro Normal file
View File

@ -0,0 +1,112 @@
---
import Search from "@/components/Search";
import Navbar from "@/components/Navbar";
import Layout from "@/layouts/Layout.astro";
import { Button } from "@/components/ui/button";
import ListWebsites from "@/components/ListWebsites";
import ListTags from "@/components/ListTags";
import "@/styles/globals.css";
import { cn } from "@/lib/utils";
import { Globe } from "lucide-react";
---
<Layout>
<main class="flex min-h-screen flex-col">
<Navbar client:load />
<section
class="container z-10 mx-auto -mb-32 flex max-w-6xl flex-col px-4 py-16 md:px-8"
>
<div class="flex flex-col items-end gap-8 md:flex-row md:gap-16">
<div class="col-span-2 flex flex-1 flex-col">
<h1 class="mb-6 text-4xl leading-tight">
<strong>Up to date</strong><br />
<span class="font-light">Online Slide Maker Compilation</span>
</h1>
<Search client:load className="w-full" />
</div>
<div
class="flex aspect-video w-96 max-w-full items-center justify-center rounded border border-dashed bg-background shadow"
>
<span class="font-light text-muted-foreground"
>Featured banner goes here</span
>
</div>
</div>
</section>
<section
class="to-red h-32 bg-gradient-to-b from-white to-muted dark:from-black dark:to-muted"
>
</section>
<section class={cn("flex-1 bg-muted")}>
<ListTags client:load />
</section>
<section class={cn("flex-1 bg-muted pb-4 md:pb-12")}>
<ListWebsites client:load />
</section>
<footer class="pb-32 pt-4 md:pb-8 md:pt-8">
<div class="container mx-auto px-4 text-sm md:px-0">
<div
class="flex flex-col-reverse items-center justify-between gap-2 md:flex-row"
>
<p class="text-muted-foreground">
Brought to you by <a
class="underline"
href="https://lukenguyen.me"
target="_blank">@lukenguyen.me</a
>
</p>
<div class="flex items-center gap-1">
<a href="https://lukenguyen.me" target="_blank">
<Button variant="ghost" size="icon"><Globe /></Button>
</a>
<a href="https://github.com/lukenguyen-me" target="_blank">
<Button variant="ghost" size="icon">
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<title>GitHub</title>
<path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
></path>
</svg>
</Button>
</a>
<a href="https://x.com/lukenguyen_me" target="_blank">
<Button variant="ghost" size="icon">
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<title>X</title>
<path
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
></path>
</svg>
</Button>
</a>
<a href="https://buymeacoffee.com/lukenguyen_me" target="_blank">
<Button variant="ghost" size="icon">
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<title>Buy Me A Coffee</title>
<path
d="M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.067 2.017.144l.228.031c.267.04.533.088.798.145.392.085.895.113 1.07.542.055.137.08.288.111.431l.319 1.484a.237.237 0 01-.199.284h-.003c-.037.006-.075.01-.112.015a36.704 36.704 0 01-4.743.295 37.059 37.059 0 01-4.699-.304c-.14-.017-.293-.042-.417-.06-.326-.048-.649-.108-.973-.161-.393-.065-.768-.032-1.123.161-.29.16-.527.404-.675.701-.154.316-.199.66-.267 1-.069.34-.176.707-.135 1.056.087.753.613 1.365 1.37 1.502a39.69 39.69 0 0011.343.376.483.483 0 01.535.53l-.071.697-1.018 9.907c-.041.41-.047.832-.125 1.237-.122.637-.553 1.028-1.182 1.171-.577.131-1.165.2-1.756.205-.656.004-1.31-.025-1.966-.022-.699.004-1.556-.06-2.095-.58-.475-.458-.54-1.174-.605-1.793l-.731-7.013-.322-3.094c-.037-.351-.286-.695-.678-.678-.336.015-.718.3-.678.679l.228 2.185.949 9.112c.147 1.344 1.174 2.068 2.446 2.272.742.12 1.503.144 2.257.156.966.016 1.942.053 2.892-.122 1.408-.258 2.465-1.198 2.616-2.657.34-3.332.683-6.663 1.024-9.995l.215-2.087a.484.484 0 01.39-.426c.402-.078.787-.212 1.074-.518.455-.488.546-1.124.385-1.766zm-1.478.772c-.145.137-.363.201-.578.233-2.416.359-4.866.54-7.308.46-1.748-.06-3.477-.254-5.207-.498-.17-.024-.353-.055-.47-.18-.22-.236-.111-.71-.054-.995.052-.26.152-.609.463-.646.484-.057 1.046.148 1.526.22.577.088 1.156.159 1.737.212 2.48.226 5.002.19 7.472-.14.45-.06.899-.13 1.345-.21.399-.072.84-.206 1.08.206.166.281.188.657.162.974a.544.544 0 01-.169.364zm-6.159 3.9c-.862.37-1.84.788-3.109.788a5.884 5.884 0 01-1.569-.217l.877 9.004c.065.78.717 1.38 1.5 1.38 0 0 1.243.065 1.658.065.447 0 1.786-.065 1.786-.065.783 0 1.434-.6 1.499-1.38l.94-9.95a3.996 3.996 0 00-1.322-.238c-.826 0-1.491.284-2.26.613z"
></path>
</svg>
</Button>
</a>
</div>
</div>
</div>
</footer>
</main></Layout
>

4
src/store.ts Normal file
View File

@ -0,0 +1,4 @@
import { atom } from "nanostores";
export const searchKeyword = atom<string>("");
export const filteredTags = atom<string[]>([]);

77
src/styles/globals.css Normal file
View File

@ -0,0 +1,77 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
@font-face {
font-family: "Geist";
src: url("/fonts/Geist[wght].ttf") format("truetype");
font-display: swap;
}
@font-face {
font-family: "GeistMono";
src: url("/fonts/GeistMono[wght].ttf") format("truetype");
font-display: swap;
}

61
tailwind.config.mjs Normal file
View File

@ -0,0 +1,61 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
theme: {
extend: {
fontFamily: {
sans: ["Geist"],
mono: ["GeistMono"],
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
chart: {
1: "hsl(var(--chart-1))",
2: "hsl(var(--chart-2))",
3: "hsl(var(--chart-3))",
4: "hsl(var(--chart-4))",
5: "hsl(var(--chart-5))",
},
},
},
},
plugins: [require("tailwindcss-animate")],
};

13
tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}