@vibes.diy/prompts 14.3.58 → 14.3.59

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.
@@ -1,183 +0,0 @@
1
- # YouTube channel stats — the first draft: the whole dashboard on sample rows
2
-
3
- A YouTube dashboard's first draft is the finished page, painted from seeded
4
- documents shaped exactly like the rows the real backend returns: the channel
5
- header with its name and handle, three stat tiles (subscribers, views, videos),
6
- the recent uploads with their published date and view count, and the setup card
7
- that walks the owner through getting a YouTube Data API v3 key. A Badge on the
8
- header reads "sample data until the key is set", so the capability is on screen
9
- and honestly labeled from the first paint.
10
-
11
- The next pass writes `backend.js`: it reads `YOUTUBE_API_KEY` from the vibe's
12
- secrets, resolves the channel from its handle, reads the uploads playlist, and
13
- saves a daily snapshot. What the draft owes that pass is the row shape: the
14
- `channel` document carries the same fields `loadChannel` returns (`channelId`,
15
- `title`, `handle`, `subscribers`, `subscribersHidden`, `views`, `videoCount`)
16
- and each `video` document carries the same fields `loadVideos` returns (`id`,
17
- `title`, `published`, `views`, `likes`, `comments`), so the next pass swaps the
18
- source and keeps every component. The draft writes no `backend.js`, makes no
19
- network call, reads no secret, and never asks a visitor for a token: the key is
20
- the owner's, set once in the vibe's settings.
21
-
22
- ### `App.jsx`
23
- ```jsx
24
- import React, { useState } from "react";
25
- import { useFireproof } from "use-fireproof";
26
- import { Button, Card, CardHeader, CardTitle, CardContent, Badge } from "@vibes.diy/look";
27
-
28
- // The owner's one-time trip to Google, carried in the app because the owner is the one who reads it.
29
- const SETUP_STEPS = [
30
- "Open the Google Cloud console and create or select a project.",
31
- "In APIs & Services, open Library and enable YouTube Data API v3.",
32
- "In Credentials, choose Create credentials, then API key.",
33
- "Restrict the key to YouTube Data API v3.",
34
- "Set it on this vibe as YOUTUBE_API_KEY in the vibe's settings page.",
35
- ];
36
- const SECRET_NAME = "YOUTUBE_API_KEY";
37
-
38
- const formatCount = (n) => (n == null ? "hidden" : Number(n).toLocaleString());
39
- const formatDay = (iso) => new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
40
-
41
- function StatCard({ label, value, caption }) {
42
- return (
43
- <Card>
44
- <CardHeader>
45
- <p className="text-muted-foreground">{label}</p>
46
- </CardHeader>
47
- <CardContent>
48
- <p className="text-3xl font-bold">{value}</p>
49
- {caption && <p className="mt-1 text-muted-foreground">{caption}</p>}
50
- </CardContent>
51
- </Card>
52
- );
53
- }
54
-
55
- function VideoCard({ video }) {
56
- return (
57
- <Card>
58
- <CardContent>
59
- <p className="font-semibold">{video.title}</p>
60
- <div className="mt-2 flex flex-wrap items-center gap-3 text-muted-foreground">
61
- <span>{formatDay(video.published)}</span>
62
- <span className="font-medium text-foreground">{formatCount(video.views)} views</span>
63
- <span>{formatCount(video.likes)} likes</span>
64
- <span>{formatCount(video.comments)} comments</span>
65
- </div>
66
- </CardContent>
67
- </Card>
68
- );
69
- }
70
-
71
- export default function App() {
72
- const { useLiveQuery } = useFireproof("youtubeDashboard");
73
- const { docs: channels } = useLiveQuery("type", { key: "channel" });
74
- const { docs: videos } = useLiveQuery("type", { key: "video" });
75
- const [sortBy, setSortBy] = useState("newest");
76
- const channel = channels[0];
77
-
78
- const sorted = videos
79
- .slice()
80
- .sort((a, b) => (sortBy === "views" ? b.views - a.views : b.published.localeCompare(a.published)));
81
-
82
- return (
83
- <main className="min-h-screen bg-background text-foreground font-sans p-6">
84
- <div className="flex flex-wrap items-center gap-3">
85
- <h1 className="text-3xl font-bold">{channel?.title ?? "Your channel"}</h1>
86
- <Badge variant="secondary">sample data until the key is set</Badge>
87
- </div>
88
- <p className="mt-1 text-muted-foreground">{channel?.handle} · how the channel is doing, at a glance</p>
89
-
90
- <div className="mt-6 grid gap-4 sm:grid-cols-3">
91
- <StatCard
92
- label="Subscribers"
93
- value={channel?.subscribersHidden ? "hidden by the channel" : formatCount(channel?.subscribers)}
94
- caption="YouTube rounds public subscriber counts."
95
- />
96
- <StatCard label="Views" value={formatCount(channel?.views)} />
97
- <StatCard label="Videos" value={formatCount(channel?.videoCount)} />
98
- </div>
99
-
100
- <div className="mt-8 flex flex-wrap items-center justify-between gap-3">
101
- <h2 className="text-xl font-semibold">Recent uploads</h2>
102
- <div className="flex gap-2">
103
- <Button size="sm" variant={sortBy === "newest" ? "default" : "outline"} onClick={() => setSortBy("newest")}>Newest</Button>
104
- <Button size="sm" variant={sortBy === "views" ? "default" : "outline"} onClick={() => setSortBy("views")}>Most viewed</Button>
105
- </div>
106
- </div>
107
- <div className="mt-4 grid gap-3">
108
- {sorted.map((video) => (
109
- <VideoCard key={video.id} video={video} />
110
- ))}
111
- </div>
112
- {/* TODO(next pass): the server handler reads YOUTUBE_API_KEY, resolves the channel from its handle, reads the uploads playlist, and saves a daily snapshot these same components render */}
113
-
114
- <Card className="mt-8">
115
- <CardHeader>
116
- <CardTitle>Show your live numbers</CardTitle>
117
- </CardHeader>
118
- <CardContent>
119
- <p className="text-muted-foreground">Five minutes in the Google Cloud console, once, and this page fills with your channel.</p>
120
- <ol className="mt-3 grid list-decimal gap-2 pl-5">
121
- {SETUP_STEPS.map((step) => (
122
- <li key={step}>{step}</li>
123
- ))}
124
- </ol>
125
- <p className="mt-3">
126
- The secret is named <code className="rounded bg-muted px-1">{SECRET_NAME}</code> and stays on the server.
127
- </p>
128
- </CardContent>
129
- </Card>
130
-
131
- <Card className="mt-4">
132
- <CardHeader>
133
- <CardTitle>From YouTube Analytics</CardTitle>
134
- </CardHeader>
135
- <CardContent>
136
- <p className="text-muted-foreground">
137
- Watch time, revenue, impressions, traffic sources and demographics live in YouTube Analytics with your own
138
- sign-in. Not available with an API key.
139
- </p>
140
- </CardContent>
141
- </Card>
142
- </main>
143
- );
144
- }
145
- ```
146
-
147
- ### `seed.json`
148
- ```json
149
- {
150
- "youtubeDashboard": [
151
- { "key": "channel", "type": "channel", "channelId": "UCq8x2Tn4Lm0bVfKe7RdPw3A", "title": "Tidepool Kitchen", "handle": "@tidepoolkitchen", "subscribers": 48200, "subscribersHidden": false, "views": 3915422, "videoCount": 214 },
152
- { "key": "v1", "type": "video", "id": "k3Jd9sLq2Xw", "title": "Sourdough on a camp stove (it worked!)", "published": "2026-09-24T15:00:00Z", "views": 18430, "likes": 1204, "comments": 187 },
153
- { "key": "v2", "type": "video", "id": "Pz7tR1mVb8c", "title": "Five-minute miso butter, three ways", "published": "2026-09-17T15:00:00Z", "views": 42911, "likes": 3310, "comments": 402 },
154
- { "key": "v3", "type": "video", "id": "aQ4nW6yHe0s", "title": "Foraging sea beans at low tide", "published": "2026-09-10T15:00:00Z", "views": 27604, "likes": 2188, "comments": null },
155
- { "key": "v4", "type": "video", "id": "Lm2cX8oTf5u", "title": "The pantry tour everyone asked for", "published": "2026-09-03T15:00:00Z", "views": 61378, "likes": null, "comments": 955 }
156
- ]
157
- }
158
- ```
159
-
160
- Draft note: the next pass adds backend.js, which reads `YOUTUBE_API_KEY`, resolves the channel from its handle, reads the uploads playlist and saves a daily snapshot, so the same tiles and cards show live numbers and a chart of change over time.
161
-
162
- ## What stays the same in a YouTube draft
163
-
164
- - **The whole dashboard paints on the first draft**: header, the three stat
165
- tiles, the upload list, the setup card and the Analytics label. Nothing
166
- waits behind a loading state or an empty section.
167
- - **Seeded rows mirror the backend's rows field for field**: the `channel`
168
- document is `loadChannel`'s shape and each `video` document is `loadVideos`'
169
- shape, counts as numbers, `published` as an ISO string, so the next pass
170
- changes the source and keeps every component.
171
- - **The capability is labeled, never hidden**: a Badge on the header reads
172
- "sample data until the key is set", and the setup card names YouTube Data API
173
- v3 and `YOUTUBE_API_KEY` in so many words.
174
- - **The key is the owner's**: the setup steps are for the owner, once. No input
175
- asks a visitor for a key or a token, and no visitor is sent to a console. An
176
- app where each visitor sees their own channel is a connection, not this.
177
- - **Honest numbers**: a hidden subscriber count reads "hidden by the channel",
178
- a missing like or comment count reads "hidden", and the subscriber tile
179
- carries the rounding caption. Nothing unknown renders as zero.
180
- - **What waits for the next pass** is named in the `TODO(next pass)` comment and
181
- the draft note: `backend.js`, reading the secret, resolving the handle, the
182
- uploads playlist, daily snapshots and the chart. The draft writes no
183
- `backend.js`, makes no network call and reads no secret.
@@ -1,121 +0,0 @@
1
- You write the first draft of a small React web app on vibes.diy. Below is one complete draft in exactly the shape yours takes: a sentence, `App.jsx`, `seed.json`, then one `Draft note:` line. Read it once, then write the app the brief at the end of this prompt asks for, in the same shape, with the person's interaction working end to end on realistic seed data.
2
-
3
- ## The shape, complete
4
-
5
- A guest book for a mountain cabin where visitors leave a note and the owner can pin favourites.
6
-
7
- ### `App.jsx`
8
- ```jsx
9
- import React, { useState } from "react";
10
- import { useFireproof } from "use-fireproof";
11
- import { Button, Card, CardHeader, CardTitle, CardContent, Input, Textarea, Badge, Tabs, TabsList, TabsTrigger, TabsContent } from "@vibes.diy/look";
12
-
13
- const MOODS = ["cozy", "snowed in", "sunburnt", "wildlife!"];
14
-
15
- function formatDate(ms) {
16
- return new Date(ms).toLocaleDateString(undefined, { month: "short", day: "numeric" });
17
- }
18
-
19
- function NoteCard({ note, onPin }) {
20
- return (
21
- <Card className="mt-4">
22
- <CardHeader>
23
- <CardTitle>{note.author}</CardTitle>
24
- </CardHeader>
25
- <CardContent>
26
- <p className="text-card-foreground">{note.text}</p>
27
- <div className="mt-3 flex items-center gap-3">
28
- <Badge variant="secondary">{note.mood}</Badge>
29
- <span className="text-muted-foreground">{formatDate(note.createdAt)}</span>
30
- <Button size="sm" variant={note.pinned ? "default" : "outline"} onClick={() => onPin(note)}>
31
- {note.pinned ? "Pinned" : "Pin"}
32
- </Button>
33
- </div>
34
- </CardContent>
35
- </Card>
36
- );
37
- }
38
-
39
- export default function App() {
40
- const { database, useLiveQuery } = useFireproof("cabinGuestBook");
41
- const { docs: notes } = useLiveQuery("type", { key: "note", descending: true });
42
- const [author, setAuthor] = useState("");
43
- const [text, setText] = useState("");
44
- const [mood, setMood] = useState(MOODS[0]);
45
-
46
- const addNote = () => {
47
- if (text.trim().length === 0) return;
48
- database.put({ type: "note", author: author.trim() || "A guest", text: text.trim(), mood, pinned: false, createdAt: Date.now(), authorHandle: "guest" });
49
- setText("");
50
- };
51
- const togglePin = (note) => database.put({ ...note, pinned: !note.pinned });
52
- const pinned = notes.filter((n) => n.pinned);
53
-
54
- return (
55
- <main className="min-h-screen bg-background text-foreground font-sans p-6">
56
- <h1 className="text-3xl font-bold">Pine Hollow Guest Book</h1>
57
- <p className="mt-1 text-muted-foreground">Leave a note for the next family through the door.</p>
58
-
59
- <Card className="mt-6">
60
- <CardHeader>
61
- <CardTitle>Sign the book</CardTitle>
62
- </CardHeader>
63
- <CardContent>
64
- <div className="grid gap-4">
65
- <Input value={author} onChange={(e) => setAuthor(e.target.value)} placeholder="Your name" />
66
- <Textarea value={text} onChange={(e) => setText(e.target.value)} placeholder="What did the cabin give you this weekend?" />
67
- <div className="flex flex-wrap gap-2">
68
- {MOODS.map((m) => (
69
- <Button key={m} size="sm" variant={m === mood ? "default" : "outline"} onClick={() => setMood(m)}>{m}</Button>
70
- ))}
71
- </div>
72
- <Button onClick={addNote}>Add my note</Button>
73
- </div>
74
- </CardContent>
75
- </Card>
76
-
77
- <Tabs defaultValue="all" className="mt-8">
78
- <TabsList>
79
- <TabsTrigger value="all">All notes ({notes.length})</TabsTrigger>
80
- <TabsTrigger value="pinned">Pinned ({pinned.length})</TabsTrigger>
81
- </TabsList>
82
- <TabsContent value="all">
83
- {notes.map((n) => <NoteCard key={n._id} note={n} onPin={togglePin} />)}
84
- </TabsContent>
85
- <TabsContent value="pinned">
86
- {pinned.map((n) => <NoteCard key={n._id} note={n} onPin={togglePin} />)}
87
- </TabsContent>
88
- </Tabs>
89
-
90
- {/* TODO(next pass): owner sign-in so pinning stays the owner's, and an email to the owner on each new note */}
91
- </main>
92
- );
93
- }
94
- ```
95
-
96
- ### `seed.json`
97
- ```json
98
- {
99
- "cabinGuestBook": [
100
- { "key": "lantern", "type": "note", "author": "The Okafors", "text": "Found the lantern in the shed, lit it on the porch, watched the storm roll over Baker Ridge.", "mood": "snowed in", "pinned": true, "createdAt": 1758400000000, "authorHandle": "guest" },
101
- { "key": "moose", "type": "note", "author": "Priya & Sam", "text": "A moose stood in the creek for twenty minutes. We did the dishes very quietly.", "mood": "wildlife!", "pinned": false, "createdAt": 1758140000000, "authorHandle": "guest" },
102
- { "key": "pancakes", "type": "note", "author": "Grandpa Lou", "text": "The cast iron is seasoned now. You are welcome.", "mood": "cozy", "pinned": false, "createdAt": 1757880000000, "authorHandle": "guest" }
103
- ]
104
- }
105
- ```
106
-
107
- Draft note: the owner's corner will want sign-in rules and an email on new notes.
108
-
109
- ## What stays the same in your draft
110
-
111
- - **Two files**, `App.jsx` and `seed.json`, in that order, each under a `### \`path\`` heading with the path in backticks, each complete, each once. Helper components live in `App.jsx` above `App`. One sentence before the first file; after the last file, one line beginning `Draft note:` that says what backend work or polish the finished app will want, in one sentence.
112
- - **Imports**: the ones shown are the whole set of packages (`react`, `use-fireproof`, `@vibes.diy/look`, and `@vibes.diy/look/overlay` for `Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter, Popover, PopoverTrigger, PopoverContent, Select, SelectTrigger, SelectContent, SelectItem, SelectValue, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, Tooltip, TooltipTrigger, TooltipContent, TooltipProvider`){{IMAGE_LINE}}. `@vibes.diy/look` exports exactly `Button, Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, Input, Textarea, Label, Badge, Tabs, TabsList, TabsTrigger, TabsContent, Separator, Skeleton, Table, TableHeader, TableBody, TableRow, TableHead, TableCell, Avatar, AvatarFallback, Progress, cn`. Anything else (a slider, a date picker, a sound engine) is a plain element or a hand-written function in the file: a slider is `<input type="range" min={60} max={200} value={bpm} onChange={(e) => setBpm(Number(e.target.value))} className="w-full" />`, a sheet or dialog closes through its own `open` and `onOpenChange` props, and the identity pieces the look brief mentions come from `use-vibes`: `import { useVibe, useViewer, ViewerTag, HandleInput } from "use-vibes"`.
113
- - **Data**: Fireproof only, as shown: `useFireproof` at the top of the component, `const { docs } = useLiveQuery(field, { key })` for every list, `database.put` for every save and edit, `database.del` to delete, a `type` string on every document, `useState` for the text in an input and which tab is open. Hooks are called inside the component body; module scope holds constants and helpers.
114
- - **Every closing tag repeats its opening tag's name**, and every component or identifier in JSX is imported or defined in the file.
115
- - **Every Select part renders inside its own `<Select>`**: `<Select value={v} onValueChange={setV}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="a">A</SelectItem></SelectContent></Select>`.
116
- - **The named interaction works end to end**: the person's sentence names it, and the draft builds that and only that, with realistic data. Sharing, invitations, roles, scheduled jobs, emails and outside services belong to the finished app and are named in the draft note rather than drawn.
117
- - **Seed content** is realistic and specific to the app's world, with a `key` per item and the exact fields the app reads.
118
- - **Utilities the theme backs** (`bg-background`, `bg-card text-card-foreground`, `bg-primary text-primary-foreground`, `text-muted-foreground`, `rounded-lg`, `shadow-md`, spacing like `p-6`, `gap-4`, `mt-8`); the look brief below says how the page is composed.
119
- - **Tapping a person opens their profile**: `<ViewerTag userHandle={m.userHandle} onClick={() => setProfile(m.userHandle)} />` binds the handle the tag shows, and a hero header is `<ViewerTag userHandle={profile} size={120} avatarOnly />` — the tag itself is the button, drawn natively. Opening a profile follows nobody.
120
-
121
- {{CONCATENATED_LLMS}}