boilerplate
This commit is contained in:
commit
d20ac09a66
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
12
.prettierrc
Normal 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
4
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal 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
48
README.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# Astro Starter Kit: Basics
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm create astro@latest -- --template basics
|
||||||
|
```
|
||||||
|
|
||||||
|
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
|
||||||
|
[](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
|
||||||
|
[](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
|
||||||
|
|
||||||
|
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 🚀 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
16
astro.config.mjs
Normal 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
21
components.json
Normal 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
43
package.json
Normal 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
5763
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
public/favicon.svg
Normal file
9
public/favicon.svg
Normal 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 |
BIN
public/fonts/Geist-Black.woff2
Normal file
BIN
public/fonts/Geist-Black.woff2
Normal file
Binary file not shown.
BIN
public/fonts/Geist-Bold.woff2
Normal file
BIN
public/fonts/Geist-Bold.woff2
Normal file
Binary file not shown.
BIN
public/fonts/Geist-ExtraBold.woff2
Normal file
BIN
public/fonts/Geist-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
public/fonts/Geist-ExtraLight.woff2
Normal file
BIN
public/fonts/Geist-ExtraLight.woff2
Normal file
Binary file not shown.
BIN
public/fonts/Geist-Light.woff2
Normal file
BIN
public/fonts/Geist-Light.woff2
Normal file
Binary file not shown.
BIN
public/fonts/Geist-Medium.woff2
Normal file
BIN
public/fonts/Geist-Medium.woff2
Normal file
Binary file not shown.
BIN
public/fonts/Geist-Regular.woff2
Normal file
BIN
public/fonts/Geist-Regular.woff2
Normal file
Binary file not shown.
BIN
public/fonts/Geist-SemiBold.woff2
Normal file
BIN
public/fonts/Geist-SemiBold.woff2
Normal file
Binary file not shown.
BIN
public/fonts/Geist-Thin.woff2
Normal file
BIN
public/fonts/Geist-Thin.woff2
Normal file
Binary file not shown.
BIN
public/fonts/GeistMono-Black.woff2
Normal file
BIN
public/fonts/GeistMono-Black.woff2
Normal file
Binary file not shown.
BIN
public/fonts/GeistMono-Bold.woff2
Normal file
BIN
public/fonts/GeistMono-Bold.woff2
Normal file
Binary file not shown.
BIN
public/fonts/GeistMono-Light.woff2
Normal file
BIN
public/fonts/GeistMono-Light.woff2
Normal file
Binary file not shown.
BIN
public/fonts/GeistMono-Medium.woff2
Normal file
BIN
public/fonts/GeistMono-Medium.woff2
Normal file
Binary file not shown.
BIN
public/fonts/GeistMono-Regular.woff2
Normal file
BIN
public/fonts/GeistMono-Regular.woff2
Normal file
Binary file not shown.
BIN
public/fonts/GeistMono-SemiBold.woff2
Normal file
BIN
public/fonts/GeistMono-SemiBold.woff2
Normal file
Binary file not shown.
BIN
public/fonts/GeistMono-Thin.woff2
Normal file
BIN
public/fonts/GeistMono-Thin.woff2
Normal file
Binary file not shown.
BIN
public/fonts/GeistMono-UltraBlack.woff2
Normal file
BIN
public/fonts/GeistMono-UltraBlack.woff2
Normal file
Binary file not shown.
BIN
public/fonts/GeistMono-UltraLight.woff2
Normal file
BIN
public/fonts/GeistMono-UltraLight.woff2
Normal file
Binary file not shown.
BIN
public/fonts/GeistMono[wght].ttf
Normal file
BIN
public/fonts/GeistMono[wght].ttf
Normal file
Binary file not shown.
BIN
public/fonts/GeistMono[wght].woff2
Normal file
BIN
public/fonts/GeistMono[wght].woff2
Normal file
Binary file not shown.
BIN
public/fonts/Geist[wght].ttf
Normal file
BIN
public/fonts/Geist[wght].ttf
Normal file
Binary file not shown.
BIN
public/preview.webp
Normal file
BIN
public/preview.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 81 KiB |
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
Sitemap: https://onlineslidemaker.com/sitemap.xml
|
9
public/sitemap.xml
Normal file
9
public/sitemap.xml
Normal 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
1
src/assets/astro.svg
Normal 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 |
1
src/assets/background.svg
Normal file
1
src/assets/background.svg
Normal 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 |
0
src/components/Header.astro
Normal file
0
src/components/Header.astro
Normal file
51
src/components/ListTags.tsx
Normal file
51
src/components/ListTags.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
78
src/components/ListWebsites.tsx
Normal file
78
src/components/ListWebsites.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
86
src/components/ModalSubmitNew.tsx
Normal file
86
src/components/ModalSubmitNew.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
55
src/components/ModeToggle.tsx
Normal file
55
src/components/ModeToggle.tsx
Normal 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
135
src/components/Navbar.tsx
Normal 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
77
src/components/Search.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
23
src/components/common/Spinner.tsx
Normal file
23
src/components/common/Spinner.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
36
src/components/ui/badge.tsx
Normal file
36
src/components/ui/badge.tsx
Normal 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 }
|
57
src/components/ui/button.tsx
Normal file
57
src/components/ui/button.tsx
Normal 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 }
|
120
src/components/ui/dialog.tsx
Normal file
120
src/components/ui/dialog.tsx
Normal 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,
|
||||||
|
}
|
116
src/components/ui/drawer.tsx
Normal file
116
src/components/ui/drawer.tsx
Normal 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,
|
||||||
|
}
|
199
src/components/ui/dropdown-menu.tsx
Normal file
199
src/components/ui/dropdown-menu.tsx
Normal 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,
|
||||||
|
}
|
22
src/components/ui/input.tsx
Normal file
22
src/components/ui/input.tsx
Normal 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 }
|
24
src/components/ui/label.tsx
Normal file
24
src/components/ui/label.tsx
Normal 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 }
|
29
src/components/ui/separator.tsx
Normal file
29
src/components/ui/separator.tsx
Normal 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 }
|
22
src/components/ui/textarea.tsx
Normal file
22
src/components/ui/textarea.tsx
Normal 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 }
|
30
src/components/ui/tooltip.tsx
Normal file
30
src/components/ui/tooltip.tsx
Normal 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
481
src/data/websites.json
Normal 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
78
src/layouts/Layout.astro
Normal 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
6
src/lib/utils.ts
Normal 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
112
src/pages/index.astro
Normal 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
4
src/store.ts
Normal 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
77
src/styles/globals.css
Normal 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
61
tailwind.config.mjs
Normal 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
13
tsconfig.json
Normal 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/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user