kaddidlehopper 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +139 -0
- package/README.md +47 -0
- package/add-ons/ai/README.md +34 -0
- package/add-ons/ai/assets/_dot_env.local.append +13 -0
- package/add-ons/ai/assets/src/components/AIAssistant.tsx +149 -0
- package/add-ons/ai/assets/src/lib/ai-hook.ts +21 -0
- package/add-ons/ai/assets/src/lib/weather-tools.ts +30 -0
- package/add-ons/ai/assets/src/routes/api.chat.ts +94 -0
- package/add-ons/ai/assets/src/routes/chat.css +175 -0
- package/add-ons/ai/assets/src/routes/chat.tsx +141 -0
- package/add-ons/ai/info.json +27 -0
- package/add-ons/ai/package.json +17 -0
- package/add-ons/ai/small-logo.svg +8 -0
- package/dist/cli.js +251 -0
- package/dist/index.js +33 -0
- package/dist/types/cli.d.ts +8 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/types.d.ts +14 -0
- package/dist/types.js +1 -0
- package/examples/blog/README.md +60 -0
- package/examples/blog/assets/content/posts/beach.md +12 -0
- package/examples/blog/assets/content/posts/jungle.md.ejs +12 -0
- package/examples/blog/assets/content/posts/mountains.md.ejs +12 -0
- package/examples/blog/assets/content/posts/snorkeling.md.ejs +12 -0
- package/examples/blog/assets/content/posts/waterfall.md.ejs +12 -0
- package/examples/blog/assets/content-collections.ts +30 -0
- package/examples/blog/assets/public/beach.jpg +0 -0
- package/examples/blog/assets/public/jungle.jpg +0 -0
- package/examples/blog/assets/public/mountains.jpg +0 -0
- package/examples/blog/assets/public/snorkeling.jpg +0 -0
- package/examples/blog/assets/public/waterfall.jpg +0 -0
- package/examples/blog/assets/src/components/Header.tsx +52 -0
- package/examples/blog/assets/src/components/VacayAssistant.tsx +205 -0
- package/examples/blog/assets/src/components/blog-posts.tsx +78 -0
- package/examples/blog/assets/src/components/ui/card.tsx +92 -0
- package/examples/blog/assets/src/lib/blog-ai-hook.ts +25 -0
- package/examples/blog/assets/src/lib/blog-tools.ts +111 -0
- package/examples/blog/assets/src/lib/utils.ts +6 -0
- package/examples/blog/assets/src/routes/__root.tsx +57 -0
- package/examples/blog/assets/src/routes/api.blog-chat.ts +117 -0
- package/examples/blog/assets/src/routes/category.$category.tsx +19 -0
- package/examples/blog/assets/src/routes/index.tsx +19 -0
- package/examples/blog/assets/src/routes/posts.$slug.tsx +63 -0
- package/examples/blog/assets/src/styles.css +138 -0
- package/examples/blog/info.json +43 -0
- package/examples/blog/package.json +23 -0
- package/examples/events/README.md +110 -0
- package/examples/events/assets/content/speakers/andre-costa.md +22 -0
- package/examples/events/assets/content/speakers/hans-mueller.md.ejs +22 -0
- package/examples/events/assets/content/speakers/isabella-martinez.md.ejs +22 -0
- package/examples/events/assets/content/speakers/kenji-nakamura.md.ejs +22 -0
- package/examples/events/assets/content/speakers/marie-dubois.md.ejs +20 -0
- package/examples/events/assets/content/speakers/priya-sharma.md.ejs +22 -0
- package/examples/events/assets/content/talks/croissant-lamination-secrets.md +39 -0
- package/examples/events/assets/content/talks/french-macaron-mastery.md.ejs +39 -0
- package/examples/events/assets/content/talks/neapolitan-pizza-tradition-meets-innovation.md.ejs +39 -0
- package/examples/events/assets/content/talks/savory-breads-of-the-mediterranean.md.ejs +39 -0
- package/examples/events/assets/content/talks/sourdough-from-starter-to-masterpiece.md.ejs +36 -0
- package/examples/events/assets/content/talks/the-art-of-the-perfect-tart.md.ejs +32 -0
- package/examples/events/assets/content/talks/the-science-of-sugar.md.ejs +39 -0
- package/examples/events/assets/content/talks/umami-in-pastry-east-meets-west.md.ejs +39 -0
- package/examples/events/assets/content-collections.ts +56 -0
- package/examples/events/assets/public/background-1.jpg +0 -0
- package/examples/events/assets/public/background-2.jpg +0 -0
- package/examples/events/assets/public/background-3.jpg +0 -0
- package/examples/events/assets/public/background-4.jpg +0 -0
- package/examples/events/assets/public/conference-logo.png +0 -0
- package/examples/events/assets/public/favicon.ico +0 -0
- package/examples/events/assets/public/speakers/andre-costa.jpg +0 -0
- package/examples/events/assets/public/speakers/hans-mueller.jpg +0 -0
- package/examples/events/assets/public/speakers/isabella-martinez.jpg +0 -0
- package/examples/events/assets/public/speakers/kenji-nakamura.jpg +0 -0
- package/examples/events/assets/public/speakers/marie-dubois.jpg +0 -0
- package/examples/events/assets/public/speakers/priya-sharma.jpg +0 -0
- package/examples/events/assets/public/talks/croissant-lamination-secrets.jpg +0 -0
- package/examples/events/assets/public/talks/french-macaron-mastery.jpg +0 -0
- package/examples/events/assets/public/talks/neapolitan-pizza-tradition-meets-innovation.jpg +0 -0
- package/examples/events/assets/public/talks/savory-breads-of-the-mediterranean.jpg +0 -0
- package/examples/events/assets/public/talks/sourdough-from-starter-to-masterpiece.jpg +0 -0
- package/examples/events/assets/public/talks/the-art-of-the-perfect-tart.jpg +0 -0
- package/examples/events/assets/public/talks/the-science-of-sugar.jpg +0 -0
- package/examples/events/assets/public/talks/umami-in-pastry-east-meets-west.jpg +0 -0
- package/examples/events/assets/public/tanstack-circle-logo.png +0 -0
- package/examples/events/assets/public/tanstack-word-logo-white.svg +1 -0
- package/examples/events/assets/src/components/Header.tsx +59 -0
- package/examples/events/assets/src/components/HeaderNav.tsx +67 -0
- package/examples/events/assets/src/components/HeroCarousel.tsx +61 -0
- package/examples/events/assets/src/components/RemyAssistant.tsx +207 -0
- package/examples/events/assets/src/components/SpeakerCard.tsx +67 -0
- package/examples/events/assets/src/components/TalkCard.tsx +77 -0
- package/examples/events/assets/src/components/ui/card.tsx +92 -0
- package/examples/events/assets/src/lib/conference-ai-hook.ts +26 -0
- package/examples/events/assets/src/lib/conference-tools.ts +210 -0
- package/examples/events/assets/src/lib/model-selection.ts +1 -0
- package/examples/events/assets/src/lib/utils.ts +6 -0
- package/examples/events/assets/src/routes/__root.tsx +70 -0
- package/examples/events/assets/src/routes/api.remy-chat.ts +119 -0
- package/examples/events/assets/src/routes/index.tsx +192 -0
- package/examples/events/assets/src/routes/schedule.index.tsx +274 -0
- package/examples/events/assets/src/routes/speakers.$slug.tsx +122 -0
- package/examples/events/assets/src/routes/speakers.index.tsx +40 -0
- package/examples/events/assets/src/routes/talks.$slug.tsx +116 -0
- package/examples/events/assets/src/routes/talks.index.tsx +40 -0
- package/examples/events/assets/src/styles.css +182 -0
- package/examples/events/info.json +74 -0
- package/examples/events/package.json +23 -0
- package/examples/marketing/README.md +60 -0
- package/examples/marketing/assets/public/logo.png +0 -0
- package/examples/marketing/assets/public/motorcycle-adventure.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-cruiser.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-scooter.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-sport.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-supersport.jpg +0 -0
- package/examples/marketing/assets/src/components/Header.tsx +36 -0
- package/examples/marketing/assets/src/components/MotorcycleAIAssistant.tsx +162 -0
- package/examples/marketing/assets/src/components/MotorcycleRecommendation.tsx +53 -0
- package/examples/marketing/assets/src/data/motorcycles.ts.ejs +77 -0
- package/examples/marketing/assets/src/lib/motorcycle-ai-hook.ts +24 -0
- package/examples/marketing/assets/src/lib/motorcycle-tools.ts +42 -0
- package/examples/marketing/assets/src/routes/__root.tsx +57 -0
- package/examples/marketing/assets/src/routes/api.motorcycle-chat.ts +78 -0
- package/examples/marketing/assets/src/routes/index.tsx +72 -0
- package/examples/marketing/assets/src/routes/motorcycles/$motorcycleId.tsx +56 -0
- package/examples/marketing/assets/src/store/motorcycle-assistant.ts +3 -0
- package/examples/marketing/assets/src/styles.css +212 -0
- package/examples/marketing/info.json +38 -0
- package/examples/marketing/package.json +14 -0
- package/examples/resume/README.md +82 -0
- package/examples/resume/assets/content/education/code-school.md +17 -0
- package/examples/resume/assets/content/jobs/freelance.md.ejs +13 -0
- package/examples/resume/assets/content/jobs/initech-junior.md +20 -0
- package/examples/resume/assets/content/jobs/initech-lead.md.ejs +29 -0
- package/examples/resume/assets/content/jobs/initrode-senior.md.ejs +28 -0
- package/examples/resume/assets/content-collections.ts +36 -0
- package/examples/resume/assets/public/headshot-on-white.jpg +0 -0
- package/examples/resume/assets/src/components/Header.tsx +33 -0
- package/examples/resume/assets/src/components/ResumeAssistant.tsx +193 -0
- package/examples/resume/assets/src/components/ui/badge.tsx +46 -0
- package/examples/resume/assets/src/components/ui/card.tsx +92 -0
- package/examples/resume/assets/src/components/ui/checkbox.tsx +30 -0
- package/examples/resume/assets/src/components/ui/hover-card.tsx +44 -0
- package/examples/resume/assets/src/components/ui/separator.tsx +26 -0
- package/examples/resume/assets/src/lib/resume-ai-hook.ts +21 -0
- package/examples/resume/assets/src/lib/resume-tools.ts +165 -0
- package/examples/resume/assets/src/lib/utils.ts +6 -0
- package/examples/resume/assets/src/routes/api.resume-chat.ts +110 -0
- package/examples/resume/assets/src/routes/index.tsx +220 -0
- package/examples/resume/assets/src/styles.css +138 -0
- package/examples/resume/info.json +25 -0
- package/examples/resume/package.json +26 -0
- package/package.json +39 -0
- package/project/base/_dot_claude/skills/content-collections/SKILL.md +505 -0
- package/project/base/_dot_claude/skills/netlify-blobs/SKILL.md +410 -0
- package/project/base/_dot_claude/skills/netlify-db/SKILL.md +424 -0
- package/project/base/_dot_claude/skills/netlify-debugging/SKILL.md +419 -0
- package/project/base/_dot_claude/skills/netlify-forms/SKILL.md +243 -0
- package/project/base/_dot_claude/skills/netlify-functions/SKILL.md +372 -0
- package/project/base/_dot_claude/skills/tanstack-start-api-routes/SKILL.md +421 -0
- package/project/base/_dot_claude/skills/tanstack-start-loaders/SKILL.md +426 -0
- package/project/base/_dot_claude/skills/tanstack-start-project-setup/SKILL.md +493 -0
- package/project/base/_dot_claude/skills/tanstack-start-routes/SKILL.md +430 -0
- package/project/base/_dot_claude/skills/tanstack-start-server-functions/SKILL.md +445 -0
- package/project/base/_dot_claude/skills/tanstack-start-typesafe-routing/SKILL.md +494 -0
- package/project/base/_dot_gitignore +8 -0
- package/project/base/netlify.toml +7 -0
- package/project/base/package.json +33 -0
- package/project/base/public/favicon.ico +0 -0
- package/project/base/public/tanstack-circle-logo.png +0 -0
- package/project/base/public/tanstack-word-logo-white.svg +1 -0
- package/project/base/src/components/Header.tsx +17 -0
- package/project/base/src/components/HeaderNav.tsx.ejs +179 -0
- package/project/base/src/router.tsx +15 -0
- package/project/base/src/routes/__root.tsx +57 -0
- package/project/base/src/routes/index.tsx +48 -0
- package/project/base/src/styles.css +15 -0
- package/project/base/tsconfig.json +28 -0
- package/project/base/vite.config.ts.ejs +25 -0
- package/project/packages.json +22 -0
- package/scripts/check-outdated-packages.js +421 -0
- package/src/cli.ts +343 -0
- package/src/index.ts +49 -0
- package/src/types.ts +15 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Blog Example
|
|
2
|
+
|
|
3
|
+
A Hawaii adventures travel blog built with TanStack Start and content-collections for Netlify deployment.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Content Collections**: Blog posts managed as markdown files with frontmatter
|
|
8
|
+
- **Category Navigation**: Filter posts by category (Relaxing, Hiking)
|
|
9
|
+
- **Beautiful UI**: Postcard-style blog cards with shadcn/ui components
|
|
10
|
+
- **SSR Ready**: Full server-side rendering with TanStack Start
|
|
11
|
+
|
|
12
|
+
## Project Structure
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
├── content/
|
|
16
|
+
│ └── posts/ # Markdown blog posts
|
|
17
|
+
├── src/
|
|
18
|
+
│ ├── components/
|
|
19
|
+
│ │ ├── blog-posts.tsx # Blog post grid
|
|
20
|
+
│ │ ├── Header.tsx # Navigation header
|
|
21
|
+
│ │ └── ui/
|
|
22
|
+
│ │ └── card.tsx # Shadcn card component
|
|
23
|
+
│ ├── lib/
|
|
24
|
+
│ │ └── utils.ts # Utility functions
|
|
25
|
+
│ └── routes/
|
|
26
|
+
│ ├── __root.tsx # Root layout
|
|
27
|
+
│ ├── index.tsx # Home page
|
|
28
|
+
│ ├── posts.$slug.tsx # Individual post
|
|
29
|
+
│ └── category.$category.tsx # Category filter
|
|
30
|
+
└── public/
|
|
31
|
+
└── *.jpg # Blog images
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Adding Blog Posts
|
|
35
|
+
|
|
36
|
+
Create a new markdown file in `content/posts/` with the following frontmatter:
|
|
37
|
+
|
|
38
|
+
```markdown
|
|
39
|
+
---
|
|
40
|
+
date: 2025-03-01
|
|
41
|
+
title: "Your Post Title"
|
|
42
|
+
summary: "A brief summary of your post"
|
|
43
|
+
categories:
|
|
44
|
+
- Category1
|
|
45
|
+
- Category2
|
|
46
|
+
image: your-image.jpg
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
Your post content here...
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Development
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Start development server
|
|
56
|
+
npm run dev
|
|
57
|
+
|
|
58
|
+
# Build for production
|
|
59
|
+
npm run build
|
|
60
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
date: 2025-03-01
|
|
3
|
+
title: "Day At The Beach"
|
|
4
|
+
summary: "Had a great day at the beach in Hawaii"
|
|
5
|
+
categories:
|
|
6
|
+
- Relaxing
|
|
7
|
+
image: beach.jpg
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
The morning sun painted the ocean in shimmering gold as I paddled out beyond the break, my surfboard gliding smoothly through the crystal-clear Hawaiian waters. The waves were perfect today - clean, consistent sets rolling in with just enough power to make things interesting without being intimidating. After catching several exhilarating rides, each one lasting what felt like an eternity, I found myself grinning from ear to ear, salt water dripping from my hair, and feeling completely in tune with the rhythm of the ocean.
|
|
11
|
+
|
|
12
|
+
After my surf session, I found the perfect spot on the warm sand to soak in the midday sun. The gentle trade winds carried the sweet scent of plumeria, while palm trees swayed overhead providing occasional patches of shade. I alternated between reading my book, taking refreshing dips in the turquoise water, and simply watching the parade of beach life unfold around me - children building sandcastles, paddleboarders gliding by in the distance, and sea turtles occasionally popping their heads above the surface. It was one of those perfect Hawaiian days that remind you why these islands are called paradise.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<% if (addOnOption.project?.bareBones) { ignoreFile() } %>---
|
|
2
|
+
date: 2025-03-02
|
|
3
|
+
title: "Hiking The Jungle"
|
|
4
|
+
summary: "Picking our way through the jungle in Hawaii"
|
|
5
|
+
categories:
|
|
6
|
+
- Hiking
|
|
7
|
+
image: jungle.jpg
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
The ascent through the Hawaiian jungle was a feast for the senses. Massive tree ferns unfurled their prehistoric fronds overhead, creating a living canopy that filtered the morning sunlight into ethereal green beams. The air was thick with humidity and alive with sound – the melodic calls of 'apapane birds echoed through the forest, while the distinctive cry of the 'i'iwi punctuated the constant background chorus of buzzing insects and rustling leaves.
|
|
11
|
+
|
|
12
|
+
As we picked our way along the narrow trail, each step carefully placed on the rain-slicked earth, the dense jungle gradually began to thin. Through gaps in the vegetation, tantalizing glimpses of the valley below emerged, hinting at the spectacular vista that awaited us at the summit. The sweet scent of wild ginger mingled with the earthy aroma of damp soil, while delicate 'ōhi'a lehua blossoms dotted the path with splashes of crimson, their presence a hopeful sign of the forest's resilience against environmental challenges.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<% if (addOnOption.project?.bareBones) { ignoreFile() } %>---
|
|
2
|
+
date: 2025-03-02
|
|
3
|
+
title: "Mountain Tops"
|
|
4
|
+
summary: "Ending our hike with mountain views"
|
|
5
|
+
categories:
|
|
6
|
+
- Hiking
|
|
7
|
+
image: mountains.jpg
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
The day's journey took an unexpected turn when we stumbled upon a hidden waterfall cascading down moss-covered rocks. The thundering sound of water drew us off our planned trail, and we spent a magical hour exploring the crystal-clear pool at its base, watching rainbow-like mist dance in the afternoon sun. What started as a simple jungle trek transformed into an adventure of discovery, with exotic birds calling from the canopy above and vibrant butterflies fluttering between patches of filtered sunlight.
|
|
11
|
+
|
|
12
|
+
As dusk approached, we finally reached the summit, where jagged peaks pierced through a sea of clouds stretching endlessly toward the horizon. However, our triumph was short-lived as we realized we'd need to navigate the descent in fading light. Armed with only two headlamps between the four of us, we carefully picked our way down the rocky trail, our hearts racing with each uncertain step. The stars emerged one by one above us, and the distant lights of our hotel served as a welcome beacon, guiding us safely back just as true darkness settled over the mountains.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<% if (addOnOption.project?.bareBones) { ignoreFile() } %>---
|
|
2
|
+
date: 2025-03-01
|
|
3
|
+
title: "Snorkeling"
|
|
4
|
+
summary: "Snorkling with the fish in Hawaii"
|
|
5
|
+
categories:
|
|
6
|
+
- Relaxing
|
|
7
|
+
image: snorkeling.jpg
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
As I glided through the crystal-clear waters off the Hawaiian coast, I found myself immersed in an underwater paradise. The warm Pacific waters, hovering around 80 degrees Fahrenheit, embraced me like a gentle blanket while visibility extended for what seemed like eternity. Each gentle wave brought new wonders into view, with sunlight dancing through the water creating mesmerizing patterns on the sandy ocean floor below.
|
|
11
|
+
|
|
12
|
+
The vibrant marine life was absolutely breathtaking, with schools of yellow tang darting between clusters of coral and parrotfish methodically grazing on the reef. Brilliant butterfly fish, their distinctive patterns catching the sunlight, swam fearlessly past my mask, while iridescent humuhumunukunukuapua'a (Hawaii's state fish) investigated my presence with curious eyes. In every direction, the coral reefs teemed with life, creating a natural aquarium that made me feel like a privileged guest in this underwater world.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<% if (addOnOption.project?.bareBones) { ignoreFile() } %>---
|
|
2
|
+
date: 2025-03-02
|
|
3
|
+
title: "Waterfall Surprise"
|
|
4
|
+
summary: "We weren't expecting this waterfall in Hawaii"
|
|
5
|
+
categories:
|
|
6
|
+
- Hiking
|
|
7
|
+
image: waterfall.jpg
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
As we made our way through the dense Hawaiian rainforest, the humid air clung to our skin and the chirping of native birds provided a natural symphony to our adventure. The trail had been relatively straightforward, winding through groves of towering bamboo and past ancient lava flows, but nothing prepared us for what lay around the final bend. The sound hit us first - a distant rushing that grew louder with each step, until we emerged into a hidden grotto where a magnificent waterfall cascaded down moss-covered volcanic rocks.
|
|
11
|
+
|
|
12
|
+
The secluded waterfall, easily sixty feet tall, created its own microclimate of cool mist and swirling breezes. Crystal clear water plunged into a pristine pool below, surrounded by delicate ferns and vibrant tropical flowers that seemed to thrive in this hidden paradise. We hadn't seen this waterfall mentioned in any of our guidebooks or trail maps, which made the discovery even more special. Standing there, with water droplets dancing in the filtered sunlight, we felt like we had stumbled upon one of Hawaii's best-kept secrets.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineCollection, defineConfig } from '@content-collections/core'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
const posts = defineCollection({
|
|
5
|
+
name: 'posts',
|
|
6
|
+
directory: 'content/posts',
|
|
7
|
+
include: '**/*.md',
|
|
8
|
+
schema: z.object({
|
|
9
|
+
title: z.string(),
|
|
10
|
+
summary: z.string(),
|
|
11
|
+
categories: z.array(z.string()),
|
|
12
|
+
slug: z.string().optional(),
|
|
13
|
+
image: z.string(),
|
|
14
|
+
date: z.string(),
|
|
15
|
+
content: z.string(),
|
|
16
|
+
}),
|
|
17
|
+
transform: async (doc) => {
|
|
18
|
+
return {
|
|
19
|
+
...doc,
|
|
20
|
+
slug: doc.title
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace('.md', '')
|
|
23
|
+
.replace(/[^\w-]+/g, '_'),
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export default defineConfig({
|
|
29
|
+
collections: [posts],
|
|
30
|
+
})
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Link } from '@tanstack/react-router'
|
|
2
|
+
|
|
3
|
+
import HeaderNav from './HeaderNav'
|
|
4
|
+
import { showVacayAssistant } from './VacayAssistant'
|
|
5
|
+
|
|
6
|
+
import { allPosts } from 'content-collections'
|
|
7
|
+
|
|
8
|
+
export default function Header() {
|
|
9
|
+
const categories = Array.from(
|
|
10
|
+
new Set(allPosts.flatMap((post) => post.categories)),
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const handleVacayClick = () => {
|
|
14
|
+
showVacayAssistant.setState(() => true)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<HeaderNav />
|
|
20
|
+
<header className="text-slate-700 font-serif font-extrabold fixed top-20 left-0 right-0 z-0 backdrop-blur-md bg-white/50 border-b border-slate-200/50 transition-all">
|
|
21
|
+
<nav className="max-w-7xl mx-auto p-4 flex gap-2 justify-between">
|
|
22
|
+
<div className="flex flex-row items-center space-x-6">
|
|
23
|
+
<div className="text-xl tracking-wide">
|
|
24
|
+
<Link to="/" className="hover:text-emerald-700 transition-colors">
|
|
25
|
+
Hawaii Adventures
|
|
26
|
+
</Link>
|
|
27
|
+
</div>
|
|
28
|
+
<div className="flex gap-6">
|
|
29
|
+
{categories.map((category) => (
|
|
30
|
+
<div key={category}>
|
|
31
|
+
<Link
|
|
32
|
+
to={`/category/${category}`}
|
|
33
|
+
className="hover:text-emerald-700 transition-colors"
|
|
34
|
+
>
|
|
35
|
+
{category}
|
|
36
|
+
</Link>
|
|
37
|
+
</div>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<button
|
|
42
|
+
onClick={handleVacayClick}
|
|
43
|
+
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-linear-to-r from-amber-400/90 via-orange-500/90 to-rose-500/90 hover:from-amber-400 hover:via-orange-500 hover:to-rose-500 text-white font-semibold text-sm transition-all hover:shadow-lg hover:shadow-orange-500/20 hover:scale-[1.02]"
|
|
44
|
+
>
|
|
45
|
+
<span className="text-base">🌴</span>
|
|
46
|
+
<span className="tracking-wide">Vacay Assistant</span>
|
|
47
|
+
</button>
|
|
48
|
+
</nav>
|
|
49
|
+
</header>
|
|
50
|
+
</>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { Send, X, Palmtree, Sun } from 'lucide-react'
|
|
3
|
+
import { Streamdown } from 'streamdown'
|
|
4
|
+
import { Store } from '@tanstack/store'
|
|
5
|
+
|
|
6
|
+
import { useBlogChat } from '@/lib/blog-ai-hook'
|
|
7
|
+
import type { BlogChatMessages } from '@/lib/blog-ai-hook'
|
|
8
|
+
|
|
9
|
+
function Messages({ messages }: { messages: BlogChatMessages }) {
|
|
10
|
+
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (messagesContainerRef.current) {
|
|
14
|
+
messagesContainerRef.current.scrollTop =
|
|
15
|
+
messagesContainerRef.current.scrollHeight
|
|
16
|
+
}
|
|
17
|
+
}, [messages])
|
|
18
|
+
|
|
19
|
+
if (!messages.length) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex-1 flex flex-col items-center justify-center text-amber-200/60 text-sm px-6 py-8">
|
|
22
|
+
<div className="relative mb-4">
|
|
23
|
+
<Sun className="w-12 h-12 text-amber-400/40 animate-pulse" />
|
|
24
|
+
<Palmtree className="w-6 h-6 text-emerald-400/60 absolute -bottom-1 -right-1" />
|
|
25
|
+
</div>
|
|
26
|
+
<p className="text-center text-amber-100/80 font-medium">
|
|
27
|
+
Aloha! 🌺 How can I help?
|
|
28
|
+
</p>
|
|
29
|
+
<p className="text-xs text-amber-200/40 mt-2 text-center max-w-[200px]">
|
|
30
|
+
Ask about adventures, get trip ideas, or explore the islands!
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div ref={messagesContainerRef} className="flex-1 overflow-y-auto">
|
|
38
|
+
{messages.map(({ id, role, parts }) => (
|
|
39
|
+
<div
|
|
40
|
+
key={id}
|
|
41
|
+
className={`py-3 ${
|
|
42
|
+
role === 'assistant'
|
|
43
|
+
? 'bg-linear-to-r from-amber-500/5 via-orange-500/5 to-rose-500/5'
|
|
44
|
+
: 'bg-transparent'
|
|
45
|
+
}`}
|
|
46
|
+
>
|
|
47
|
+
{parts.map((part, index) => {
|
|
48
|
+
if (part.type === 'text' && part.content) {
|
|
49
|
+
return (
|
|
50
|
+
<div key={index} className="flex items-start gap-3 px-4">
|
|
51
|
+
{role === 'assistant' ? (
|
|
52
|
+
<div className="w-7 h-7 rounded-full bg-linear-to-br from-amber-400 via-orange-500 to-rose-500 flex items-center justify-center text-xs font-bold text-white flex-shrink-0 shadow-lg shadow-orange-500/20">
|
|
53
|
+
🌴
|
|
54
|
+
</div>
|
|
55
|
+
) : (
|
|
56
|
+
<div className="w-7 h-7 rounded-full bg-slate-600 flex items-center justify-center text-xs font-medium text-white flex-shrink-0">
|
|
57
|
+
You
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
<div className="flex-1 min-w-0 text-amber-50 prose dark:prose-invert max-w-none prose-sm prose-p:text-amber-50 prose-headings:text-amber-100 prose-strong:text-amber-200">
|
|
61
|
+
<Streamdown>{part.content}</Streamdown>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
return null
|
|
67
|
+
})}
|
|
68
|
+
</div>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface VacayAssistantProps {
|
|
75
|
+
slug?: string
|
|
76
|
+
postTitle?: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Export store for header control
|
|
80
|
+
export const showVacayAssistant = new Store(false)
|
|
81
|
+
|
|
82
|
+
export default function VacayAssistant({
|
|
83
|
+
slug,
|
|
84
|
+
postTitle,
|
|
85
|
+
}: VacayAssistantProps) {
|
|
86
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
87
|
+
const { messages, sendMessage, isLoading } = useBlogChat(slug)
|
|
88
|
+
const [input, setInput] = useState('')
|
|
89
|
+
|
|
90
|
+
// Sync with store for header control
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
return showVacayAssistant.subscribe(() => {
|
|
93
|
+
setIsOpen(showVacayAssistant.state)
|
|
94
|
+
})
|
|
95
|
+
}, [])
|
|
96
|
+
|
|
97
|
+
const handleToggle = () => {
|
|
98
|
+
const newState = !isOpen
|
|
99
|
+
setIsOpen(newState)
|
|
100
|
+
showVacayAssistant.setState(() => newState)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const handleSend = () => {
|
|
104
|
+
if (input.trim()) {
|
|
105
|
+
sendMessage(input)
|
|
106
|
+
setInput('')
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!isOpen) return null
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className="fixed top-20 right-4 z-[100] w-[400px] h-[520px] rounded-3xl shadow-2xl flex flex-col overflow-hidden border border-amber-500/20 backdrop-blur-xl bg-linear-to-b from-slate-900/98 via-slate-900/95 to-slate-800/98">
|
|
114
|
+
{/* Decorative top gradient */}
|
|
115
|
+
<div className="absolute top-0 left-0 right-0 h-32 bg-linear-to-b from-amber-500/10 via-orange-500/5 to-transparent pointer-events-none" />
|
|
116
|
+
|
|
117
|
+
{/* Header */}
|
|
118
|
+
<div className="relative flex items-center justify-between p-4 border-b border-amber-500/10">
|
|
119
|
+
<div className="flex items-center gap-3">
|
|
120
|
+
<div className="w-10 h-10 rounded-2xl bg-linear-to-br from-amber-400 via-orange-500 to-rose-500 flex items-center justify-center shadow-lg shadow-orange-500/30 rotate-3 hover:rotate-0 transition-transform">
|
|
121
|
+
<span className="text-lg">🌴</span>
|
|
122
|
+
</div>
|
|
123
|
+
<div>
|
|
124
|
+
<h3 className="font-bold text-amber-100 text-base tracking-tight">
|
|
125
|
+
Vacay Assistant
|
|
126
|
+
</h3>
|
|
127
|
+
{postTitle && (
|
|
128
|
+
<p className="text-xs text-amber-300/50 truncate max-w-[220px]">
|
|
129
|
+
📍 {postTitle}
|
|
130
|
+
</p>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
<button
|
|
135
|
+
onClick={handleToggle}
|
|
136
|
+
className="text-amber-300/50 hover:text-amber-100 transition-colors p-2 hover:bg-white/5 rounded-xl"
|
|
137
|
+
>
|
|
138
|
+
<X className="w-5 h-5" />
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Messages */}
|
|
143
|
+
<Messages messages={messages} />
|
|
144
|
+
|
|
145
|
+
{/* Loading indicator */}
|
|
146
|
+
{isLoading && (
|
|
147
|
+
<div className="px-4 py-3 border-t border-amber-500/10">
|
|
148
|
+
<div className="flex items-center gap-2 text-amber-400/80 text-xs">
|
|
149
|
+
<div className="flex gap-1">
|
|
150
|
+
<span className="w-2 h-2 bg-amber-400 rounded-full animate-bounce [animation-delay:-0.3s]"></span>
|
|
151
|
+
<span className="w-2 h-2 bg-orange-400 rounded-full animate-bounce [animation-delay:-0.15s]"></span>
|
|
152
|
+
<span className="w-2 h-2 bg-rose-400 rounded-full animate-bounce"></span>
|
|
153
|
+
</div>
|
|
154
|
+
<span className="font-medium">Planning your adventure...</span>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
{/* Input */}
|
|
160
|
+
<div className="relative p-4 border-t border-amber-500/10 bg-slate-900/50">
|
|
161
|
+
<form
|
|
162
|
+
onSubmit={(e) => {
|
|
163
|
+
e.preventDefault()
|
|
164
|
+
handleSend()
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
<div className="relative">
|
|
168
|
+
<textarea
|
|
169
|
+
value={input}
|
|
170
|
+
onChange={(e) => setInput(e.target.value)}
|
|
171
|
+
placeholder="Ask about Hawaii adventures..."
|
|
172
|
+
disabled={isLoading}
|
|
173
|
+
className="w-full rounded-2xl border border-amber-500/20 bg-slate-800/50 pl-4 pr-12 py-3 text-sm text-amber-50 placeholder-amber-200/30 focus:outline-none focus:ring-2 focus:ring-amber-500/40 focus:border-transparent resize-none overflow-hidden disabled:opacity-50 transition-all"
|
|
174
|
+
rows={1}
|
|
175
|
+
style={{ minHeight: '48px', maxHeight: '100px' }}
|
|
176
|
+
onInput={(e) => {
|
|
177
|
+
const target = e.target as HTMLTextAreaElement
|
|
178
|
+
target.style.height = 'auto'
|
|
179
|
+
target.style.height = Math.min(target.scrollHeight, 100) + 'px'
|
|
180
|
+
}}
|
|
181
|
+
onKeyDown={(e) => {
|
|
182
|
+
if (
|
|
183
|
+
e.key === 'Enter' &&
|
|
184
|
+
!e.shiftKey &&
|
|
185
|
+
input.trim() &&
|
|
186
|
+
!isLoading
|
|
187
|
+
) {
|
|
188
|
+
e.preventDefault()
|
|
189
|
+
handleSend()
|
|
190
|
+
}
|
|
191
|
+
}}
|
|
192
|
+
/>
|
|
193
|
+
<button
|
|
194
|
+
type="submit"
|
|
195
|
+
disabled={!input.trim() || isLoading}
|
|
196
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 p-2 rounded-xl bg-linear-to-r from-amber-500 to-orange-500 text-white disabled:opacity-30 disabled:bg-gray-600 disabled:from-gray-600 disabled:to-gray-600 transition-all hover:shadow-lg hover:shadow-amber-500/20"
|
|
197
|
+
>
|
|
198
|
+
<Send className="w-4 h-4" />
|
|
199
|
+
</button>
|
|
200
|
+
</div>
|
|
201
|
+
</form>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router";
|
|
2
|
+
|
|
3
|
+
import { type Post } from "content-collections";
|
|
4
|
+
|
|
5
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
6
|
+
|
|
7
|
+
export default function BlogPosts({
|
|
8
|
+
title,
|
|
9
|
+
posts,
|
|
10
|
+
}: {
|
|
11
|
+
title: string;
|
|
12
|
+
posts: Post[];
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<div>
|
|
16
|
+
<div className="container mx-auto max-w-7xl">
|
|
17
|
+
<h1 className="mb-16 text-7xl font-bold text-teal-900 text-center font-serif italic">
|
|
18
|
+
{title}
|
|
19
|
+
</h1>
|
|
20
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 px-4">
|
|
21
|
+
{posts.map((post, index) => (
|
|
22
|
+
<Link
|
|
23
|
+
to={`/posts/${post.slug}`}
|
|
24
|
+
key={post._meta.path}
|
|
25
|
+
className="group relative block"
|
|
26
|
+
>
|
|
27
|
+
<Card
|
|
28
|
+
className={`relative overflow-hidden transform transition-all duration-500 bg-white
|
|
29
|
+
${
|
|
30
|
+
index % 2 === 0
|
|
31
|
+
? "rotate-[-1deg] hover:rotate-[-2deg]"
|
|
32
|
+
: "rotate-[1deg] hover:rotate-[2deg]"
|
|
33
|
+
}
|
|
34
|
+
hover:scale-105 hover:z-10 hover:shadow-[0_20px_40px_rgba(0,0,0,0.25)]
|
|
35
|
+
before:absolute before:inset-0 before:z-10 before:border-[12px] before:border-white
|
|
36
|
+
after:absolute after:inset-0 after:z-0 after:bg-[radial-gradient(#00000005_1px,transparent_1px)] after:bg-[length:4px_4px]`}
|
|
37
|
+
>
|
|
38
|
+
<div className="relative z-20">
|
|
39
|
+
{/* Postcard Stamp */}
|
|
40
|
+
<div className="absolute top-4 right-4 w-16 h-20 border-2 border-dashed border-teal-900/20 rounded-sm" />
|
|
41
|
+
|
|
42
|
+
{/* Image Container */}
|
|
43
|
+
<div className="aspect-[3/2] relative overflow-hidden">
|
|
44
|
+
<div className="absolute inset-0 bg-black/10 z-10 transition-opacity group-hover:opacity-0" />
|
|
45
|
+
<img
|
|
46
|
+
src={`/${post.image}`}
|
|
47
|
+
alt={post.title}
|
|
48
|
+
className="object-cover w-full h-full"
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* Content */}
|
|
53
|
+
<CardContent className="p-6 bg-transparent">
|
|
54
|
+
<div className="space-y-4">
|
|
55
|
+
<h3 className="text-3xl font-serif italic text-teal-900 group-hover:text-teal-800 transition-colors">
|
|
56
|
+
{post.title}
|
|
57
|
+
</h3>
|
|
58
|
+
<p className="text-teal-800/80 font-serif">
|
|
59
|
+
{post.summary}
|
|
60
|
+
</p>
|
|
61
|
+
<p className="text-teal-700/60 text-sm italic font-serif">
|
|
62
|
+
{post.date}
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
</CardContent>
|
|
66
|
+
|
|
67
|
+
{/* Decorative Lines */}
|
|
68
|
+
<div className="absolute left-0 right-0 bottom-0 h-1 bg-[repeating-linear-gradient(45deg,transparent,transparent_2px,#0F766E20_2px,#0F766E20_4px)]" />
|
|
69
|
+
<div className="absolute left-0 top-0 bottom-0 w-1 bg-[repeating-linear-gradient(0deg,transparent,transparent_2px,#0F766E20_2px,#0F766E20_4px)]" />
|
|
70
|
+
</div>
|
|
71
|
+
</Card>
|
|
72
|
+
</Link>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
data-slot="card"
|
|
9
|
+
className={cn(
|
|
10
|
+
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
data-slot="card-header"
|
|
22
|
+
className={cn(
|
|
23
|
+
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
data-slot="card-title"
|
|
35
|
+
className={cn("leading-none font-semibold", className)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
data-slot="card-description"
|
|
45
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
data-slot="card-action"
|
|
55
|
+
className={cn(
|
|
56
|
+
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
57
|
+
className
|
|
58
|
+
)}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
data-slot="card-content"
|
|
68
|
+
className={cn("px-6", className)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
data-slot="card-footer"
|
|
78
|
+
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
Card,
|
|
86
|
+
CardHeader,
|
|
87
|
+
CardFooter,
|
|
88
|
+
CardTitle,
|
|
89
|
+
CardAction,
|
|
90
|
+
CardDescription,
|
|
91
|
+
CardContent,
|
|
92
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fetchServerSentEvents,
|
|
3
|
+
useChat,
|
|
4
|
+
createChatClientOptions,
|
|
5
|
+
} from "@tanstack/ai-react";
|
|
6
|
+
import type { InferChatMessages } from "@tanstack/ai-react";
|
|
7
|
+
|
|
8
|
+
// Default chat options for type inference
|
|
9
|
+
const defaultChatOptions = createChatClientOptions({
|
|
10
|
+
connection: fetchServerSentEvents("/api/blog-chat"),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export type BlogChatMessages = InferChatMessages<typeof defaultChatOptions>;
|
|
14
|
+
|
|
15
|
+
export const useBlogChat = (slug?: string) => {
|
|
16
|
+
const chatOptions = createChatClientOptions({
|
|
17
|
+
connection: fetchServerSentEvents("/api/blog-chat", {
|
|
18
|
+
body: {
|
|
19
|
+
slug,
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return useChat(chatOptions);
|
|
25
|
+
};
|