create-atsdc-stack 1.0.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/app/README.md ADDED
@@ -0,0 +1,251 @@
1
+ # ATSDC Stack Application
2
+
3
+ This is the main Astro application for the ATSDC Stack.
4
+
5
+ ## 🚀 Quick Start
6
+
7
+ ### Prerequisites
8
+
9
+ - Node.js >= 18.0.0
10
+ - PostgreSQL database (Vercel Postgres, Neon, or local)
11
+ - API keys for Clerk, OpenAI, and optionally Exa
12
+
13
+ ### Installation
14
+
15
+ ```bash
16
+ # Install dependencies
17
+ npm install
18
+
19
+ # Copy environment template
20
+ cp .env.example .env
21
+
22
+ # Configure your .env file with your credentials
23
+ ```
24
+
25
+ ### Environment Variables
26
+
27
+ Create a `.env` file with the following variables:
28
+
29
+ ```env
30
+ # Database
31
+ DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
32
+
33
+ # Clerk Authentication
34
+ PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_..."
35
+ CLERK_SECRET_KEY="sk_test_..."
36
+
37
+ # OpenAI (for Vercel AI SDK)
38
+ OPENAI_API_KEY="sk-..."
39
+
40
+ # Exa Search (optional)
41
+ EXA_API_KEY="..."
42
+ ```
43
+
44
+ ### Database Setup
45
+
46
+ ```bash
47
+ # Push schema to database
48
+ npm run db:push
49
+
50
+ # Or generate migrations
51
+ npm run db:generate
52
+ npm run db:migrate
53
+
54
+ # Open Drizzle Studio (database GUI)
55
+ npm run db:studio
56
+ ```
57
+
58
+ ### Development
59
+
60
+ ```bash
61
+ # Start dev server
62
+ npm run dev
63
+ ```
64
+
65
+ Visit `http://localhost:4321`
66
+
67
+ ## 📝 Available Scripts
68
+
69
+ - `npm run dev` - Start development server
70
+ - `npm run build` - Build for production
71
+ - `npm run preview` - Preview production build
72
+ - `npm run astro` - Run Astro CLI commands
73
+ - `npm run db:generate` - Generate database migrations
74
+ - `npm run db:migrate` - Run database migrations
75
+ - `npm run db:push` - Push schema changes to database
76
+ - `npm run db:studio` - Open Drizzle Studio
77
+
78
+ ## 📁 Project Structure
79
+
80
+ ```text
81
+ src/
82
+ ├── components/ # Reusable Astro components
83
+ ├── db/ # Database schema and client
84
+ │ ├── initialize.ts # Database initialization
85
+ │ ├── schema.ts # Drizzle ORM schemas
86
+ │ └── validations.ts # Zod validation schemas
87
+ ├── layouts/ # Page layouts
88
+ │ └── Layout.astro
89
+ ├── lib/ # Utility libraries
90
+ │ ├── config.ts # App configuration
91
+ │ ├── content-converter.ts # Markdown/HTML conversion
92
+ │ ├── dom-utils.ts # DOM manipulation
93
+ │ └── exa-search.ts # AI-powered search
94
+ ├── pages/ # Routes and pages
95
+ │ ├── api/ # API endpoints
96
+ │ │ ├── chat.ts # AI chat endpoint
97
+ │ │ └── posts.ts # Posts CRUD
98
+ │ └── index.astro # Home page
99
+ └── styles/ # SCSS stylesheets
100
+ ├── variables/ # SCSS variables and mixins
101
+ ├── components/ # Component styles
102
+ ├── pages/ # Page styles
103
+ ├── reset.scss # CSS reset
104
+ └── global.scss # Global styles
105
+ ```
106
+
107
+ ## 🎨 SCSS Architecture
108
+
109
+ This app uses a strict SCSS architecture:
110
+
111
+ - **No inline `<style>` tags** in `.astro` files
112
+ - **All styles in external SCSS files** for better maintainability
113
+ - **Data attributes for modifiers** (preferred over BEM)
114
+ - **Semantic class names** (no utility classes)
115
+
116
+ Example:
117
+
118
+ ```astro
119
+ ---
120
+ import '@/styles/components/button.scss';
121
+ ---
122
+ <button class="btn" data-variant="primary" data-size="lg">
123
+ Click Me
124
+ </button>
125
+ ```
126
+
127
+ ## 🗄️ Database
128
+
129
+ ### Schema Definition
130
+
131
+ Define your database schema in `src/db/schema.ts` using Drizzle ORM:
132
+
133
+ ```typescript
134
+ export const posts = pgTable('posts', {
135
+ id: varchar('id', { length: 21 })
136
+ .primaryKey()
137
+ .$defaultFn(() => nanoid()),
138
+ title: varchar('title', { length: 255 }).notNull(),
139
+ content: text('content').notNull(),
140
+ createdAt: timestamp('created_at').defaultNow().notNull(),
141
+ });
142
+ ```
143
+
144
+ ### Validation
145
+
146
+ Define Zod schemas in `src/db/validations.ts`:
147
+
148
+ ```typescript
149
+ export const createPostSchema = z.object({
150
+ title: z.string().min(1).max(255),
151
+ content: z.string().min(1),
152
+ });
153
+ ```
154
+
155
+ ## 🔐 Authentication
156
+
157
+ Authentication is handled by Clerk. Configure in `astro.config.mjs`:
158
+
159
+ ```javascript
160
+ clerk({
161
+ afterSignInUrl: '/',
162
+ afterSignUpUrl: '/',
163
+ })
164
+ ```
165
+
166
+ ## 🤖 AI Features
167
+
168
+ ### Vercel AI SDK
169
+
170
+ Chat endpoint example in `src/pages/api/chat.ts`:
171
+
172
+ ```typescript
173
+ import { OpenAI } from 'ai';
174
+
175
+ export const POST: APIRoute = async ({ request }) => {
176
+ // AI chat implementation
177
+ };
178
+ ```
179
+
180
+ ### Exa Search
181
+
182
+ AI-powered search utilities in `src/lib/exa-search.ts`.
183
+
184
+ ## 📱 Progressive Web App
185
+
186
+ This app includes PWA support with offline capabilities:
187
+
188
+ - Service worker auto-generated
189
+ - Installable on mobile/desktop
190
+ - Offline caching configured in `astro.config.mjs`
191
+
192
+ ## 🚀 Deployment
193
+
194
+ ### Vercel (Recommended)
195
+
196
+ ```bash
197
+ # Install Vercel CLI
198
+ npm i -g vercel
199
+
200
+ # Deploy
201
+ vercel
202
+ ```
203
+
204
+ Make sure to set these environment variables in your Vercel project settings:
205
+
206
+ - `DATABASE_URL`
207
+ - `PUBLIC_CLERK_PUBLISHABLE_KEY`
208
+ - `CLERK_SECRET_KEY`
209
+ - `OPENAI_API_KEY`
210
+ - `EXA_API_KEY` (optional)
211
+
212
+ ## 📚 Documentation
213
+
214
+ - [Astro Documentation](https://docs.astro.build)
215
+ - [Drizzle ORM](https://orm.drizzle.team)
216
+ - [Clerk](https://clerk.com/docs)
217
+ - [Vercel AI SDK](https://sdk.vercel.ai/docs)
218
+ - [Zod](https://zod.dev)
219
+ - [Exa Search](https://docs.exa.ai)
220
+
221
+ ## 🛠️ Utilities
222
+
223
+ ### Content Conversion
224
+
225
+ ```typescript
226
+ import { htmlToMarkdown, markdownToHtml } from '@/lib/content-converter';
227
+
228
+ const markdown = htmlToMarkdown('<h1>Hello</h1>');
229
+ const html = markdownToHtml('# Hello');
230
+ ```
231
+
232
+ ### DOM Manipulation
233
+
234
+ ```typescript
235
+ import { extractText, findLinks } from '@/lib/dom-utils';
236
+
237
+ const text = extractText(htmlString);
238
+ const links = findLinks(htmlString);
239
+ ```
240
+
241
+ ### AI Search
242
+
243
+ ```typescript
244
+ import { searchWithExa } from '@/lib/exa-search';
245
+
246
+ const results = await searchWithExa('your query');
247
+ ```
248
+
249
+ ## 📄 License
250
+
251
+ MIT
@@ -0,0 +1,83 @@
1
+ import { defineConfig } from 'astro/config';
2
+ import react from '@astrojs/react';
3
+ import vercel from '@astrojs/vercel';
4
+ import clerk from '@clerk/astro';
5
+ import { VitePWA } from 'vite-plugin-pwa';
6
+
7
+ // https://astro.build/config
8
+ export default defineConfig({
9
+ output: 'server',
10
+ adapter: vercel({
11
+ imageService: true,
12
+ }),
13
+ image: {
14
+ service: {
15
+ entrypoint: 'astro/assets/services/noop',
16
+ },
17
+ },
18
+ integrations: [
19
+ react(),
20
+ clerk({
21
+ afterSignInUrl: '/',
22
+ afterSignUpUrl: '/',
23
+ }),
24
+ ],
25
+ vite: {
26
+ plugins: [
27
+ VitePWA({
28
+ registerType: 'autoUpdate',
29
+ includeAssets: ['favicon.ico', 'apple-touch-icon.png', 'mask-icon.svg'],
30
+ manifest: {
31
+ name: 'ATSDC Stack App',
32
+ short_name: 'ATSDC',
33
+ description: 'Progressive Web App built with the ATSDC Stack',
34
+ theme_color: '#ffffff',
35
+ background_color: '#ffffff',
36
+ display: 'standalone',
37
+ icons: [
38
+ {
39
+ src: 'pwa-192x192.png',
40
+ sizes: '192x192',
41
+ type: 'image/png',
42
+ },
43
+ {
44
+ src: 'pwa-512x512.png',
45
+ sizes: '512x512',
46
+ type: 'image/png',
47
+ },
48
+ {
49
+ src: 'pwa-512x512.png',
50
+ sizes: '512x512',
51
+ type: 'image/png',
52
+ purpose: 'any maskable',
53
+ },
54
+ ],
55
+ },
56
+ workbox: {
57
+ globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
58
+ runtimeCaching: [
59
+ {
60
+ urlPattern: /^https:\/\/api\./i,
61
+ handler: 'NetworkFirst',
62
+ options: {
63
+ cacheName: 'api-cache',
64
+ expiration: {
65
+ maxEntries: 50,
66
+ maxAgeSeconds: 60 * 60 * 24, // 24 hours
67
+ },
68
+ },
69
+ },
70
+ ],
71
+ },
72
+ }),
73
+ ],
74
+ css: {
75
+ preprocessorOptions: {
76
+ scss: {
77
+ api: 'modern-compiler',
78
+ additionalData: `@use "@/styles/variables/globals.scss" as *;`,
79
+ },
80
+ },
81
+ },
82
+ },
83
+ });
@@ -0,0 +1,16 @@
1
+ import type { Config } from 'drizzle-kit';
2
+
3
+ if (!process.env.DATABASE_URL) {
4
+ throw new Error('DATABASE_URL environment variable is required');
5
+ }
6
+
7
+ export default {
8
+ schema: './src/db/schema.ts',
9
+ out: './drizzle',
10
+ dialect: 'postgresql',
11
+ dbCredentials: {
12
+ url: process.env.DATABASE_URL,
13
+ },
14
+ verbose: true,
15
+ strict: true,
16
+ } satisfies Config;
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "atsdc-app",
3
+ "version": "1.0.0",
4
+ "description": "ATSDC Stack - Astro application",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "astro dev",
8
+ "build": "astro check && astro build",
9
+ "preview": "astro preview",
10
+ "astro": "astro",
11
+ "db:generate": "drizzle-kit generate",
12
+ "db:migrate": "drizzle-kit migrate",
13
+ "db:push": "drizzle-kit push",
14
+ "db:studio": "drizzle-kit studio"
15
+ },
16
+ "dependencies": {
17
+ "@astrojs/check": "^0.9.6",
18
+ "@astrojs/react": "^4.4.2",
19
+ "@astrojs/vercel": "^9.0.2",
20
+ "@clerk/astro": "^1.4.3",
21
+ "@clerk/clerk-react": "^5.17.3",
22
+ "@rocicorp/zero": "^0.2.0",
23
+ "@vercel/postgres": "^0.10.0",
24
+ "ai": "^5.0.0",
25
+ "astro": "^5.16.6",
26
+ "cheerio": "^1.0.0",
27
+ "drizzle-orm": "^0.36.4",
28
+ "exa-js": "^1.1.1",
29
+ "marked": "^14.1.3",
30
+ "nanoid": "^5.0.9",
31
+ "open-props": "^1.7.17",
32
+ "react": "^18.3.1",
33
+ "react-dom": "^18.3.1",
34
+ "sass": "^1.82.0",
35
+ "turndown": "^7.2.0",
36
+ "typescript": "^5.7.2",
37
+ "zod": "^3.24.1"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^22.10.5",
41
+ "@types/react": "^18.3.18",
42
+ "@types/react-dom": "^18.3.5",
43
+ "@types/turndown": "^5.0.5",
44
+ "@vite-pwa/assets-generator": "^0.2.6",
45
+ "drizzle-kit": "^0.29.1",
46
+ "vercel": "^39.2.0",
47
+ "vite-plugin-pwa": "^0.21.2"
48
+ },
49
+ "engines": {
50
+ "node": ">=18.0.0"
51
+ }
52
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "ATSDC Stack App",
3
+ "short_name": "ATSDC",
4
+ "description": "Progressive Web App built with the ATSDC Stack",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#ffffff",
8
+ "theme_color": "#4f46e5",
9
+ "orientation": "portrait-primary",
10
+ "icons": [
11
+ {
12
+ "src": "/pwa-192x192.png",
13
+ "sizes": "192x192",
14
+ "type": "image/png",
15
+ "purpose": "any"
16
+ },
17
+ {
18
+ "src": "/pwa-512x512.png",
19
+ "sizes": "512x512",
20
+ "type": "image/png",
21
+ "purpose": "any"
22
+ },
23
+ {
24
+ "src": "/pwa-512x512.png",
25
+ "sizes": "512x512",
26
+ "type": "image/png",
27
+ "purpose": "maskable"
28
+ }
29
+ ],
30
+ "categories": [
31
+ "productivity",
32
+ "utilities"
33
+ ],
34
+ "screenshots": [],
35
+ "shortcuts": []
36
+ }
@@ -0,0 +1,36 @@
1
+ ---
2
+ /**
3
+ * Card Component
4
+ * Demonstrates proper SCSS architecture:
5
+ * - No <style> tag in this file
6
+ * - Styles imported from external SCSS file
7
+ * - Reusable component with props
8
+ */
9
+
10
+ interface Props {
11
+ title?: string;
12
+ href?: string;
13
+ class?: string;
14
+ }
15
+
16
+ const { title, href, class: className = '' } = Astro.props;
17
+
18
+ // Import component-specific styles
19
+ import '@/styles/components/card.scss';
20
+ ---
21
+
22
+ {href ? (
23
+ <a href={href} class={`card-component ${className}`}>
24
+ {title && <h3 class="card-component__title">{title}</h3>}
25
+ <div class="card-component__content">
26
+ <slot />
27
+ </div>
28
+ </a>
29
+ ) : (
30
+ <div class={`card-component ${className}`}>
31
+ {title && <h3 class="card-component__title">{title}</h3>}
32
+ <div class="card-component__content">
33
+ <slot />
34
+ </div>
35
+ </div>
36
+ )}
@@ -0,0 +1,107 @@
1
+ import { sql } from '@vercel/postgres';
2
+ import { drizzle } from 'drizzle-orm/vercel-postgres';
3
+ import * as schema from './schema';
4
+ import { posts, comments } from './schema';
5
+
6
+ /**
7
+ * Database client configuration
8
+ * Uses Vercel Postgres for production-ready connection pooling
9
+ */
10
+
11
+ // Create Drizzle instance with schema
12
+ export const db = drizzle(sql, { schema });
13
+
14
+ // Export schema for convenience
15
+ export { schema };
16
+
17
+ /**
18
+ * Database initialization utilities
19
+ * Handles table creation, migrations, and initial setup
20
+ */
21
+
22
+ /**
23
+ * Check if the database tables exist by attempting a simple query
24
+ */
25
+ export async function checkTablesExist(): Promise<boolean> {
26
+ try {
27
+ await db.select().from(posts).limit(1);
28
+ return true;
29
+ } catch (error) {
30
+ console.error('Error checking if tables exist:', error);
31
+ return false;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Initialize the database by creating tables if they don't exist
37
+ * Note: In production, use Drizzle migrations instead
38
+ */
39
+ export async function initializeDatabase(): Promise<void> {
40
+ try {
41
+ const tablesExist = await checkTablesExist();
42
+
43
+ if (!tablesExist) {
44
+ console.log('Database tables do not exist. Please run migrations using:');
45
+ console.log(' npm run db:push');
46
+ console.log(' or');
47
+ console.log(' npm run db:migrate');
48
+ throw new Error('Database tables not initialized');
49
+ }
50
+
51
+ console.log('Database tables verified successfully');
52
+ } catch (error) {
53
+ console.error('Database initialization failed:', error);
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Verify database connection
60
+ */
61
+ export async function verifyConnection(): Promise<boolean> {
62
+ try {
63
+ await db.select().from(posts).limit(1);
64
+ return true;
65
+ } catch (error) {
66
+ console.error('Database connection failed:', error);
67
+ return false;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Get database health status
73
+ */
74
+ export async function getDatabaseHealth(): Promise<{
75
+ connected: boolean;
76
+ tablesExist: boolean;
77
+ timestamp: Date;
78
+ }> {
79
+ const connected = await verifyConnection();
80
+ const tablesExist = connected ? await checkTablesExist() : false;
81
+
82
+ return {
83
+ connected,
84
+ tablesExist,
85
+ timestamp: new Date(),
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Seed initial data (optional - for development)
91
+ */
92
+ export async function seedDatabase(): Promise<void> {
93
+ try {
94
+ // Check if data already exists
95
+ const existingPosts = await db.select().from(posts).limit(1);
96
+
97
+ if (existingPosts.length > 0) {
98
+ console.log('Database already contains data, skipping seed');
99
+ return;
100
+ }
101
+
102
+ console.log('Database seeding completed');
103
+ } catch (error) {
104
+ console.error('Database seeding failed:', error);
105
+ throw error;
106
+ }
107
+ }
@@ -0,0 +1,72 @@
1
+ import { pgTable, text, timestamp, boolean, varchar } from 'drizzle-orm/pg-core';
2
+ import { nanoid } from 'nanoid';
3
+
4
+ /**
5
+ * Posts table schema
6
+ * Uses NanoID for primary keys for better URL-safe unique identifiers
7
+ */
8
+ export const posts = pgTable('posts', {
9
+ // Primary key using NanoID (21 characters, URL-safe)
10
+ id: varchar('id', { length: 21 })
11
+ .primaryKey()
12
+ .$defaultFn(() => nanoid()),
13
+
14
+ // Post content fields
15
+ title: varchar('title', { length: 255 }).notNull(),
16
+ slug: varchar('slug', { length: 255 }).notNull().unique(),
17
+ content: text('content').notNull(),
18
+ excerpt: text('excerpt'),
19
+
20
+ // Author information (Clerk user ID)
21
+ authorId: varchar('author_id', { length: 255 }).notNull(),
22
+ authorName: varchar('author_name', { length: 255 }),
23
+
24
+ // Post metadata
25
+ published: boolean('published').default(false).notNull(),
26
+ featured: boolean('featured').default(false).notNull(),
27
+
28
+ // SEO fields
29
+ metaTitle: varchar('meta_title', { length: 255 }),
30
+ metaDescription: text('meta_description'),
31
+
32
+ // Timestamps
33
+ createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
34
+ updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
35
+ publishedAt: timestamp('published_at', { withTimezone: true }),
36
+ });
37
+
38
+ /**
39
+ * Comments table schema
40
+ * Demonstrates relationship with posts using NanoID
41
+ */
42
+ export const comments = pgTable('comments', {
43
+ id: varchar('id', { length: 21 })
44
+ .primaryKey()
45
+ .$defaultFn(() => nanoid()),
46
+
47
+ // Foreign key to posts table
48
+ postId: varchar('post_id', { length: 21 })
49
+ .notNull()
50
+ .references(() => posts.id, { onDelete: 'cascade' }),
51
+
52
+ // Comment content
53
+ content: text('content').notNull(),
54
+
55
+ // Author information (Clerk user ID)
56
+ authorId: varchar('author_id', { length: 255 }).notNull(),
57
+ authorName: varchar('author_name', { length: 255 }),
58
+
59
+ // Moderation
60
+ approved: boolean('approved').default(false).notNull(),
61
+ flagged: boolean('flagged').default(false).notNull(),
62
+
63
+ // Timestamps
64
+ createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
65
+ updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
66
+ });
67
+
68
+ // Type exports for use in application
69
+ export type Post = typeof posts.$inferSelect;
70
+ export type NewPost = typeof posts.$inferInsert;
71
+ export type Comment = typeof comments.$inferSelect;
72
+ export type NewComment = typeof comments.$inferInsert;