Compare commits

...

10 Commits

Author SHA1 Message Date
itsamejms 68c393af5e setting up firebase for static hosting 2026-05-28 16:28:13 +01:00
itsamejms c5f4221c8d updating website copy and adding new ProblemSolution.tsx 2026-05-28 15:40:57 +01:00
itsamejms b8bb2ede70 adding the Account Deletion stuff for the play store 2026-05-10 15:10:10 +01:00
itsamejms ccd08dac4e troubleshooting the blog builds 2026-05-02 09:01:36 +01:00
itsamejms 931063b7c3 adding the blogs 2026-04-30 21:42:02 +01:00
itsamejms bc920a292d updating cooke styling 2026-03-20 09:53:17 +00:00
itsamejms 0f4976e58a adding google analytics to see site visits 2026-03-20 09:36:54 +00:00
itsamejms d1f6711d87 complete reskin of the frontend 2026-03-19 18:16:14 +00:00
itsamejms e253e1cbc5 adding redirect after waitlist success 2026-03-05 17:42:19 +00:00
itsamejms 7a096c13e9 adding a waitlist page 2026-03-05 14:29:15 +00:00
114 changed files with 4016 additions and 9692 deletions
+4
View File
@@ -0,0 +1,4 @@
VITE_GA_ID=G-XXXXXXXXXX
# Add your Google Analytics Measurement ID above.
# Example: VITE_GA_ID=G-1A2B3C4D5E
+31
View File
@@ -0,0 +1,31 @@
tailtrails-logo.png,1779981120237,25213feab6216a1fca7c6c84eecd70e7183c867279c2ea28885d800ee4e21168
robots.txt,1779981120236,6d6ba7a91379049f0c537dab9c6af1cf57bdabd056ee7923a4a0d917937b2678
placeholder.svg,1779981120236,69548e0e084c5cf2efe962717e3855bc81e133a8a23895508b0673308246ff5a
index.html,1779981120291,7ad5d1dd9f5500daef6c2d9b3e9fbe41e2f43db0a87cca4964de9fda9ea535b4
hero.mp4,1779981120222,b8107714858fdcb7f31cd4b9ff2da143d7b85ef09d21d4905133f3a58851604f
app_store_button.svg,1779981120140,898762c1c4511688925b4494df0f0164358d104e7bcffe1a0f860a3755e12e4a
IMG_2725.PNG,1779981120139,fb5ee86caf2de96735d9a723724572cb681720c82c28afbab47fa485ddfd6b6f
IMG_2724.PNG,1779981120138,d2daf949d118406283c8a1572f3fef93360a027d562655c09469a5aec2db828d
IMG_2722.PNG,1779981120138,751b407a25e5ca29147a8b9c0a79e9ce3708059440819ba7b71b723ac42f2de4
IMG_2721.PNG,1779981120137,f122766aa4f43089ea7aaff9781d6aa198abf1fc4ead95ab3c50b5edaabcf963
CNAME,1779981120326,e4a11dde197a450444c324a92a2c539b37517bd75052195e5a810fbc66c0c762
404.html,1779981120329,7ad5d1dd9f5500daef6c2d9b3e9fbe41e2f43db0a87cca4964de9fda9ea535b4
raw-blogs/walking-your-dog-in-the-algarve-hidden-dangers-every-owner-should-know.md,1779981120236,f23f15b0f2fd306bad91730dcf0bcb31bc263377985197e20ad646997ef7838e
raw-blogs/top-10-hikes-to-do-with-your-dog-in-lagos-and-the-algarve.md,1779981120236,4bbf52eb97adb7851b48c44e28444e3e9d48a1687255bdf88ab68b8c0985b8a7
raw-blogs/the-importance-of-variety-in-your-dogs-life-and-how-to-do-it-right.md,1779981120236,51c814149946286c9934d899dc375666edea5bd6e361205d31d943ef59a5aacb
raw-blogs/off-leash-dog-rules-in-portugal-what-you-need-to-know-in-2026.md,1779981120236,244c337c4a3b0a6c93145993fb1d5c5a7ccc44ce99f24a6d16286ab2f4b3c7da
raw-blogs/how-to-give-a-reactive-dog-a-fulfilling-life-without-avoiding-the-world.md,1779981120236,065db19eb077716b5bb65f241089b877c774814eb41f4d71fd0223881df1fb4e
raw-blogs/how-to-choose-the-right-walk-for-your-dog-based-on-their-personality-and-needs.md,1779981120236,1a15afc0d69599e5096003db584115249db68d78d4e68d8ee643a2c013048f40
raw-blogs/best-off-leash-areas-for-dogs-in-lagos-and-nearby.md,1779981120236,9c315b7e176b2bc162bf081ad4a46a4333ddc88901030ccdef856d55dcb9e655
landing/sad-dog.avif,1779981120235,3b50e7a96d4351871fe2f350fcd3f5bdf30a53a010e3c9438addab72cd9c2225
landing/happy-dog.avif,1779981120235,7360b2adf84a2061a598b83a2d3d3d090dbfef203e9d8c601c906e3fa8128237
blog-images/troy-bridges-lbRNtUZPXXQ-unsplash.jpg,1779981120183,9a0d7eaa207291485b4a28496986b72be53fb01519c5fbcbe088c48b2c27db51
blog-images/tatum-bergen-Vt3odrTM4cQ-unsplash.jpg,1779981120176,ef6ec8b86f80a54b40b90a1eae3be824754e7bd04e8acd4b6e67417781ecebb7
blog-images/rafaella-waasdorp-EkzMdwI_YE4-unsplash.jpg,1779981120173,4f7307562e231a56a7de503d9fe51d9eb8ea5ce383e0a5abf34414956f5ae054
blog-images/joe-caione-qO-PIF84Vxg-unsplash.jpg,1779981120169,2b8c3f8acf6af4a7b393a8519f18f1bd1b29dff8239af607483426e51e0199d6
blog-images/jamie-street-m4BkRRprvP0-unsplash.jpg,1779981120163,e49aaeb7a75de79bd31bca28537b1f52f2c3ef42d43cb1962cb5253ca77b3588
blog-images/eric-ward-ISg37AI2A-s-unsplash.jpg,1779981120157,730cc24eef3e5dd4416cea11cfd400959aab10484ff4858dc664f8bc377e7675
blog-images/dagmar-klauzova-afBc-k2luH4-unsplash.jpg,1779981120154,4f6adbc259fd2710b1626c2d6caf2eebfbd4f80af8bde6560cdd0ead7ed95128
blog-images/afra-ramio-cp2HCVzguw4-unsplash.jpg,1779981120146,b18fdc3760ba9137773cd144dea1f0f7d79565f33ea0c6a123cb0646d954f1c6
assets/index-Bp_UvqRF.css,1779981120291,7d1d66e5f82b558a737c3aaa1b758e33a6271e174701a91ff4d8a1ce82d969d6
assets/index-BYX2M-TQ.js,1779981120291,1fcf5dbf9296f9455d29976c57a0cf262e28f7638c55b89c7b81d48d9efd7d2b
+5
View File
@@ -0,0 +1,5 @@
{
"projects": {
"default": "tail-trails"
}
}
+5
View File
@@ -1,2 +1,7 @@
# tt-website
The simple react homepage/ CTA for TailTrails.
# Deploy with fireabse
- firebase login --reauth
- npm run build
- firebase deploy
-20
View File
@@ -1,20 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
-26
View File
@@ -1,26 +0,0 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"@typescript-eslint/no-unused-vars": "off",
},
},
);
+16
View File
@@ -0,0 +1,16 @@
{
"hosting": {
"public": "dist",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}
+1309 -4852
View File
File diff suppressed because it is too large Load Diff
+22 -67
View File
@@ -6,7 +6,7 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"postbuild": "node ./scripts/postbuild.cjs",
"postbuild": "node ./scripts/postbuild.cjs",
"predeploy": "npm run build",
"deploy": "gh-pages -d dist",
"build:dev": "vite build --mode development",
@@ -14,73 +14,28 @@
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-aspect-ratio": "^1.1.7",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-context-menu": "^2.2.15",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-hover-card": "^1.1.14",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-menubar": "^1.1.15",
"@radix-ui/react-navigation-menu": "^1.2.13",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toast": "^1.2.14",
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-query": "^5.83.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.61.1",
"react-resizable-panels": "^2.1.9",
"react-router-dom": "^6.30.1",
"recharts": "^2.15.4",
"sonner": "^1.7.4",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.9",
"zod": "^3.25.76"
"@radix-ui/react-toast": "^1.2.15",
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^6.0.1",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"lucide-react": "^0.577.0",
"marked": "^18.0.2",
"motion": "^12.23.24",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.13.1",
"simple-icons": "^16.12.0",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@eslint/js": "^9.32.0",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^22.16.5",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react-swc": "^3.11.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.32.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^15.15.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0",
"vite": "^5.4.19",
"gh-pages": "^5.0.0"
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"autoprefixer": "^10.4.27",
"gh-pages": "^6.3.0",
"tailwindcss": "^4.2.2",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
"vite": "^8.0.1"
}
}
-6
View File
@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

+46
View File
@@ -0,0 +1,46 @@
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
<title>Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917</title>
<g>
<g>
<g>
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/>
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
</g>
<g id="_Group_" data-name="&lt;Group&gt;">
<g id="_Group_2" data-name="&lt;Group&gt;">
<g id="_Group_3" data-name="&lt;Group&gt;">
<path id="_Path_" data-name="&lt;Path&gt;" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
<path id="_Path_2" data-name="&lt;Path&gt;" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
</g>
</g>
<g>
<path d="M42.30227,27.13965h-4.7334l-1.13672,3.35645H34.42727l4.4834-12.418h2.083l4.4834,12.418H43.438ZM38.0591,25.59082h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/>
<path d="M55.15969,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H48.4302v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.645,21.34766,55.15969,23.16406,55.15969,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30227,29.01563,53.24953,27.81934,53.24953,25.96973Z" style="fill: #fff"/>
<path d="M65.12453,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H58.395v1.50586h.03418A3.21162,3.21162,0,0,1,61.312,21.34766C63.60988,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26711,29.01563,63.21438,27.81934,63.21438,25.96973Z" style="fill: #fff"/>
<path d="M71.71047,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z" style="fill: #fff"/>
<path d="M86.065,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72609,30.6084,86.065,28.82617,86.065,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40039,1.16211-2.40039,3.10742c0,1.96191.89453,3.10645,2.40039,3.10645S92.76027,27.93164,92.76027,25.96973Z" style="fill: #fff"/>
<path d="M96.18606,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z" style="fill: #fff"/>
<path d="M109.3843,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10207,25.13477Z" style="fill: #fff"/>
</g>
</g>
</g>
<g id="_Group_4" data-name="&lt;Group&gt;">
<g>
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z" style="fill: #fff"/>
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z" style="fill: #fff"/>
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z" style="fill: #fff"/>
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z" style="fill: #fff"/>
<path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z" style="fill: #fff"/>
<path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z" style="fill: #fff"/>
<path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z" style="fill: #fff"/>
<path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z" style="fill: #fff"/>
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z" style="fill: #fff"/>
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/>
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z" style="fill: #fff"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

@@ -0,0 +1,126 @@
### **Introduction**
Finding a good place to let your dog off-leash in Lagos isnt always straightforward.
Some areas are:
* Too busy
* Too unpredictable
* Or not actually suitable for your dog
But when you find the right spot, it changes everything.
Here are some of the best off-leash-friendly areas around Lagos, and how to choose the right one for your dog.
---
## **1. Mata Nacional de Barão de São João**
**Best for:** Most dogs
**Why it works:**
* Wide, open forest trails
* Plenty of space to create distance
* Shaded and cooler
Ideal for dogs who need **space without isolation**
---
## **2. Praia da Bordeira (West Coast)**
**Best for:** High-energy dogs
**Why it works:**
* Huge open beach
* Low crowd density (outside peak times)
* Great for running
Watch:
* Wind exposure
* Seasonal rules
---
## **3. Ria de Alvor (Quiet Sections)**
**Best for:** Lower-energy or calm dogs
**Why it works:**
* Flat terrain
* Open visibility
Avoid peak hours, it can get busy
---
## **4. Monchique Countryside**
**Best for:** Dogs needing low stimulation
**Why it works:**
* Quiet
* Natural
* Cooler temperatures
Great for reactive or sensitive dogs
---
## **5. Local Countryside Trails (Hidden Gems)**
Often overlooked, but some of the best options.
These areas offer:
* Lower dog traffic
* More control
* Flexible space
Especially useful for:
* Training recall
* Building confidence
Check out trails near countryside villages like Bensafrim and Vila do Bispo.
---
## **Important: Not Every “Off-Leash Area” Is Right**
Just because a place allows off-leash doesnt mean its suitable.
Before letting your dog off:
* Check the environment
* Look at dog traffic
* Consider your dogs behaviour
---
## **What Makes a Good Off-Leash Spot?**
The best places have:
* Space to move freely
* Clear visibility
* Low unpredictability
* An environment suited to your dog
---
## **Final Thoughts**
The goal isnt just to let your dog off-leash.
Its to do it in a way that:
* Feels safe
* Feels calm
* And actually benefits your dog
Because the right environment makes all the difference.
<!-- **Want help finding the right trails for your dog? Download TailTrails here.** -->
@@ -0,0 +1,131 @@
### **Introduction**
Choosing a walk for your dog often comes down to convenience.
Whats nearby.
What youve done before.
What seems “good enough.”
But the best walks arent chosen that way.
Theyre chosen based on **your dog**.
---
### **Step 1: Understand Your Dogs Profile**
Start with the basics:
* Energy level (low, medium, high)
* Sociability (loves dogs vs prefers space)
* Confidence (bold vs cautious)
* Sensitivities (noise, movement, crowds)
* Physical needs (age, injuries, endurance)
This becomes your baseline.
---
### **Step 2: Match the Environment**
Now consider the walk itself:
#### **High-energy dogs**
Need:
* Long routes
* Open space
* Opportunities to run
Best environments:
* Wide trails
* Open countryside
* Low restriction areas
---
**Sensitive or reactive dogs**
Need:
* Predictability
* Space from others
* Low stimulation
Best environments:
* Quiet trails
* Low dog traffic areas
* Wide paths with visibility
---
#### **Older or recovering dogs**
Need:
* Shorter distances
* Even terrain
* Shade
Best environments:
* Flat trails
* Forest paths
* Low-exertion routes
---
### **Step 3: Look Beyond the Surface**
A trail might look perfect, but details matter:
* Is it narrow or open?
* Is there shade?
* Are dogs typically off-leash there?
* Is it busy at certain times?
These small factors make a big difference.
---
### **Step 4: Pay Attention to Feedback**
Your dog will tell you if its working.
Look for:
* Relaxed body language
* Natural curiosity
* Steady energy (not frantic or shut down)
* Willing engagement with surroundings
If youre seeing stress signals, it may not be the right fit.
---
### **Step 5: Prioritise Variety Intentionally**
Variety is important.
But random variety isnt helpful.
Instead:
* Rotate between environments that suit your dog
* Gradually introduce new challenges
* Build confidence over time
---
### **Final Thoughts**
The best walk isnt the longest or the most popular.
Its the one that fits your dog.
Because when you choose the right environment, youre not just going for a walk, youre creating a better experience for both of you.
<!-- **Want help finding the right trails for your dog? Download TailTrails here.** -->
@@ -0,0 +1,120 @@
### **Introduction**
Living with a reactive dog can feel limiting.
You might:
* Avoid certain places
* Walk at odd hours
* Stick to the same safe routes
But heres the truth:
Your dog can still have a full, enriching life.
It just requires a different approach.
---
## **What Reactivity Often Means**
Reactivity isnt “bad behaviour.”
Its usually:
* Overstimulation
* Fear or uncertainty
* Lack of space
* Too much, too quickly
---
## **Environment Matters More Than Training Alone**
Training is important, but environment is foundational.
Even a well-trained dog can struggle in:
* Busy areas
* Narrow paths
* High dog-traffic zones
---
## **Choosing the Right Environments**
Look for places that offer:
* Space to move away
* Predictability
* Lower levels of stimulation
Examples:
* Wide trails
* Quiet countryside
* Off-peak walking times
---
## **Give Them the Right Kind of Freedom**
Reactive dogs dont need:
* More pressure
* More exposure too quickly
They need:
* Controlled experiences
* Space to decompress
* Opportunities to succeed
---
## **Common Mistakes**
* Taking them to busy “dog-friendly” areas
* Forcing social interactions
* Ignoring early stress signals
---
## **Building Confidence Over Time**
Start small.
* Familiar environments
* Gradual changes
* Positive reinforcement
Confidence grows through:
Consistent, successful experiences
---
## **Fulfilment Looks Different**
A fulfilling life doesnt mean:
* Busy parks
* Social environments
It means:
* Calm exploration
* Comfortable movement
* Feeling safe in the world
---
## **Final Thoughts**
Your dog doesnt need to handle everything.
They just need the **right environments to thrive in**.
And when you give them that, everything starts to shift.
<!-- **Want help finding the right trails for your dog? Download TailTrails here.** -->
@@ -0,0 +1,125 @@
### **Introduction**
Letting your dog off-leash is one of the simplest ways to give them freedom, enrichment, and a more natural experience.
But if youre living in or visiting Portugal, the rules arent always obvious, and getting it wrong can lead to fines, unsafe situations, or conflict with others.
Heres what you need to know.
---
### **Are Dogs Allowed Off-Leash in Portugal?**
In general, dogs in Portugal are expected to be **under control at all times in public spaces**.
That doesnt always mean physically on a leash - but it *does* mean:
* Your dog must respond reliably to commands
* They must not pose a risk to people, other animals, or property
* They must not cause disturbance
In many **urban areas**, leash use is effectively required.
In more **rural or remote areas**, off-leash walking is more commonly accepted - but still comes with responsibility.
---
### **Official Rules, Restrictions, and Fines**
In Portugal, there are also specific legal restrictions to be aware of, especially around beaches and public spaces. During the summer season (typically June to September), many beaches prohibit dogs entirely, regardless of whether they are on or off-leash. These rules are set by local municipalities and are usually clearly signposted at beach entrances. Outside of summer, some beaches become dog-friendly again, but leash expectations can still apply.
Failing to follow these rules can result in on-the-spot fines, which typically range from around €25 up to €250 or more, depending on the situation and location. Additional penalties may apply if a dog is considered out of control, causes disturbance, or poses a risk to others.
Because enforcement and rules can vary by region, its always worth checking local signage before letting your dog off-leash, especially in popular or seasonal areas.
---
### **Where Off-Leash Walking Is (Usually) Acceptable**
Youre more likely to safely and responsibly walk your dog off-leash in:
* Quiet countryside trails
* Forest paths with low foot traffic
* Remote coastal routes (away from crowds)
* Open land where visibility is high
However, even in these areas, its not just about permission - its about suitability for your dog.
---
### **Where You Should Keep Your Dog On-Leash**
There are several environments where off-leash walking is **not recommended or restricted**:
* Busy beaches (especially during peak season)
* Urban parks and promenades
* Narrow cliff paths
* Areas with livestock or wildlife
* High dog-traffic zones
Even if technically allowed, these environments can be:
* Overstimulating
* Unsafe
* Stressful for reactive or sensitive dogs
---
### **The Risks Most Dog Owners Overlook**
Many owners assume off-leash = freedom.
But without the right environment, it can quickly become the opposite.
Common risks include:
* Uncontrolled interactions with other dogs
* Encounters with wildlife or livestock
* Sudden terrain hazards (cliffs, unstable ground)
* Overheating in exposed areas
* Poor recall in unfamiliar settings
The result? Stress for both you *and* your dog.
---
### **Why “Dog-Friendly” Isnt Enough**
A beach might allow dogs.
A trail might be popular with dog walkers.
But that doesnt mean its **right for your dog**.
Every dog has different needs:
* Some thrive in social, high-energy spaces
* Others need quiet, predictable environments
* Some need open space to run
* Others need structured, low-stimulation walks
Choosing the wrong environment can undo the very thing youre trying to give them.
---
### **How to Choose the Right Off-Leash Environment**
Before letting your dog off-leash, ask:
* How busy is this area?
* Are there other dogs nearby?
* Is the terrain safe and visible?
* Is there wildlife or livestock?
* Will my dog feel confident here, or overwhelmed?
The goal isnt just freedom.
Its **setting your dog up for a good time**.
---
### **Final Thoughts**
Walking your dog off-leash in Portugal can be an awesome experience, when its done in the right place, at the right time, for the right dog.
Because its not just about whether dogs are allowed.
Its about whether the environment actually fits.
<!-- **Want help finding the right trails for your dog? Download TailTrails here.** -->
@@ -0,0 +1,115 @@
### **Introduction**
Most dogs live very predictable lives.
The same route.
The same pace.
The same environment.
And while routine has its place, too much of it can limit your dogs experience of the world.
---
## **Why Variety Matters**
Dogs are naturally curious.
Theyre wired to:
* Explore
* Smell
* Navigate new environments
When every walk is the same, they miss out on:
* Mental stimulation
* Physical engagement
* Confidence-building experiences
---
## **Its Not Just About Exercise**
A longer walk doesnt always mean a better walk.
A short walk in a new environment can be:
* More stimulating
* More engaging
* More fulfilling
---
## **The Benefits of Variety**
Introducing new environments can:
* Reduce boredom
* Improve behaviour
* Build confidence
* Strengthen your bond
---
## **But Random Variety Isnt the Answer**
This is where many owners get it wrong.
Throwing your dog into:
* Busy environments
* Unpredictable situations
* Overstimulating spaces
Can actually:
* Increase stress
* Lead to reactivity
* Create negative experiences
---
## **How to Introduce Variety Properly**
### **1. Start with similar environments**
If your dog is used to quiet trails, try new *quiet* trails first.
---
### **2. Change one variable at a time**
* New location, same energy level
* New terrain, same level of stimulation
---
### **3. Watch your dogs response**
Let their behaviour guide your decisions.
---
### **4. Build gradually**
Confidence grows with positive experiences.
---
## **The Goal**
Not constant novelty.
But **intentional variation** that suits your dog.
---
## **Final Thoughts**
Your dog doesnt need more walks.
They need **better, more varied experiences**.
Because when you get it right, even a simple walk can feel completely different.
<!-- **Want help finding the right trails for your dog? Download TailTrails here.** -->
@@ -0,0 +1,86 @@
### **Introduction**
If youre based in or visiting Lagos, youre surrounded by some of the most beautiful walking routes in Europe.
But not all hikes are equal - especially when youre walking with a dog.
Some are:
* Too exposed
* Too busy
* Too steep
* Or just not suited to your dogs needs
Here are 10 of the best dog-friendly hikes in the Algarve, plus what makes each one a good fit.
---
## **1. Ponta da Piedade Coastal Walk**
**Best for:** Calm, confident dogs
**Type:** Cliffside / scenic
Incredible views, but:
* Narrow in parts
* High foot traffic
Best early morning or off-season
---
## **2. Mata Nacional de Barão de São João**
**Best for:** Most dogs
**Type:** Forest / shaded
* Wide trails
* Plenty of shade
* Lower stimulation
---
## **3. Ria de Alvor Nature Reserve**
**Best for:** Low to moderate energy dogs
**Type:** Boardwalk / coastal
* Flat and accessible
* Can be busy
* Limited shade
---
## **How to Choose the Right One**
Instead of asking:
“Which hike is best?”
Ask:
“Which hike is right for my dog?”
Consider:
* Energy level
* Sociability
* Confidence
* Weather
* Time of day
---
## **Final Thoughts**
The Algarve is full of incredible places to explore with your dog.
But the best walks arent just beautiful.
Theyre the ones that:
* Suit your dog
* Feel comfortable
* And allow them to be at their best
<!-- **Want help finding the right trails for your dog? Download TailTrails here.** -->
@@ -0,0 +1,71 @@
### **Introduction**
The Algarve is one of the best places in Europe to explore with your dog.
Cliffside trails.
Quiet forests.
Open countryside.
But beneath all that beauty, there are a few **less obvious risks** that many dog owners (especially newcomers) dont see coming.
Knowing what to look out for can make the difference between a great walk and an emergency vet visit.
---
## **1. Pine Processionary Caterpillars (High Risk)**
These are one of the **most dangerous hazards** for dogs in Portugal.
They appear in:
* Pine forests
* Woodland trails
* Warmer months (typically late winter to spring)
### **Why theyre dangerous:**
Their tiny hairs contain a toxin that can cause:
* Severe swelling of the mouth and tongue
* Vomiting
* Difficulty breathing
* Tissue damage (in serious cases)
### **What to look for:**
* Caterpillars moving in a line (procession)
* Nests in pine trees (silk-like bundles)
---
## **2. Grass Seeds (Foxtails)**
Small, sharp, and incredibly problematic.
Theyre common in:
* Dry grasslands
* Trail edges
* Late spring and summer
### **Why theyre dangerous:**
They can lodge in:
* Noses
* Ears
* Eyes
* Between toes
---
## **Final Thoughts**
Walking your dog in the Algarve can be one of the best parts of living or visiting here.
But the best walks happen when you:
* Know what to look out for
* Choose environments intentionally
* Match the walk to your dogs needs
<!-- **Want help finding the right trails for your dog? Download TailTrails here.** -->
+2 -2
View File
@@ -12,10 +12,10 @@
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
filter: drop-shadow(0 0 2em rgba(255,254,119,0.67));
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
filter: drop-shadow(0 0 2em rgba(212,255,94,0.67));
}
@keyframes logo-spin {
+24 -16
View File
@@ -1,29 +1,37 @@
import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Index from "./pages/Index";
import Privacy from "./pages/Privacy";
import NotFound from "./pages/NotFound";
const queryClient = new QueryClient();
import Waitlist from "./pages/Waitlist";
import RequestAccountDeletion from "./pages/RequestAccountDeletion";
import FinalizeAccountDeletion from "./pages/FinalizeAccountDeletion";
import BlogList from "./pages/BlogList";
import BlogPost from "./pages/BlogPost";
import CookieConsent from "./components/ui/CookieConsent";
import { ToastProvider, ToastViewport } from "./components/ui/toast";
import Toaster from "./components/ui/Toaster";
const App = () => (
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<Toaster />
<Sonner />
<>
<ToastProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/privacy" element={<Privacy />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
<Route path="/" element={<Index />} />
<Route path="/privacy" element={<Privacy />} />
<Route path="/waitlist" element={<Waitlist />} />
<Route path="/forget-me" element={<RequestAccountDeletion />} />
<Route path="/finalize-forget-me" element={<FinalizeAccountDeletion />} />
<Route path="/blog" element={<BlogList />} />
<Route path="/blog/:slug" element={<BlogPost />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</TooltipProvider>
</QueryClientProvider>
<Toaster />
<ToastViewport />
</ToastProvider>
<CookieConsent />
</>
);
export default App;
-28
View File
@@ -1,28 +0,0 @@
import { NavLink as RouterNavLink, NavLinkProps } from "react-router-dom";
import { forwardRef } from "react";
import { cn } from "@/lib/utils";
interface NavLinkCompatProps extends Omit<NavLinkProps, "className"> {
className?: string;
activeClassName?: string;
pendingClassName?: string;
}
const NavLink = forwardRef<HTMLAnchorElement, NavLinkCompatProps>(
({ className, activeClassName, pendingClassName, to, ...props }, ref) => {
return (
<RouterNavLink
ref={ref}
to={to}
className={({ isActive, isPending }) =>
cn(className, isActive && activeClassName, isPending && pendingClassName)
}
{...props}
/>
);
},
);
NavLink.displayName = "NavLink";
export { NavLink };
+67 -28
View File
@@ -1,38 +1,77 @@
import { Button } from "@/components/ui/button";
import { ArrowRight } from "lucide-react";
import { motion } from 'motion/react';
const CTA = () => {
return (
<section className="py-24 bg-background relative overflow-hidden">
{/* Background gradient */}
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-primary/5 to-primary/10" />
<div className="container mx-auto px-6 relative z-10">
<div className="max-w-4xl mx-auto text-center">
<h2 className="text-4xl md:text-5xl lg:text-6xl font-display font-bold text-foreground mb-6 text-balance">
Ready to Start Your <span className="text-primary">Walking Journey</span>?
</h2>
<p className="text-lg md:text-xl text-muted-foreground font-body mb-10 max-w-2xl mx-auto">
Join thousands of dog owners who are making every walk count.
It's free to get started!
</p>
<section className="py-20">
<div className="max-w-7xl mx-auto px-6">
<div className="glass rounded-[48px] p-8 md:p-20 text-center relative overflow-hidden border border-border-subtle">
<div className="absolute top-0 left-0 w-full h-full bg-brand/5 -z-10" />
<div className="absolute -top-24 -right-24 w-64 h-64 bg-brand/10 blur-[80px] rounded-full" />
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Button variant="hero" size="xl" className="group" asChild>
<a href="https://app.tailtrails.club/signup" target="_blank" rel="noopener noreferrer">
Get Started Free
<ArrowRight className="w-5 h-5 transition-transform group-hover:translate-x-1" />
</a>
</Button>
</div>
<p className="mt-6 text-sm text-muted-foreground font-body">
No credit card required Free forever for basic features
</p>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ type: "spring", stiffness: 100, damping: 20 }}
>
<h2 className="text-4xl md:text-6xl font-display font-extrabold mb-6 text-text-primary tracking-tight">
Ready to Start Your <br />
<span className="text-brand">Walking Journey?</span>
</h2>
<p className="text-text-secondary text-lg md:text-xl max-w-2xl mx-auto mb-10">
Join thousands of dog owners who are making every walk count. Download the app today!
</p>
{/* Download Buttons Container */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 max-w-md mx-auto">
{/* App Store Active Link */}
<motion.a
href="https://apps.apple.com/us/app/tail-trails-club/id6758676111"
target="_blank"
rel="noopener noreferrer"
whileHover={{ scale: 1.03, y: -2 }}
whileTap={{ scale: 0.98 }}
className="w-full sm:w-auto min-w-[200px] bg-brand text-zinc-950 px-6 py-4 rounded-2xl font-bold flex items-center justify-center gap-3 transition-colors shadow-lg shadow-brand/10"
>
{/* Apple SVG Icon */}
<svg className="w-6 h-6 fill-current" viewBox="0 0 24 24">
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M15.97 4.17c.66-.81 1.11-1.93.99-3.06-1 .04-2.21.67-2.93 1.49-.62.69-1.16 1.84-1.01 2.96 1.12.09 2.27-.58 2.95-1.39z"/>
</svg>
<div className="text-left leading-tight">
<p className="text-[11px] uppercase opacity-80 font-medium">Download on the</p>
<p className="text-base font-extrabold -mt-0.5">App Store</p>
</div>
</motion.a>
{/* Play Store Disabled / Coming Soon Button */}
<div
className="w-full sm:w-auto min-w-[200px] bg-zinc-900 border border-zinc-800 text-zinc-400 px-6 py-4 rounded-2xl font-bold flex items-center justify-center gap-3 relative group"
title="Android version coming soon!"
>
{/* Play Store SVG Icon */}
<svg className="w-6 h-6 fill-current opacity-50" viewBox="0 0 24 24">
<path d="M5,3.14V20.86c0,0.5,0.4,0.92,0.91,0.85l9.92-9.92L5.91,2.29C5.4,2.22,5,2.64,5,3.14Z"/>
<path d="M17.23,10.37,6.83,4.41a0.39,0.39,0,0,0-.41,0L15.8,13.8Z"/>
<path d="M15.8,13.8l-9.38,9.38a0.39,0.39,0,0,0,.41,0l10.4-5.96A2.16,2.16,0,0,0,17.23,13.63,2.18,2.18,0,0,0,17.23,10.37Z"/>
</svg>
<div className="text-left leading-tight">
<p className="text-[11px] uppercase opacity-60 font-medium">Get it on</p>
<p className="text-base font-extrabold -mt-0.5 whitespace-nowrap">Google Play</p>
</div>
{/* Absolute overlay badge indicating "Coming Soon" */}
<span className="absolute -top-2.5 right-3 bg-zinc-800 text-zinc-400 text-[10px] uppercase font-black px-2 py-0.5 rounded-full tracking-wider border border-zinc-700">
Soon
</span>
</div>
</div>
</motion.div>
</div>
</div>
</section>
);
};
export default CTA;
export default CTA;
+89 -64
View File
@@ -1,76 +1,101 @@
import { MapPin, Share2, Bookmark, Tag, Users, TrendingUp } from "lucide-react";
const features = [
{
icon: MapPin,
title: "Record Every Walk",
description: "Track your routes with GPS precision. See distance, duration, and the path you explored with your furry friend.",
},
{
icon: Share2,
title: "Share Your Adventures",
description: "Post your favorite walks and let other dog owners discover amazing routes in their neighborhood.",
},
{
icon: Bookmark,
title: "Save & Redo",
description: "Found a perfect trail? Save it for later and easily navigate back to your dog's favorite spots.",
},
{
icon: Tag,
title: "Smart Labels",
description: "Tag walks with helpful info like 'good for older dogs', 'off-leash friendly', or 'water access'.",
},
{
icon: Users,
title: "Community Driven",
description: "Connect with local dog walkers, share tips, and discover hidden gems in your area.",
},
{
icon: TrendingUp,
title: "Track Progress",
description: "Monitor your walking stats over time and celebrate milestones with your four-legged companion.",
},
];
import { motion } from 'motion/react';
import { MapPin, Share2, Bookmark, Tag, Users, TrendingUp, Compass, Route } from 'lucide-react';
const Features = () => {
const features = [
{
title: "Record Every Walk",
desc: "Track your routes with GPS precision. See distance, duration, and the path you explored with your furry friend.",
icon: MapPin,
size: "md:col-span-8",
color: "bg-brand/20",
iconColor: "text-brand"
},
{
title: "Share Your Adventures",
desc: "Post your favorite walks and let other dog owners discover amazing routes in their neighborhood.",
icon: Share2,
size: "md:col-span-4",
color: "bg-brand",
iconColor: "text-zinc-950"
},
{
title: "Save & Redo",
desc: "Found a perfect trail? Save it for later and easily navigate back to your dog's favorite spots.",
icon: Bookmark,
size: "md:col-span-4",
color: "bg-brand/20",
iconColor: "text-brand"
},
{
title: "Smart Marching",
desc: "You tell us about your dog. Our community tags every trail. Then we combine this information - so you can choose where to go with confidence.",
icon: Tag,
size: "md:col-span-8",
color: "bg-brand/20",
iconColor: "text-brand"
},
{
title: "Community Driven",
desc: "Connect with local dog walkers, share tips, and discover hidden gems in your area.",
icon: Users,
size: "md:col-span-8",
color: "bg-brand/20",
iconColor: "text-brand"
},
{
title: "Track Progress",
desc: "Monitor your walking stats over time and celebrate milestones with your four-legged companion.",
icon: TrendingUp,
size: "md:col-span-4",
color: "bg-brand/20",
iconColor: "text-brand"
},
{
title: "Discover new places",
desc: "Find trails you didn't know existed - that bring out the best in your dog.",
icon: Compass,
size: "md:col-span-5",
color: "bg-brand/20",
iconColor: "text-brand"
},
{
title: "Break the routine",
desc: "No more defaulting to the same path. Open the app. Go somewhere new.",
icon: Route,
size: "md:col-span-7",
color: "bg-brand/20",
iconColor: "text-brand"
}
];
return (
<section id="features" className="py-24 bg-card relative overflow-hidden">
{/* Background decoration */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[400px] bg-primary/5 rounded-full blur-3xl" />
<div className="container mx-auto px-6 relative z-10">
{/* Section header */}
<div className="text-center max-w-2xl mx-auto mb-16">
<span className="inline-block px-4 py-1.5 rounded-full bg-primary/10 text-primary text-sm font-medium font-body mb-4">
Features
</span>
<h2 className="text-4xl md:text-5xl font-display font-bold text-foreground mb-4">
Everything You Need for Happy Walks
</h2>
<p className="text-lg text-muted-foreground font-body">
<section id="features" className="py-20 bg-bg-primary scroll-mt-18">
<div className="max-w-7xl mx-auto px-6">
<div className="text-center mb-16">
<h2 className="text-4xl md:text-5xl font-display font-bold mb-4 text-text-primary">Everything You Need for Happy Walks</h2>
<p className="text-text-secondary font-medium max-w-2xl mx-auto">
Designed with dog lovers in mind, TailTrails makes every walk memorable and shareable.
</p>
</div>
{/* Features grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
{features.map((feature, index) => (
<div
key={feature.title}
className="group p-6 rounded-2xl bg-background border border-border/50 hover:border-primary/30 hover:shadow-elevated transition-all duration-300"
style={{ animationDelay: `${index * 0.1}s` }}
<div className="grid grid-cols-1 md:grid-cols-12 gap-6">
{features.map((f, i) => (
<motion.div
key={i}
whileHover={{ y: -5 }}
className={`${f.size} bento-card flex flex-col justify-between min-h-[300px] ${f.color === 'bg-brand' ? 'bg-brand' : ''}`}
>
<div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
<feature.icon className="w-6 h-6 text-primary" />
<div>
<div className={`w-14 h-14 ${f.color === 'bg-brand' ? 'bg-bg-primary/20' : f.color} rounded-2xl flex items-center justify-center mb-6`}>
<f.icon className={`${f.iconColor} w-8 h-8`} />
</div>
<h3 className={`text-2xl font-display font-bold mb-4 ${f.color === 'bg-brand' ? 'text-bg-primary' : 'text-text-primary'}`}>{f.title}</h3>
<p className={`text-lg ${f.color === 'bg-brand' ? 'text-bg-primary/70' : 'text-text-primary/70'}`}>
{f.desc}
</p>
</div>
<h3 className="text-xl font-display font-semibold text-foreground mb-2">
{feature.title}
</h3>
<p className="text-muted-foreground font-body leading-relaxed">
{feature.description}
</p>
</div>
</motion.div>
))}
</div>
</div>
+55 -27
View File
@@ -1,35 +1,63 @@
import logo from "@/assets/tailtrails-logo.png";
import { Heart } from 'lucide-react';
import { siTiktok, siFacebook, siInstagram } from 'simple-icons';
const Footer = () => {
return (
<footer className="py-12 bg-card border-t border-border/50">
<div className="container mx-auto px-6">
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
{/* Logo */}
<div className="flex items-center gap-3">
<img src={logo} alt="TailTrails" className="h-8 w-auto" />
<span className="font-display text-lg font-semibold text-foreground">
TailTrails
</span>
<footer className="py-20 border-t border-border-subtle">
<div className="max-w-7xl mx-auto px-6">
<div className="flex flex-col md:flex-row justify-between items-start gap-12 mb-16">
<div>
<div className="flex items-center gap-2 mb-6">
<div className="w-10 h-10 bg-brand rounded-full flex items-center justify-center">
<img src="/tailtrails-logo.png" alt="TailTrails Logo" className="w-6 h-6" />
</div>
<span className="text-2xl font-display font-bold tracking-tight text-text-primary">TailTrails</span>
</div>
<p className="text-text-secondary max-w-xs">
The Strava for Dog Lovers. Making every walk an adventure since 2026.
</p>
</div>
{/* Links */}
<nav className="flex items-center gap-6 text-sm font-body">
<a href="#features" className="text-muted-foreground hover:text-foreground transition-colors">
Features
</a>
<a href="#how-it-works" className="text-muted-foreground hover:text-foreground transition-colors">
How It Works
</a>
{/* <a href="#testimonials" className="text-muted-foreground hover:text-foreground transition-colors">
Reviews
</a> */}
</nav>
{/* Copyright */}
<p className="text-sm text-muted-foreground font-body">
© 2026 TailTrails. All rights reserved.
</p>
<div className="flex flex-wrap gap-12 md:gap-24">
<div>
<h4 className="font-bold mb-6 uppercase tracking-widest text-xs text-text-muted">Navigation</h4>
<ul className="space-y-4 font-medium">
<li><a href="#features" className="text-text-secondary hover:text-brand transition-colors">Features</a></li>
<li><a href="#how-it-works" className="text-text-secondary hover:text-brand transition-colors">How It Works</a></li>
<li><a href="/waitlist" className="text-text-secondary hover:text-brand transition-colors">Join Waitlist</a></li>
<li><a href="/privacy" className="text-text-secondary hover:text-brand transition-colors">Privacy Policy</a></li>
</ul>
</div>
<div>
<h4 className="font-bold mb-6 uppercase tracking-widest text-xs text-text-muted">Connect</h4>
<div className="flex gap-4">
<a href="https://www.tiktok.com/@packlifeinportugal" className="w-10 h-10 glass rounded-full flex items-center justify-center text-text-secondary hover:bg-brand hover:text-bg-primary transition-colors">
<svg className="w-5 h-5" viewBox="0 0 24 24" role="img" aria-hidden="true">
<path d={siTiktok.path} fill="currentColor" />
</svg>
</a>
<a href="https://www.facebook.com/packlifeinportugal" className="w-10 h-10 glass rounded-full flex items-center justify-center text-text-secondary hover:bg-brand hover:text-bg-primary transition-colors">
<svg className="w-5 h-5" viewBox="0 0 24 24" role="img" aria-hidden="true">
<path d={siFacebook.path} fill="currentColor" />
</svg>
</a>
<a href="https://www.instagram.com/packlifeinportugal" className="w-10 h-10 glass rounded-full flex items-center justify-center text-text-secondary hover:bg-brand hover:text-bg-primary transition-colors">
<svg className="w-5 h-5" viewBox="0 0 24 24" role="img" aria-hidden="true">
<path d={siInstagram.path} fill="currentColor" />
</svg>
</a>
</div>
</div>
</div>
</div>
<div className="pt-8 border-t border-border-subtle flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-text-muted text-sm">© 2026 TailTrails. All rights reserved.</p>
<div className="flex items-center gap-2 text-text-muted text-sm">
<span>Made with</span>
<Heart className="w-4 h-4 text-brand fill-brand" />
<span>for dog lovers.</span>
</div>
</div>
</div>
</footer>
+53 -34
View File
@@ -1,43 +1,62 @@
import { Button } from "@/components/ui/button";
import logo from "@/assets/tailtrails-logo.png";
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { Dog, Menu, X } from 'lucide-react';
const Header = () => {
return (
<header className="fixed top-0 left-0 right-0 z-50 bg-background/80 backdrop-blur-md border-b border-border/50">
<div className="container mx-auto px-6 h-16 flex items-center justify-between">
<div className="flex items-center gap-3">
<img src={logo} alt="TailTrails" className="h-10 w-auto" />
<span className="font-display text-xl font-semibold text-foreground">
TailTrails
</span>
</div>
<nav className="hidden md:flex items-center gap-8">
<a href="#features" className="text-muted-foreground hover:text-foreground transition-colors font-body">
Features
</a>
<a href="#how-it-works" className="text-muted-foreground hover:text-foreground transition-colors font-body">
How It Works
</a>
{/* <a href="#testimonials" className="text-muted-foreground hover:text-foreground transition-colors font-body">
Reviews
</a> */}
</nav>
const [isScrolled, setIsScrolled] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
<div className="flex items-center gap-3">
<Button variant="ghost" size="sm" asChild>
<a href="https://app.tailtrails.club" target="_blank" rel="noopener noreferrer">
Log In
useEffect(() => {
const handleScroll = () => setIsScrolled(window.scrollY > 20);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<nav className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${isScrolled ? 'py-4' : 'py-6'}`}>
<div className="max-w-7xl mx-auto px-6">
<div className={`glass rounded-full px-6 py-3 flex items-center justify-between transition-all duration-300 ${isScrolled ? 'shadow-lg bg-bg-secondary/80' : ''}`}>
<a href="/" className="flex items-center gap-2">
<div className="w-10 h-10 bg-brand rounded-full flex items-center justify-center">
<img src="/tailtrails-logo.png" alt="TailTrails Logo" className="w-6 h-6" />
</div>
<span className="text-xl font-display font-bold tracking-tight text-text-primary">TailTrails</span>
</a>
<div className="hidden md:flex items-center gap-8">
<a href="#features" className="text-sm font-medium text-text-muted hover:text-brand transition-colors">Features</a>
<a href="#how-it-works" className="text-sm font-medium text-text-muted hover:text-brand transition-colors">How It Works</a>
<a href="/blog" className="text-sm font-medium text-text-muted" onClick={() => setIsMenuOpen(false)}>Blogs</a>
<a href="/waitlist" className="bg-brand hover:bg-brand-light text-bg-primary px-6 py-2 rounded-full text-sm font-bold transition-all transform hover:scale-105 active:scale-95 inline-flex items-center justify-center">
Join Waitlist
</a>
</Button>
<Button variant="default" size="sm" asChild>
<a href="https://app.tailtrails.club/signup" target="_blank" rel="noopener noreferrer">
Get Started
</a>
</Button>
</div>
<button className="md:hidden text-text-primary" onClick={() => setIsMenuOpen(!isMenuOpen)}>
{isMenuOpen ? <X /> : <Menu />}
</button>
</div>
</div>
</header>
<AnimatePresence>
{isMenuOpen && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="absolute top-full left-6 right-6 mt-4 glass rounded-3xl p-6 md:hidden"
>
<div className="flex flex-col gap-4">
<a href="#features" className="text-lg font-medium text-text-muted" onClick={() => setIsMenuOpen(false)}>Features</a>
<a href="#how-it-works" className="text-lg font-medium text-text-muted" onClick={() => setIsMenuOpen(false)}>How It Works</a>
<a href="/waitlist" className="bg-brand text-bg-primary w-full py-4 rounded-2xl font-bold inline-flex items-center justify-center" onClick={() => setIsMenuOpen(false)}>
Join Waitlist
</a>
</div>
</motion.div>
)}
</AnimatePresence>
</nav>
);
};
+54 -147
View File
@@ -1,85 +1,66 @@
import React, { useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { MapPin, Share2, Heart } from "lucide-react";
import { motion } from 'motion/react';
import { Zap, Facebook, Instagram } from 'lucide-react';
import PhoneMockup from './PhoneMockup';
import { siFacebook, siInstagram, siTiktok } from 'simple-icons';
const Hero = () => {
return (
<section className="relative min-h-screen flex items-center justify-center pt-16 overflow-hidden">
{/* Background decoration */}
<div className="absolute inset-0 gradient-hero" />
<div className="absolute top-1/4 -right-32 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 -left-32 w-80 h-80 bg-accent/10 rounded-full blur-3xl" />
<div className="container mx-auto px-6 py-20 relative z-10">
<div className="max-w-4xl mx-auto text-center">
{/* Badge */}
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 text-primary mb-8 animate-fade-up opacity-0">
<span className="w-2 h-2 rounded-full bg-primary animate-pulse-soft" />
<span className="text-sm font-medium font-body">The Strava for Dog Lovers</span>
</div>
<section className="relative pt-32 pb-20 overflow-hidden">
<div className="max-w-7xl mx-auto px-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
<motion.div
initial={{ opacity: 0, x: -30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8 }}
className="text-left"
>
<div className="inline-flex items-center gap-2 bg-bg-secondary px-4 py-2 rounded-full mb-8 border border-border-subtle">
<span className="text-xs font-bold uppercase tracking-widest text-accent-secondary">Life Unleashed</span>
</div>
{/* Main headline */}
<h1 className="text-5xl md:text-6xl lg:text-7xl font-display font-bold text-foreground leading-tight mb-6 animate-fade-up opacity-0 stagger-1 text-balance">
Every Walk Becomes an{" "}
<span className="text-primary">Adventure</span>
</h1>
<h1 className="text-6xl md:text-7xl font-display font-extrabold tracking-tight mb-8 leading-[1.05] text-text-primary">
Every walk <br />
becomes <br />
<span className="text-accent-secondary italic">an adventure.</span>
</h1>
{/* Subheadline */}
<p className="text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto mb-10 font-body animate-fade-up opacity-0 stagger-2 text-balance">
Record your dog walks, discover new routes, and connect with fellow dog owners.
Track every tail wag, share your favorite trails, and build a community of happy pups.
</p>
<p className="text-text-primary/70 text-lg md:text-xl max-w-xl mb-12 font-medium leading-relaxed">
Discover new trails that match your dog's needs, and turn everyday walks into real adventures.
</p>
{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-16 animate-fade-up opacity-0 stagger-3">
<Button variant="hero" size="xl" asChild>
<a href="https://app.tailtrails.club" target="_blank" rel="noopener noreferrer">
Start Walking Free
<div className="flex flex-col sm:flex-row items-center gap-6 mb-12">
<a href="/waitlist" className="w-full sm:w-auto bg-brand hover:bg-brand-light text-bg-primary px-10 py-5 rounded-2xl font-extrabold text-lg transition-all transform hover:scale-105 active:scale-95 flex items-center justify-center gap-2">
Join the Waitlist
</a>
</Button>
<Button variant="hero-outline" size="xl" asChild>
<a href="#how-it-works">
See How It Works
</div>
<div className="hidden sm:flex items-center gap-6">
<a href="https://www.tiktok.com/@packlifeinportugal" className="w-10 h-10 glass rounded-full flex items-center justify-center text-text-secondary hover:bg-brand hover:text-bg-primary transition-colors">
<svg className="w-5 h-5" viewBox="0 0 24 24" role="img" aria-hidden="true">
<path d={siTiktok.path} fill="currentColor" />
</svg>
</a>
</Button>
</div>
<a href="https://www.facebook.com/packlifeinportugal" className="w-10 h-10 glass rounded-full flex items-center justify-center text-text-secondary hover:bg-brand hover:text-bg-primary transition-colors">
<svg className="w-5 h-5" viewBox="0 0 24 24" role="img" aria-hidden="true">
<path d={siFacebook.path} fill="currentColor" />
</svg>
</a>
<a href="https://www.instagram.com/packlifeinportugal" className="w-10 h-10 glass rounded-full flex items-center justify-center text-text-secondary hover:bg-brand hover:text-bg-primary transition-colors">
<svg className="w-5 h-5" viewBox="0 0 24 24" role="img" aria-hidden="true">
<path d={siInstagram.path} fill="currentColor" />
</svg>
</a>
</div>
</motion.div>
{/* Feature pills */}
<div className="flex flex-wrap items-center justify-center gap-4 animate-fade-up opacity-0 stagger-4">
<div className="flex items-center gap-2 px-4 py-2 rounded-full bg-card shadow-soft">
<MapPin className="w-4 h-4 text-primary" />
<span className="text-sm font-body text-foreground">GPS Tracking</span>
</div>
<div className="flex items-center gap-2 px-4 py-2 rounded-full bg-card shadow-soft">
<Share2 className="w-4 h-4 text-primary" />
<span className="text-sm font-body text-foreground">Share Routes</span>
</div>
<div className="flex items-center gap-2 px-4 py-2 rounded-full bg-card shadow-soft">
<Heart className="w-4 h-4 text-primary" />
<span className="text-sm font-body text-foreground">Dog-Friendly Labels</span>
</div>
</div>
</div>
{/* Hero image/illustration area (now renders lazy-loading video preview) */}
<div className="mt-16 relative max-w-5xl mx-auto animate-fade-up opacity-0 stagger-5">
<div className="relative rounded-2xl overflow-hidden shadow-elevated bg-card p-2">
<div className="aspect-[16/9] rounded-xl bg-gradient-to-br from-cream-dark to-cream overflow-hidden flex items-center justify-center">
{/* Video preview container */}
<VideoPreview />
</div>
</div>
{/* Floating stats */}
<div className="absolute -left-4 top-1/3 bg-card rounded-xl p-4 shadow-elevated animate-float hidden lg:block">
<div className="text-2xl font-display font-bold text-primary">2.5k</div>
<div className="text-sm text-muted-foreground font-body">Active Walkers</div>
</div>
<div className="absolute -right-4 top-1/2 bg-card rounded-xl p-4 shadow-elevated animate-float hidden lg:block" style={{ animationDelay: '0.5s' }}>
<div className="text-2xl font-display font-bold text-primary">15k+</div>
<div className="text-sm text-muted-foreground font-body">Routes Shared</div>
</div>
<motion.div
initial={{ opacity: 0, x: 30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
className="relative"
>
<PhoneMockup />
</motion.div>
</div>
</div>
</section>
@@ -87,77 +68,3 @@ const Hero = () => {
};
export default Hero;
// Simple inline video preview component. Uses IntersectionObserver to lazy-load
// the source only when the element is visible. Uses /hero.mp4 in the public folder.
function VideoPreview() {
const ref = useRef<HTMLVideoElement | null>(null);
const [visible, setVisible] = useState(false);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) setVisible(true);
});
},
{ threshold: 0.25 }
);
io.observe(el);
return () => io.disconnect();
}, []);
useEffect(() => {
if (!visible || loaded || !ref.current) return;
const v = ref.current;
// Create sources dynamically to prevent early download
const webmSrc = "/hero.webm"; // optional: if you generate a webm
const mp4Src = "/hero.mp4";
// prefer webm if available (browser will pick compatible source)
if (webmSrc) {
const s1 = document.createElement("source");
s1.src = webmSrc;
s1.type = "video/webm";
v.appendChild(s1);
}
const s2 = document.createElement("source");
s2.src = mp4Src;
s2.type = "video/mp4";
v.appendChild(s2);
setLoaded(true);
v.load();
v.play().catch(() => {
// autoplay may be blocked; that's fine
});
}, [visible, loaded]);
const toggle = () => {
const v = ref.current;
if (!v) return;
if (v.paused) v.play().catch(() => {});
else v.pause();
};
return (
<video
ref={ref}
poster="/hero-poster.jpg"
muted
loop
playsInline
autoPlay
preload="none"
onClick={toggle}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
aria-label="App preview"
/>
);
}
+50 -66
View File
@@ -1,76 +1,60 @@
import { Download, Play, Share2, Heart } from "lucide-react";
const steps = [
{
icon: Download,
step: "01",
title: "Sign Up",
description: "Create your free account in seconds. Add your dog's profile and you're ready to go.",
},
{
icon: Play,
step: "02",
title: "Start Recording",
description: "Hit record when you head out. The app tracks your route, distance, and time automatically.",
},
{
icon: Share2,
step: "03",
title: "Share & Tag",
description: "Add photos, labels, and notes. Share with the community or keep it for yourself.",
},
{
icon: Heart,
step: "04",
title: "Discover & Connect",
description: "Explore routes shared by others. Find dog-friendly spots and make walking buddies.",
},
];
import { UserPlus, Play, Image, Search } from 'lucide-react';
const HowItWorks = () => {
const steps = [
{
num: "01",
title: "Sign Up",
desc: "Create your free account in seconds. Add your dog's profile and you're ready to go.",
icon: UserPlus
},
{
num: "02",
title: "Start Recording",
desc: "Hit record when you head out. The app tracks your route, distance, and time automatically.",
icon: Play
},
{
num: "03",
title: "Share & Tag",
desc: "Add photos, labels, and notes. Share with the community or keep it for yourself.",
icon: Image
},
{
num: "04",
title: "Discover & Connect",
desc: "Explore routes shared by others. Find dog-friendly spots and make walking buddies.",
icon: Search
}
];
return (
<section id="how-it-works" className="py-24 bg-background relative">
<div className="container mx-auto px-6">
{/* Section header */}
<div className="text-center max-w-2xl mx-auto mb-16">
<span className="inline-block px-4 py-1.5 rounded-full bg-primary/10 text-primary text-sm font-medium font-body mb-4">
How It Works
</span>
<h2 className="text-4xl md:text-5xl font-display font-bold text-foreground mb-4">
Start Your Adventure in Minutes
</h2>
<p className="text-lg text-muted-foreground font-body">
Getting started with TailTrails is as easy as going for a walk.
</p>
<section id="how-it-works" className="py-20 relative overflow-hidden scroll-mt-18">
<div className="max-w-7xl mx-auto px-6">
<div className="text-center mb-16">
<h2 className="text-4xl md:text-5xl font-display font-bold mb-4 text-text-primary">Start Your Adventure in Minutes</h2>
<p className="text-text-secondary font-medium">Getting started with TailTrails is as easy as going for a walk.</p>
</div>
{/* Steps */}
<div className="max-w-4xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{steps.map((step, index) => (
<div
key={step.step}
className="relative p-6 rounded-2xl bg-card border border-border/50 hover:shadow-elevated transition-all duration-300"
>
{/* Step number */}
<div className="absolute -top-3 -left-3 w-10 h-10 rounded-full bg-primary flex items-center justify-center shadow-soft">
<span className="text-sm font-bold text-primary-foreground font-body">{step.step}</span>
</div>
<div className="ml-4">
<div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center mb-4">
<step.icon className="w-6 h-6 text-primary" />
</div>
<h3 className="text-xl font-display font-semibold text-foreground mb-2">
{step.title}
</h3>
<p className="text-muted-foreground font-body leading-relaxed">
{step.description}
</p>
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{steps.map((step, i) => (
<div key={i} className="relative group">
<div className="mb-6 flex items-end gap-4">
<span className="text-6xl font-display font-black text-white/10 group-hover:text-brand/30 transition-colors duration-500 leading-none">
{step.num}
</span>
<div className="w-12 h-12 glass rounded-xl flex items-center justify-center mb-1 border-accent-secondary/20">
<step.icon className="text-brand w-6 h-6" />
</div>
</div>
))}
</div>
<h4 className="text-xl font-bold mb-2 text-text-primary">{step.title}</h4>
<p className="text-text-primary/70 leading-relaxed">{step.desc}</p>
{i < steps.length - 1 && (
<div className="hidden md:block absolute top-12 -right-4 w-8 h-[1px] bg-white/10" />
)}
</div>
))}
</div>
</div>
</section>
+89
View File
@@ -0,0 +1,89 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { Facebook, Instagram, ShieldCheck, Users } from 'lucide-react';
const PhoneMockup = () => {
const [currentIndex, setCurrentIndex] = useState(0);
const images = [
"IMG_2721.PNG",
"IMG_2722.PNG",
"IMG_2724.PNG",
"IMG_2725.PNG"
];
useEffect(() => {
const timer = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % images.length);
}, 5000);
return () => clearInterval(timer);
}, []);
return (
<div className="relative w-[280px] h-[580px] md:w-[320px] md:h-[650px] mx-auto">
{/* Phone Frame */}
<div className="absolute inset-0 bg-zinc-900 rounded-[3rem] border-[8px] border-bg-variant shadow-2xl overflow-hidden z-20">
{/* Screen Content */}
<div className="relative w-full h-full bg-bg-primary overflow-hidden">
<AnimatePresence mode="wait">
<motion.img
key={currentIndex}
src={images[currentIndex]}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{ duration: 0.5 }}
className="w-full h-full object-cover"
referrerPolicy="no-referrer"
/>
</AnimatePresence>
</div>
</div>
{/* Floating Overlays */}
<motion.div
animate={{ y: [0, -10, 0] }}
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
className="absolute -top-6 -right-12 md:-right-20 z-30 bg-bg-primary/80 p-4 rounded-2xl shadow-xl flex items-center gap-3 min-w-[180px] border border-border-subtle/60"
>
<div className="w-10 h-10 rounded-full bg-accent-secondary/20 flex items-center justify-center">
<ShieldCheck className="text-accent-secondary w-6 h-6" />
</div>
<div className="text-left">
<p className="text-[10px] font-bold text-text-secondary uppercase tracking-wider">Status</p>
<p className="text-sm font-bold text-text-primary">Added to 'Sunset Loop'</p>
</div>
</motion.div>
<motion.div
animate={{ y: [0, 10, 0] }}
transition={{ duration: 5, repeat: Infinity, ease: "easeInOut", delay: 0.5 }}
className="absolute top-1/2 -left-12 md:-left-24 z-30 bg-bg-primary/80 p-4 rounded-2xl shadow-xl flex items-center gap-3 min-w-[180px] border border-border-subtle/60"
>
<div className="w-10 h-10 rounded-full bg-accent-primary/20 flex items-center justify-center">
<Users className="text-accent-primary w-6 h-6" />
</div>
<div className="text-left">
<p className="text-[10px] font-bold text-text-secondary uppercase tracking-wider">New Collectibles</p>
<p className="text-sm font-bold text-text-primary">+3 this week</p>
</div>
</motion.div>
<motion.div
animate={{ y: [0, -8, 0] }}
transition={{ duration: 4.5, repeat: Infinity, ease: "easeInOut", delay: 1 }}
className="absolute -bottom-6 -right-8 md:-right-16 z-30 bg-bg-primary/80 p-4 rounded-2xl shadow-xl flex items-center gap-3 min-w-[180px] border border-border-subtle/60"
>
<div className="w-2 h-2 rounded-full bg-accent-secondary animate-pulse" />
<div className="text-left">
<p className="text-[10px] font-bold text-text-secondary uppercase tracking-wider">Live Walk</p>
<p className="text-sm font-bold text-text-primary">4.2km <span className="text-text-secondary font-normal text-xs">Tracked</span></p>
</div>
</motion.div>
{/* Background Circle Decor */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[140%] aspect-square border border-border-subtle/30 rounded-full -z-10" />
</div>
);
};
export default PhoneMockup;
+170
View File
@@ -0,0 +1,170 @@
import React from 'react';
import { motion } from 'motion/react';
const ProblemSolution = () => {
// Animation variants for the parent container (triggers cascading/staggered effects)
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.2, // Time gap between card animations
},
},
};
// Animation variants for individual cards
const cardVariants = {
hidden: { opacity: 0, y: 40 },
visible: {
opacity: 1,
y: 0,
transition: { type: "spring", stiffness: 100, damping: 15 },
},
};
return (
<section id="problem-solution" className="py-20 bg-bg-secondary overflow-hidden">
<div className="max-w-7xl mx-auto px-6">
{/* Animated Header Section */}
<motion.div
initial={{ opacity: 0, y: -20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<h2 className="text-4xl md:text-5xl font-display font-bold mb-4 text-text-primary">
Finding the Right Walk for Your Dog
</h2>
<p className="text-text-secondary font-medium max-w-3xl mx-auto">
Understanding the challenge and how TailTrails solves it
</p>
</motion.div>
{/* Grid Container coordinating the card entries */}
<motion.div
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-stretch" // "items-stretch" forces equal height
>
{/* Problem Card */}
<motion.div
variants={cardVariants}
whileHover={{ y: -6 }} // Subtle interactive lift on hover
className="bg-bg-primary rounded-3xl border border-border-subtle p-8 shadow-lg flex flex-col justify-between"
>
<div>
<div className="mb-6 overflow-hidden rounded-xl">
<motion.img
whileHover={{ scale: 1.03 }} // Subtle zoom effect inside the card container
transition={{ duration: 0.4 }}
src="/landing/sad-dog.avif"
alt="Problem with dog-friendly places"
className="w-full h-80 object-cover"
/>
</div>
<div className="flex items-center mb-6">
<div className="w-12 h-12 rounded-xl bg-red-500/10 flex items-center justify-center mr-4 shrink-0">
<span className="text-red-500 font-bold text-lg">!</span>
</div>
<h3 className="text-2xl font-bold text-text-primary">The Problem</h3>
</div>
<p className="text-text-primary font-medium mb-4 text-lg">
Most "dog-friendly" places aren't actually right for your dog.
</p>
<ul className="mb-6 space-y-3">
<li className="flex items-start">
<span className="text-red-500 mr-3 mt-0.5 font-bold"></span>
<span className="text-text-secondary leading-relaxed">Too busy. Too unpredictable. Too many dogs. Not enough space.</span>
</li>
<li className="flex items-start">
<span className="text-red-500 mr-3 mt-0.5 font-bold"></span>
<span className="text-text-secondary leading-relaxed">Or the opposite; too quiet, too short, too much of the same thing.</span>
</li>
</ul>
<p className="text-text-primary font-medium mb-3">
So you either:
</p>
<ul className="mb-6 space-y-3">
<li className="flex items-start">
<span className="text-red-500 mr-3 mt-0.5 font-bold"></span>
<span className="text-text-secondary leading-relaxed">Play it safe and stick to the same walk</span>
</li>
<li className="flex items-start">
<span className="text-red-500 mr-3 mt-0.5 font-bold"></span>
<span className="text-text-secondary leading-relaxed">Or take a risk and hope it works out</span>
</li>
</ul>
</div>
<p className="text-red-500 font-semibold pt-4 border-t border-border-subtle/50">
Neither feels great.
</p>
</motion.div>
{/* Solution Card */}
<motion.div
variants={cardVariants}
whileHover={{ y: -6 }}
className="bg-bg-primary rounded-3xl border border-border-subtle p-8 shadow-lg flex flex-col justify-between"
>
<div>
<div className="mb-6 overflow-hidden rounded-xl">
<motion.img
whileHover={{ scale: 1.03 }}
transition={{ duration: 0.4 }}
src="/landing/happy-dog.avif"
alt="TailTrails solution"
className="w-full h-80 object-cover" // Synced height with Problem image for visual symmetry
/>
</div>
<div className="flex items-center mb-6">
<div className="w-12 h-12 rounded-xl bg-green-500/10 flex items-center justify-center mr-4 shrink-0">
<span className="text-green-500 font-bold text-lg"></span>
</div>
<h3 className="text-2xl font-bold text-text-primary">The Solution</h3>
</div>
<p className="text-text-primary font-medium mb-4 text-lg">
TailTrails filters the world around your dog.
</p>
<ul className="mb-6 space-y-3">
<li className="flex items-start">
<span className="text-green-500 mr-3 mt-0.5 font-bold"></span>
<span className="text-text-secondary leading-relaxed">Every trail is tagged - environment, terrain, traffic, exposure.</span>
</li>
<li className="flex items-start">
<span className="text-green-500 mr-3 mt-0.5 font-bold"></span>
<span className="text-text-secondary leading-relaxed">Every dog is different - energy, behaviour, needs.</span>
</li>
</ul>
<p className="text-text-primary font-medium mb-4">
We bring the two together.
</p>
</div>
<p className="text-brand font-semibold pt-4 border-t border-border-subtle/50">
So you can choose walks that bring out the best in your dog.
</p>
</motion.div>
</motion.div>
</div>
</section>
);
};
export default ProblemSolution;
-76
View File
@@ -1,76 +0,0 @@
import { Star } from "lucide-react";
const testimonials = [
{
name: "Sarah M.",
dog: "with Buddy, Golden Retriever",
quote: "TailTrails has completely changed our walking routine! We've discovered so many new trails in our area that are perfect for older dogs like Buddy.",
rating: 5,
},
{
name: "James K.",
dog: "with Luna, Border Collie",
quote: "Love the labeling feature! I can easily find off-leash areas and routes with water access. Luna approves!",
rating: 5,
},
{
name: "Emily R.",
dog: "with Max & Charlie",
quote: "Finally, an app that understands what dog owners need. The community is amazing, and I've made so many walking buddies.",
rating: 5,
},
];
const Testimonials = () => {
return (
<section id="testimonials" className="py-24 bg-card relative overflow-hidden">
{/* Background decoration */}
<div className="absolute bottom-0 right-0 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
<div className="container mx-auto px-6 relative z-10">
{/* Section header */}
<div className="text-center max-w-2xl mx-auto mb-16">
<span className="inline-block px-4 py-1.5 rounded-full bg-primary/10 text-primary text-sm font-medium font-body mb-4">
Reviews
</span>
<h2 className="text-4xl md:text-5xl font-display font-bold text-foreground mb-4">
Loved by Dog Owners Everywhere
</h2>
<p className="text-lg text-muted-foreground font-body">
See what our community of happy walkers has to say.
</p>
</div>
{/* Testimonials grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto">
{testimonials.map((testimonial, index) => (
<div
key={testimonial.name}
className="p-6 rounded-2xl bg-background border border-border/50 hover:shadow-elevated transition-all duration-300"
>
{/* Rating */}
<div className="flex items-center gap-1 mb-4">
{Array.from({ length: testimonial.rating }).map((_, i) => (
<Star key={i} className="w-4 h-4 fill-amber text-amber" />
))}
</div>
{/* Quote */}
<p className="text-foreground font-body leading-relaxed mb-6">
"{testimonial.quote}"
</p>
{/* Author */}
<div>
<p className="font-display font-semibold text-foreground">{testimonial.name}</p>
<p className="text-sm text-muted-foreground font-body">{testimonial.dog}</p>
</div>
</div>
))}
</div>
</div>
</section>
);
};
export default Testimonials;
+84
View File
@@ -0,0 +1,84 @@
import React, { useEffect, useState } from "react";
declare global {
interface Window {
dataLayer?: any[];
gtag?: (...args: any[]) => void;
}
}
const STORAGE_KEY = "tt_cookie_consent";
function loadGtag(id: string) {
if (!id) return;
if (document.querySelector(`script[data-gtag="${id}"]`)) return;
const script = document.createElement("script");
script.async = true;
script.src = `https://www.googletagmanager.com/gtag/js?id=${id}`;
script.setAttribute("data-gtag", id);
document.head.appendChild(script);
window.dataLayer = window.dataLayer || [];
window.gtag = function () {
// @ts-ignore - forward arguments to the dataLayer
window.dataLayer!.push(arguments);
};
window.gtag("js", new Date());
window.gtag("config", id, { anonymize_ip: true });
}
const CookieConsent: React.FC = () => {
const [consent, setConsent] = useState<string | null>(null);
useEffect(() => {
const v = localStorage.getItem(STORAGE_KEY);
setConsent(v);
if (v === "granted") {
const id = (import.meta.env as any).VITE_GA_ID as string | undefined;
if (id) loadGtag(id);
}
}, []);
function accept() {
localStorage.setItem(STORAGE_KEY, "granted");
setConsent("granted");
const id = (import.meta.env as any).VITE_GA_ID as string | undefined;
if (id) loadGtag(id);
}
function deny() {
localStorage.setItem(STORAGE_KEY, "denied");
setConsent("denied");
}
if (consent !== null) return null;
return (
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 z-50 pointer-events-none">
<div className="pointer-events-auto bento-card rounded-lg p-2 md:p-3 w-full max-w-[18rem] md:max-w-xs flex items-center gap-2 md:gap-3 shadow-sm text-xs">
<div className="text-xs text-text-secondary text-center md:text-left flex-1 leading-tight">
We use cookies for analytics.
</div>
<div className="flex items-center gap-2 md:gap-2">
<button
onClick={accept}
className="bg-brand hover:bg-brand-light text-bg-primary px-2 py-1 rounded-full text-xs font-medium transition-all"
>
Yes
</button>
<button
onClick={deny}
className="border border-border-subtle text-text-muted bg-transparent px-2 py-1 rounded-full text-xs transition-colors"
>
No
</button>
</div>
</div>
</div>
);
};
export default CookieConsent;
+31
View File
@@ -0,0 +1,31 @@
import * as React from "react";
import { useToast } from "@/components/ui/use-toast";
import { Toast, ToastTitle, ToastDescription, ToastClose } from "@/components/ui/toast";
const Toaster: React.FC = () => {
const { toasts } = useToast();
return (
<>
{toasts.map((t) => (
<Toast
key={t.id}
open={t.open}
onOpenChange={(open) => t.onOpenChange?.(open)}
variant={(t as any).variant}
>
<div className="flex-1">
{t.title && <ToastTitle>{t.title}</ToastTitle>}
{t.description && <ToastDescription>{t.description}</ToastDescription>}
</div>
{t.action}
<ToastClose />
</Toast>
))}
</>
);
};
export default Toaster;
-52
View File
@@ -1,52 +0,0 @@
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
-104
View File
@@ -1,104 +0,0 @@
import * as React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
const AlertDialog = AlertDialogPrimitive.Root;
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
const AlertDialogPortal = AlertDialogPrimitive.Portal;
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
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}
ref={ref}
/>
));
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.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}
/>
</AlertDialogPortal>
));
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
AlertDialogHeader.displayName = "AlertDialogHeader";
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
);
AlertDialogFooter.displayName = "AlertDialogFooter";
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
));
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
));
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
{...props}
/>
));
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};
-43
View File
@@ -1,43 +0,0 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} />
),
);
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
),
);
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription };
-5
View File
@@ -1,5 +0,0 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
const AspectRatio = AspectRatioPrimitive.Root;
export { AspectRatio };
-38
View File
@@ -1,38 +0,0 @@
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image ref={ref} className={cn("aspect-square h-full w-full", className)} {...props} />
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };
-29
View File
@@ -1,29 +0,0 @@
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-full 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 hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground 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 };
-90
View File
@@ -1,90 +0,0 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className,
)}
{...props}
/>
),
);
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
({ className, ...props }, ref) => (
<li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
),
);
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return <Comp ref={ref} className={cn("transition-colors hover:text-foreground", className)} {...props} />;
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
),
);
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
<li role="presentation" aria-hidden="true" className={cn("[&>svg]:size-3.5", className)} {...props}>
{children ?? <ChevronRight />}
</li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};
-50
View File
@@ -1,50 +0,0 @@
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-lg text-sm font-medium ring-offset-background transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 font-body",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-olive-dark shadow-soft hover:shadow-elevated",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border-2 border-primary bg-transparent text-primary hover:bg-primary hover:text-primary-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-cream-dark",
ghost: "hover:bg-secondary hover:text-secondary-foreground",
link: "text-primary underline-offset-4 hover:underline",
hero: "bg-primary text-primary-foreground hover:bg-olive-dark shadow-elevated hover:shadow-glow text-base",
"hero-outline": "border-2 border-primary bg-transparent text-primary hover:bg-primary hover:text-primary-foreground text-base",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-12 rounded-lg px-8",
xl: "h-14 rounded-xl px-10 text-base",
icon: "h-10 w-10",
},
},
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 };
-54
View File
@@ -1,54 +0,0 @@
import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(buttonVariants({ variant: "ghost" }), "h-9 w-9 p-0 font-normal aria-selected:opacity-100"),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ..._props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ..._props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar";
export { Calendar };
-43
View File
@@ -1,43 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
),
);
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
),
);
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
),
);
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />,
);
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
),
);
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
-224
View File
@@ -1,224 +0,0 @@
import * as React from "react";
import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
const Carousel = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & CarouselProps>(
({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins,
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return;
}
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext],
);
React.useEffect(() => {
if (!api || !setApi) {
return;
}
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) {
return;
}
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);
return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
},
);
Carousel.displayName = "Carousel";
const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel();
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn("flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className)}
{...props}
/>
</div>
);
},
);
CarouselContent.displayName = "CarouselContent";
const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const { orientation } = useCarousel();
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn("min-w-0 shrink-0 grow-0 basis-full", orientation === "horizontal" ? "pl-4" : "pt-4", className)}
{...props}
/>
);
},
);
CarouselItem.displayName = "CarouselItem";
const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
);
},
);
CarouselPrevious.displayName = "CarouselPrevious";
const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
);
},
);
CarouselNext.displayName = "CarouselNext";
export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };
-303
View File
@@ -1,303 +0,0 @@
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> });
};
type ChartContextProps = {
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig;
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className,
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
});
ChartContainer.displayName = "Chart";
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`,
)
.join("\n"),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref,
) => {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item.dataKey || item.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>;
}
if (!value) {
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className,
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center",
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
})}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center",
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
},
);
ChartTooltipContent.displayName = "ChartTooltip";
const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
}
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
const { config } = useChart();
if (!payload?.length) {
return null;
}
return (
<div
ref={ref}
className={cn("flex items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn("flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground")}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
});
ChartLegendContent.displayName = "ChartLegend";
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload =
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
}
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
}
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle };
-26
View File
@@ -1,26 +0,0 @@
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };
-9
View File
@@ -1,9 +0,0 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
-132
View File
@@ -1,132 +0,0 @@
import * as React from "react";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import { cn } from "@/lib/utils";
import { Dialog, DialogContent } from "@/components/ui/dialog";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className,
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />);
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className,
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className,
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};
-178
View File
@@ -1,178 +0,0 @@
import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const ContextMenu = ContextMenuPrimitive.Root;
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
const ContextMenuGroup = ContextMenuPrimitive.Group;
const ContextMenuPortal = ContextMenuPrimitive.Portal;
const ContextMenuSub = ContextMenuPrimitive.Sub;
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
));
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
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}
/>
));
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 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}
/>
</ContextMenuPrimitive.Portal>
));
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
));
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.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 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
));
ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.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 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
));
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold text-foreground", inset && "pl-8", className)}
{...props}
/>
));
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} />
));
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
};
ContextMenuShortcut.displayName = "ContextMenuShortcut";
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};
-95
View File
@@ -1,95 +0,0 @@
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 data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<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,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
-87
View File
@@ -1,87 +0,0 @@
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,
};
-179
View File
@@ -1,179 +0,0 @@
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 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent focus:bg-accent",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</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 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
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 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
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 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
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,
};
-129
View File
@@ -1,129 +0,0 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from "react-hook-form";
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
},
);
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return <Label ref={ref} className={cn(error && "text-destructive", className)} htmlFor={formItemId} {...props} />;
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
);
},
);
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return <p ref={ref} id={formDescriptionId} className={cn("text-sm text-muted-foreground", className)} {...props} />;
},
);
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p ref={ref} id={formMessageId} className={cn("text-sm font-medium text-destructive", className)} {...props}>
{body}
</p>
);
},
);
FormMessage.displayName = "FormMessage";
export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };
-27
View File
@@ -1,27 +0,0 @@
import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "@/lib/utils";
const HoverCard = HoverCardPrimitive.Root;
const HoverCardTrigger = HoverCardPrimitive.Trigger;
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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}
/>
));
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
export { HoverCard, HoverCardTrigger, HoverCardContent };
-61
View File
@@ -1,61 +0,0 @@
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp";
import { Dot } from "lucide-react";
import { cn } from "@/lib/utils";
const InputOTP = React.forwardRef<React.ElementRef<typeof OTPInput>, React.ComponentPropsWithoutRef<typeof OTPInput>>(
({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn("flex items-center gap-2 has-[:disabled]:opacity-50", containerClassName)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
),
);
InputOTP.displayName = "InputOTP";
const InputOTPGroup = React.forwardRef<React.ElementRef<"div">, React.ComponentPropsWithoutRef<"div">>(
({ className, ...props }, ref) => <div ref={ref} className={cn("flex items-center", className)} {...props} />,
);
InputOTPGroup.displayName = "InputOTPGroup";
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
return (
<div
ref={ref}
className={cn(
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background",
className,
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink h-4 w-px bg-foreground duration-1000" />
</div>
)}
</div>
);
});
InputOTPSlot.displayName = "InputOTPSlot";
const InputOTPSeparator = React.forwardRef<React.ElementRef<"div">, React.ComponentPropsWithoutRef<"div">>(
({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
),
);
InputOTPSeparator.displayName = "InputOTPSeparator";
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
-22
View File
@@ -1,22 +0,0 @@
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-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background 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-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };
-17
View File
@@ -1,17 +0,0 @@
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 };
-207
View File
@@ -1,207 +0,0 @@
import * as React from "react";
import * as MenubarPrimitive from "@radix-ui/react-menubar";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const MenubarMenu = MenubarPrimitive.Menu;
const MenubarGroup = MenubarPrimitive.Group;
const MenubarPortal = MenubarPrimitive.Portal;
const MenubarSub = MenubarPrimitive.Sub;
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn("flex h-10 items-center space-x-1 rounded-md border bg-background p-1", className)}
{...props}
/>
));
Menubar.displayName = MenubarPrimitive.Root.displayName;
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
/>
));
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[state=open]:bg-accent data-[state=open]:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
));
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground 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}
/>
));
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(({ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, ref) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in 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}
/>
</MenubarPrimitive.Portal>
));
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
));
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.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 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
));
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.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 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
));
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
));
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
));
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
};
MenubarShortcut.displayname = "MenubarShortcut";
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
};
-120
View File
@@ -1,120 +0,0 @@
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center", className)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn("group flex flex-1 list-none items-center justify-center space-x-1", className)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto",
className,
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className,
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};
-81
View File
@@ -1,81 +0,0 @@
import * as React from "react";
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
import { ButtonProps, buttonVariants } from "@/components/ui/button";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
);
Pagination.displayName = "Pagination";
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
({ className, ...props }, ref) => (
<ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
),
);
PaginationContent.displayName = "PaginationContent";
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
));
PaginationItem.displayName = "PaginationItem";
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">;
const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className,
)}
{...props}
/>
);
PaginationLink.displayName = "PaginationLink";
const PaginationPrevious = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink aria-label="Go to previous page" size="default" className={cn("gap-1 pl-2.5", className)} {...props}>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
);
PaginationPrevious.displayName = "PaginationPrevious";
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink aria-label="Go to next page" size="default" className={cn("gap-1 pr-2.5", className)} {...props}>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
PaginationNext.displayName = "PaginationNext";
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span aria-hidden className={cn("flex h-9 w-9 items-center justify-center", className)} {...props}>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
);
PaginationEllipsis.displayName = "PaginationEllipsis";
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
};
-29
View File
@@ -1,29 +0,0 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };
-23
View File
@@ -1,23 +0,0 @@
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils";
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };
-36
View File
@@ -1,36 +0,0 @@
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
});
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
});
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioGroup, RadioGroupItem };
-37
View File
@@ -1,37 +0,0 @@
import { GripVertical } from "lucide-react";
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@/lib/utils";
const ResizablePanelGroup = ({ className, ...props }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn("flex h-full w-full data-[panel-group-direction=vertical]:flex-col", className)}
{...props}
/>
);
const ResizablePanel = ResizablePrimitive.Panel;
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean;
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className,
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
);
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
-38
View File
@@ -1,38 +0,0 @@
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root ref={ref} className={cn("relative overflow-hidden", className)} {...props}>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };
-143
View File
@@ -1,143 +0,0 @@
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover 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",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label ref={ref} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} />
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};
-20
View File
@@ -1,20 +0,0 @@
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 };
-107
View File
@@ -1,107 +0,0 @@
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
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}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
},
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(
({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-secondary hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
),
);
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title ref={ref} className={cn("text-lg font-semibold text-foreground", className)} {...props} />
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetOverlay,
SheetPortal,
SheetTitle,
SheetTrigger,
};
-637
View File
@@ -1,637 +0,0 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { VariantProps, cva } from "class-variance-authority";
import { PanelLeft } from "lucide-react";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Sheet, SheetContent } from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
const SIDEBAR_COOKIE_NAME = "sidebar:state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContext = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContext | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context;
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
>(({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }, ref) => {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open],
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContext>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
);
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn("group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar", className)}
ref={ref}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
});
SidebarProvider.displayName = "SidebarProvider";
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}
>(({ side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, ...props }, ref) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
<div
className={cn("flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground", className)}
ref={ref}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
ref={ref}
className="group peer hidden text-sidebar-foreground md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
)}
/>
<div
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className,
)}
{...props}
>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
{children}
</div>
</div>
</div>
);
});
Sidebar.displayName = "Sidebar";
const SidebarTrigger = React.forwardRef<React.ElementRef<typeof Button>, React.ComponentProps<typeof Button>>(
({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn("h-7 w-7", className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
},
);
SidebarTrigger.displayName = "SidebarTrigger";
const SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<"button">>(
({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 hover:after:bg-sidebar-border sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className,
)}
{...props}
/>
);
},
);
SidebarRail.displayName = "SidebarRail";
const SidebarInset = React.forwardRef<HTMLDivElement, React.ComponentProps<"main">>(({ className, ...props }, ref) => {
return (
<main
ref={ref}
className={cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className,
)}
{...props}
/>
);
});
SidebarInset.displayName = "SidebarInset";
const SidebarInput = React.forwardRef<React.ElementRef<typeof Input>, React.ComponentProps<typeof Input>>(
({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
className,
)}
{...props}
/>
);
},
);
SidebarInput.displayName = "SidebarInput";
const SidebarHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return <div ref={ref} data-sidebar="header" className={cn("flex flex-col gap-2 p-2", className)} {...props} />;
});
SidebarHeader.displayName = "SidebarHeader";
const SidebarFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return <div ref={ref} data-sidebar="footer" className={cn("flex flex-col gap-2 p-2", className)} {...props} />;
});
SidebarFooter.displayName = "SidebarFooter";
const SidebarSeparator = React.forwardRef<React.ElementRef<typeof Separator>, React.ComponentProps<typeof Separator>>(
({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
data-sidebar="separator"
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
);
},
);
SidebarSeparator.displayName = "SidebarSeparator";
const SidebarContent = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className,
)}
{...props}
/>
);
});
SidebarContent.displayName = "SidebarContent";
const SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
);
});
SidebarGroup.displayName = "SidebarGroup";
const SidebarGroupLabel = React.forwardRef<HTMLDivElement, React.ComponentProps<"div"> & { asChild?: boolean }>(
({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "div";
return (
<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className,
)}
{...props}
/>
);
},
);
SidebarGroupLabel.displayName = "SidebarGroupLabel";
const SidebarGroupAction = React.forwardRef<HTMLButtonElement, React.ComponentProps<"button"> & { asChild?: boolean }>(
({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
},
);
SidebarGroupAction.displayName = "SidebarGroupAction";
const SidebarGroupContent = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
({ className, ...props }, ref) => (
<div ref={ref} data-sidebar="group-content" className={cn("w-full text-sm", className)} {...props} />
),
);
SidebarGroupContent.displayName = "SidebarGroupContent";
const SidebarMenu = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(({ className, ...props }, ref) => (
<ul ref={ref} data-sidebar="menu" className={cn("flex w-full min-w-0 flex-col gap-1", className)} {...props} />
));
SidebarMenu.displayName = "SidebarMenu";
const SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ className, ...props }, ref) => (
<li ref={ref} data-sidebar="menu-item" className={cn("group/menu-item relative", className)} {...props} />
));
SidebarMenuItem.displayName = "SidebarMenuItem";
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>
>(({ asChild = false, isActive = false, variant = "default", size = "default", tooltip, className, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar();
const button = (
<Comp
ref={ref}
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
);
if (!tooltip) {
return button;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent side="right" align="center" hidden={state !== "collapsed" || isMobile} {...tooltip} />
</Tooltip>
);
});
SidebarMenuButton.displayName = "SidebarMenuButton";
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean;
showOnHover?: boolean;
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform peer-hover/menu-button:text-sidebar-accent-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className,
)}
{...props}
/>
);
});
SidebarMenuAction.displayName = "SidebarMenuAction";
const SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.ComponentProps<"div">>(
({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
),
);
SidebarMenuBadge.displayName = "SidebarMenuBadge";
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
showIcon?: boolean;
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
<Skeleton
className="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
);
});
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
const SidebarMenuSub = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
),
);
SidebarMenuSub.displayName = "SidebarMenuSub";
const SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ ...props }, ref) => (
<li ref={ref} {...props} />
));
SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<"a"> & {
asChild?: boolean;
size?: "sm" | "md";
isActive?: boolean;
}
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return (
<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className,
)}
{...props}
/>
);
});
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
};
-7
View File
@@ -1,7 +0,0 @@
import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />;
}
export { Skeleton };
-23
View File
@@ -1,23 +0,0 @@
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "@/lib/utils";
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn("relative flex w-full touch-none select-none items-center", className)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };
-27
View File
@@ -1,27 +0,0 @@
import { useTheme } from "next-themes";
import { Toaster as Sonner, toast } from "sonner";
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
);
};
export { Toaster, toast };
-27
View File
@@ -1,27 +0,0 @@
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };
-72
View File
@@ -1,72 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
</div>
),
);
Table.displayName = "Table";
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />,
);
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
),
);
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tfoot ref={ref} className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} {...props} />
),
);
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn("border-b transition-colors data-[state=selected]:bg-muted hover:bg-muted/50", className)}
{...props}
/>
),
);
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
),
);
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<td ref={ref} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} />
),
);
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
({ className, ...props }, ref) => (
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
),
);
TableCaption.displayName = "TableCaption";
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
-53
View File
@@ -1,53 +0,0 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
className,
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };
-21
View File
@@ -1,21 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
});
Textarea.displayName = "Textarea";
export { Textarea };
+1 -1
View File
@@ -108,4 +108,4 @@ export {
ToastDescription,
ToastClose,
ToastAction,
};
};
-24
View File
@@ -1,24 +0,0 @@
import { useToast } from "@/hooks/use-toast";
import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast";
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && <ToastDescription>{description}</ToastDescription>}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}
-49
View File
@@ -1,49 +0,0 @@
import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { toggleVariants } from "@/components/ui/toggle";
const ToggleGroupContext = React.createContext<VariantProps<typeof toggleVariants>>({
size: "default",
variant: "default",
});
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root ref={ref} className={cn("flex items-center justify-center gap-1", className)} {...props}>
<ToggleGroupContext.Provider value={{ variant, size }}>{children}</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
));
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className,
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
});
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
export { ToggleGroup, ToggleGroupItem };
-37
View File
@@ -1,37 +0,0 @@
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root ref={ref} className={cn(toggleVariants({ variant, size, className }))} {...props} />
));
Toggle.displayName = TogglePrimitive.Root.displayName;
export { Toggle, toggleVariants };
-28
View File
@@ -1,28 +0,0 @@
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.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md 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}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
@@ -1,3 +1,3 @@
import { useToast, toast } from "@/hooks/use-toast";
export { useToast, toast };
export { useToast, toast };
+59 -209
View File
@@ -1,229 +1,79 @@
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Fraunces:ital,opsz,wght@0,9..144,100..900;1,9..144,100..900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&display=swap');
@import "tailwindcss";
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* TailTrails Color Palette - Warm & Natural */
--background: 40 23% 96%; /* #F5F3EE cream */
--foreground: 80 20% 20%; /* Dark olive text */
--card: 40 20% 98%;
--card-foreground: 80 20% 20%;
--popover: 40 20% 98%;
--popover-foreground: 80 20% 20%;
/* Olive/Sage Green Primary */
--primary: 90 14% 42%; /* #6B7B5F */
--primary-foreground: 40 23% 96%;
--secondary: 40 15% 90%;
--secondary-foreground: 80 20% 20%;
--muted: 40 15% 92%;
--muted-foreground: 80 10% 45%;
--accent: 35 60% 50%; /* Warm amber accent */
--accent-foreground: 40 23% 96%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 40 15% 85%;
--input: 40 15% 85%;
--ring: 90 14% 42%;
--radius: 0.75rem;
/* Custom tokens */
--olive: 90 14% 42%;
--olive-dark: 90 20% 25%;
--olive-light: 90 14% 55%;
--cream: 40 23% 96%;
--cream-dark: 40 20% 90%;
--amber: 35 60% 50%;
--amber-light: 35 70% 60%;
/* Gradients */
--gradient-hero: linear-gradient(135deg, hsl(40 23% 96%) 0%, hsl(40 20% 92%) 100%);
--gradient-olive: linear-gradient(135deg, hsl(90 14% 42%) 0%, hsl(90 20% 35%) 100%);
--gradient-card: linear-gradient(180deg, hsl(40 20% 98%) 0%, hsl(40 23% 96%) 100%);
/* Shadows */
--shadow-soft: 0 4px 20px -4px hsl(90 14% 42% / 0.15);
--shadow-elevated: 0 8px 30px -8px hsl(90 14% 42% / 0.2);
--shadow-glow: 0 0 40px hsl(90 14% 42% / 0.15);
/* Typography */
--font-display: 'Fraunces', serif;
--font-body: 'DM Sans', sans-serif;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 80 20% 10%;
--foreground: 40 23% 96%;
--card: 80 18% 12%;
--card-foreground: 40 23% 96%;
--popover: 80 18% 12%;
--popover-foreground: 40 23% 96%;
--primary: 90 14% 55%;
--primary-foreground: 80 20% 10%;
--secondary: 80 15% 18%;
--secondary-foreground: 40 23% 96%;
--muted: 80 15% 20%;
--muted-foreground: 40 15% 65%;
--accent: 35 60% 55%;
--accent-foreground: 80 20% 10%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 80 15% 20%;
--input: 80 15% 20%;
--ring: 90 14% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground font-body antialiased;
}
h1, h2, h3, h4, h5, h6 {
@apply font-display;
}
}
@layer utilities {
.font-display {
font-family: var(--font-display);
}
@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-display: "Outfit", sans-serif;
.font-body {
font-family: var(--font-body);
}
--color-bg-primary: #191E07;
--color-bg-secondary: #272F0A;
--color-bg-variant: #3E481B;
--color-accent-primary: #FFFE77;
--color-accent-secondary: #D4FF5E;
--color-text-primary: #FFFFFF;
--color-text-secondary: #BEC056;
--color-text-muted: #B4BC9A;
--color-border-subtle: #343A20;
/* Map brand colors for existing components */
--color-brand: var(--color-accent-secondary);
--color-brand-light: var(--color-accent-primary);
--color-brand-dark: #86efac; /* Fallback or specific shade */
.gradient-hero {
background: var(--gradient-hero);
}
/* destructive / danger color used by some components */
--color-destructive: #dc2626; /* red-600 */
--color-destructive-foreground: #ffffff;
--color-red-300: #fca5a5;
}
.gradient-olive {
background: var(--gradient-olive);
}
.gradient-card {
background: var(--gradient-card);
}
.shadow-soft {
box-shadow: var(--shadow-soft);
}
.shadow-elevated {
box-shadow: var(--shadow-elevated);
}
.shadow-glow {
box-shadow: var(--shadow-glow);
}
.text-balance {
text-wrap: balance;
@layer base {
body {
@apply bg-bg-primary text-text-primary font-sans antialiased;
}
}
/* Animations */
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
@utility glass {
@apply bg-white/5 backdrop-blur-xl border border-border-subtle;
}
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
@utility bento-card {
@apply bg-bg-secondary border border-border-subtle rounded-3xl p-8 transition-all duration-300;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
/* Semantic utility aliases to map theme variables to class names used across the app */
:root {
/* expose aliases for legacy class names */
}
@keyframes slide-in-left {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.bg-background { background-color: var(--color-bg-primary); }
.bg-bg-primary { background-color: var(--color-bg-primary); }
.bg-bg-secondary { background-color: var(--color-bg-secondary); }
.bg-bg-variant { background-color: var(--color-bg-variant); }
@keyframes pulse-soft {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.text-foreground { color: var(--color-text-primary); }
.text-text-primary { color: var(--color-text-primary); }
.text-text-secondary { color: var(--color-text-secondary); }
.text-text-muted { color: var(--color-text-muted); }
.text-muted-foreground { color: var(--color-text-muted); }
.animate-float {
animation: float 3s ease-in-out infinite;
}
.border-border { border-color: var(--color-border-subtle); }
.border-border-subtle { border-color: var(--color-border-subtle); }
.animate-fade-up {
animation: fade-up 0.6s ease-out forwards;
}
.bg-brand { background-color: var(--color-brand); }
.bg-brand-light { background-color: var(--color-brand-light); }
.text-bg-primary { color: var(--color-bg-primary); }
.animate-fade-in {
animation: fade-in 0.5s ease-out forwards;
}
.bg-secondary { background-color: rgba(255,255,255,0.04); }
.text-secondary-foreground { color: var(--color-text-secondary); }
.animate-slide-in-left {
animation: slide-in-left 0.5s ease-out forwards;
}
.glass { background-color: rgba(255,255,255,0.05); }
.animate-pulse-soft {
animation: pulse-soft 2s ease-in-out infinite;
}
/* destructive / danger utilities (used by some components) */
.bg-destructive { background-color: var(--color-destructive); }
.border-destructive { border-color: var(--color-destructive); }
.text-destructive-foreground { color: var(--color-destructive-foreground); }
.group\.\[\.destructive\]\:text-red-300 { color: var(--color-red-300); }
/* Staggered animations */
.stagger-1 { animation-delay: 0.1s; }
.stagger-2 { animation-delay: 0.2s; }
.stagger-3 { animation-delay: 0.3s; }
.stagger-4 { animation-delay: 0.4s; }
.stagger-5 { animation-delay: 0.5s; }
+14
View File
@@ -0,0 +1,14 @@
// Generated list of filenames in the `public/blog-images` folder.
// If you add or remove images, update this list accordingly.
const BLOG_IMAGE_FILENAMES = [
'afra-ramio-cp2HCVzguw4-unsplash.jpg',
'dagmar-klauzova-afBc-k2luH4-unsplash.jpg',
'eric-ward-ISg37AI2A-s-unsplash.jpg',
'jamie-street-m4BkRRprvP0-unsplash.jpg',
'joe-caione-qO-PIF84Vxg-unsplash.jpg',
'rafaella-waasdorp-EkzMdwI_YE4-unsplash.jpg',
'tatum-bergen-Vt3odrTM4cQ-unsplash.jpg',
'troy-bridges-lbRNtUZPXXQ-unsplash.jpg',
];
export default BLOG_IMAGE_FILENAMES;
+77
View File
@@ -0,0 +1,77 @@
export type BlogMeta = {
title?: string;
date?: string;
description?: string;
category?: string;
readTime?: string;
live?: boolean;
};
const meta: Record<string, BlogMeta> = {
"best-off-leash-areas-for-dogs-in-lagos-and-nearby": {
title: "Best Off-Leash Areas for Dogs in Lagos (and Nearby)",
date: "2024-05-01",
description: "Looking for off-leash areas for dogs in Lagos, Portugal? Discover the best spots for safe, enjoyable off-leash walks, plus what to watch out for.",
category: "Insights",
readTime: "6 min",
live: true,
},
"how-to-choose-the-right-walk-for-your-dog-based-on-their-personality-and-needs": {
title: "How to Choose the Right Walk for Your Dog (Based on Their Personality and Needs)",
date: "2024-04-20",
description: "Not all walks are equal. Learn how to choose the right environment for your dog based on energy, temperament, and needs.",
category: "Strategy",
readTime: "5 min",
live: true,
},
"how-to-give-a-reactive-dog-a-fulfilling-life-without-avoiding-the-world": {
title: "How to Give a Reactive Dog a Fulfilling Life (Without Avoiding the World)",
date: "2024-03-15",
description: "Have a reactive dog? Learn how to give them a fulfilling life with the right environments, structure, and approach.",
category: "Insights",
readTime: "7 min",
live: true,
},
"off-leash-dog-rules-in-portugal-what-you-need-to-know-in-2026": {
title: "Off-Leash Dog Rules in Portugal: What You Need to Know in 2026",
date: "2026-04-01",
description: "Can you walk your dog off-leash in Portugal? Learn the rules, risks, and best practices for safe and legal off-leash walks.",
category: "Trends",
readTime: "6 min",
live: true,
},
"top-10-hikes-to-do-with-your-dog-in-lagos-and-the-algarve": {
title: "Top 10 Hikes to do with your Dog in Lagos and the Algarve",
date: "2024-06-10",
description: "Discover the best hikes in Lagos and the Algarve to take your dog on. From coastal trails to quiet forests, find the right walk for your dog.",
category: "Insights",
readTime: "8 min",
live: true,
},
"walking-your-dog-in-the-algarve-hidden-dangers-every-owner-should-know": {
title: "Walking Your Dog in the Algarve: Hidden Dangers Every Owner Should Know",
date: "2024-07-05",
description: "Walking your dog in the Algarve? Learn about hidden dangers like pine processionary caterpillars, grass seeds, heat, and more — plus how to keep your dog safe.",
category: "Tutorial",
readTime: "7 min",
live: true,
},
"the-importance-of-variety-in-your-dogs-life-and-how-to-do-it-right": {
title: "The Importance of Variety in Your Dogs Life (And How to Do It Right)",
date: "2024-02-28",
description: "Dogs need more than the same daily walk. Learn why variety matters and how to safely introduce new environments into your dogs routine.",
category: "Strategy",
readTime: "5 min",
live: true,
},
"why-dog-friendly-isn-t-enough-and-what-actually-matters": {
title: "Why “Dog-Friendly” Isnt Enough (And What Actually Matters)",
date: "2024-01-18",
description: "“Dog-friendly” doesnt always mean suitable. Learn what really makes a walk right for your dog, and how to choose better environments.",
category: "Insights",
readTime: "5 min",
live: true,
},
};
export default meta;
+94
View File
@@ -0,0 +1,94 @@
export type BlogPost = {
slug: string;
title: string;
date?: string; // created at / published date
description?: string;
content: string;
category?: string;
readTime?: string;
live?: boolean;
};
import meta from './blogMeta';
function slugify(name: string) {
return name
.toLowerCase()
.replace(/\.md$/, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
}
function parseFrontmatter(raw: string) {
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
const fm: Record<string, any> = {};
let content = raw;
if (fmMatch) {
const body = fmMatch[1];
body.split(/\n+/).forEach((line) => {
const idx = line.indexOf(':');
if (idx > -1) {
const key = line.slice(0, idx).trim();
let val = line.slice(idx + 1).trim().replace(/^"|"$/g, '');
// coerce booleans
if (/^(true|false)$/i.test(val)) {
fm[key] = val.toLowerCase() === 'true';
} else {
fm[key] = val;
}
}
});
content = raw.slice(fmMatch[0].length).trim();
}
return { fm, content };
}
let _postsCache: BlogPost[] | null = null;
async function loadPosts(): Promise<BlogPost[]> {
if (_postsCache) return _postsCache;
const slugs = Object.keys(meta);
const results: BlogPost[] = [];
for (const slug of slugs) {
try {
const url = `/raw-blogs/${slug}.md`;
const res = await fetch(url);
const raw = await res.text();
const { fm, content } = parseFrontmatter(raw);
const merged = { ...(meta[slug] || {}), ...fm } as Record<string, any>;
results.push({
slug,
title: merged.title ?? slug,
date: merged.date,
description: merged.description,
content,
category: merged.category,
readTime: merged.readTime,
live: typeof merged.live === 'boolean' ? merged.live : merged.live === 'true',
} as BlogPost);
} catch (e) {
// eslint-disable-next-line no-console
console.warn('Failed to load blog for slug:', slug, e);
}
}
results.sort((a, b) => {
const da = a.date ?? '';
const db = b.date ?? '';
return db.localeCompare(da);
});
_postsCache = results;
return results;
}
export async function getAllPosts(): Promise<BlogPost[]> {
return await loadPosts();
}
export async function getPostBySlug(slug: string): Promise<BlogPost | undefined> {
const all = await loadPosts();
return all.find((p) => p.slug === slug);
}
+32
View File
@@ -1,6 +1,38 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import BLOG_IMAGE_FILENAMES from './blogImages';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/**
* Return an Unsplash Source URL for a given query and size.
* Uses the Unsplash Source endpoint so no API key is required.
*/
export function getUnsplashImage(query: string, width = 1200, height = 675, seed?: string) {
const q = encodeURIComponent(query || 'nature');
// `sig` helps vary the image per post so images don't all cache to the same photo
const sig = seed ? `&sig=${encodeURIComponent(seed)}` : '';
return `https://source.unsplash.com/${width}x${height}/?${q}${sig}`;
}
// Load local blog images (as URLs) from the public folder root.
const _blogImageModules = import.meta.glob('/blog-images/*', { eager: true, query: '?url', import: 'default' }) as Record<string, string>;
let _blogImageUrls = Object.values(_blogImageModules);
// Fallback: if the glob didn't return anything (public assets may not be importable),
// construct URLs from known filenames in the `public/blog-images` folder.
if (!_blogImageUrls || _blogImageUrls.length === 0) {
_blogImageUrls = BLOG_IMAGE_FILENAMES.map((n) => `/blog-images/${n}`);
}
/**
* Pick a local blog image by numeric index. Images are assigned in order.
*/
export function getBlogImage(index?: number, fallback?: string) {
if (_blogImageUrls.length === 0) return fallback ?? '';
if (typeof index !== 'number' || isNaN(index)) return _blogImageUrls[0];
const idx = Math.abs(Math.floor(index)) % _blogImageUrls.length;
return _blogImageUrls[idx];
}

Some files were not shown because too many files have changed in this diff Show More