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.
@@ -0,0 +1,158 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Zod validation schemas for database models
5
+ * Provides runtime type safety and validation for user inputs
6
+ */
7
+
8
+ // Slug validation helper
9
+ const slugSchema = z
10
+ .string()
11
+ .min(1, 'Slug is required')
12
+ .max(255, 'Slug must be 255 characters or less')
13
+ .regex(
14
+ /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
15
+ 'Slug must be lowercase alphanumeric with hyphens only'
16
+ );
17
+
18
+ /**
19
+ * Post validation schemas
20
+ */
21
+
22
+ // Schema for creating a new post
23
+ export const createPostSchema = z.object({
24
+ title: z
25
+ .string()
26
+ .min(1, 'Title is required')
27
+ .max(255, 'Title must be 255 characters or less')
28
+ .trim(),
29
+
30
+ slug: slugSchema,
31
+
32
+ content: z
33
+ .string()
34
+ .min(1, 'Content is required')
35
+ .max(50000, 'Content must be 50,000 characters or less'),
36
+
37
+ excerpt: z
38
+ .string()
39
+ .max(500, 'Excerpt must be 500 characters or less')
40
+ .optional()
41
+ .nullable(),
42
+
43
+ authorId: z.string().min(1, 'Author ID is required'),
44
+
45
+ authorName: z
46
+ .string()
47
+ .max(255, 'Author name must be 255 characters or less')
48
+ .optional()
49
+ .nullable(),
50
+
51
+ published: z.boolean().default(false),
52
+
53
+ featured: z.boolean().default(false),
54
+
55
+ metaTitle: z
56
+ .string()
57
+ .max(255, 'Meta title must be 255 characters or less')
58
+ .optional()
59
+ .nullable(),
60
+
61
+ metaDescription: z
62
+ .string()
63
+ .max(500, 'Meta description must be 500 characters or less')
64
+ .optional()
65
+ .nullable(),
66
+ });
67
+
68
+ // Schema for updating an existing post
69
+ export const updatePostSchema = createPostSchema.partial().extend({
70
+ id: z.string().length(21, 'Invalid post ID'),
71
+ });
72
+
73
+ // Schema for publishing/unpublishing a post
74
+ export const publishPostSchema = z.object({
75
+ id: z.string().length(21, 'Invalid post ID'),
76
+ published: z.boolean(),
77
+ });
78
+
79
+ // Schema for post query parameters
80
+ export const postQuerySchema = z.object({
81
+ page: z.coerce.number().int().positive().default(1),
82
+ limit: z.coerce.number().int().positive().max(100).default(10),
83
+ published: z
84
+ .string()
85
+ .transform((val) => val === 'true')
86
+ .optional(),
87
+ featured: z
88
+ .string()
89
+ .transform((val) => val === 'true')
90
+ .optional(),
91
+ authorId: z.string().optional(),
92
+ search: z.string().optional(),
93
+ });
94
+
95
+ /**
96
+ * Comment validation schemas
97
+ */
98
+
99
+ // Schema for creating a new comment
100
+ export const createCommentSchema = z.object({
101
+ postId: z.string().length(21, 'Invalid post ID'),
102
+
103
+ content: z
104
+ .string()
105
+ .min(1, 'Comment content is required')
106
+ .max(2000, 'Comment must be 2,000 characters or less')
107
+ .trim(),
108
+
109
+ authorId: z.string().min(1, 'Author ID is required'),
110
+
111
+ authorName: z
112
+ .string()
113
+ .max(255, 'Author name must be 255 characters or less')
114
+ .optional()
115
+ .nullable(),
116
+ });
117
+
118
+ // Schema for updating a comment
119
+ export const updateCommentSchema = z.object({
120
+ id: z.string().length(21, 'Invalid comment ID'),
121
+ content: z
122
+ .string()
123
+ .min(1, 'Comment content is required')
124
+ .max(2000, 'Comment must be 2,000 characters or less')
125
+ .trim(),
126
+ });
127
+
128
+ // Schema for moderating a comment
129
+ export const moderateCommentSchema = z.object({
130
+ id: z.string().length(21, 'Invalid comment ID'),
131
+ approved: z.boolean().optional(),
132
+ flagged: z.boolean().optional(),
133
+ });
134
+
135
+ // Schema for comment query parameters
136
+ export const commentQuerySchema = z.object({
137
+ postId: z.string().length(21, 'Invalid post ID').optional(),
138
+ page: z.coerce.number().int().positive().default(1),
139
+ limit: z.coerce.number().int().positive().max(100).default(20),
140
+ approved: z
141
+ .string()
142
+ .transform((val) => val === 'true')
143
+ .optional(),
144
+ authorId: z.string().optional(),
145
+ });
146
+
147
+ /**
148
+ * Type exports for use in application
149
+ */
150
+ export type CreatePostInput = z.infer<typeof createPostSchema>;
151
+ export type UpdatePostInput = z.infer<typeof updatePostSchema>;
152
+ export type PublishPostInput = z.infer<typeof publishPostSchema>;
153
+ export type PostQueryInput = z.infer<typeof postQuerySchema>;
154
+
155
+ export type CreateCommentInput = z.infer<typeof createCommentSchema>;
156
+ export type UpdateCommentInput = z.infer<typeof updateCommentSchema>;
157
+ export type ModerateCommentInput = z.infer<typeof moderateCommentSchema>;
158
+ export type CommentQueryInput = z.infer<typeof commentQuerySchema>;
@@ -0,0 +1 @@
1
+ /// <reference path="../.astro/types.d.ts" />
@@ -0,0 +1,63 @@
1
+ ---
2
+ /**
3
+ * Base Layout Component
4
+ * Demonstrates proper SCSS architecture:
5
+ * - No <style> tags in this file
6
+ * - All styles imported from external SCSS files
7
+ * - Global styles applied via import in the head
8
+ */
9
+
10
+ import { siteConfig } from '@/lib/config';
11
+
12
+ interface Props {
13
+ pageTitle?: string | string[];
14
+ description?: string;
15
+ }
16
+
17
+ const {
18
+ pageTitle,
19
+ description = siteConfig.stackDescription,
20
+ } = Astro.props;
21
+
22
+ const pageArray = Array.isArray(pageTitle) ? pageTitle : [pageTitle];
23
+
24
+ const firstElement = pageArray?.[0];
25
+ const shouldSkipFirst =
26
+ firstElement === '' ||
27
+ firstElement === siteConfig.stackName ||
28
+ firstElement?.toLowerCase() === 'index' ||
29
+ firstElement?.toLowerCase() === 'home';
30
+
31
+ const newPageTitle = (pageArray && pageArray.length > 0 && firstElement)
32
+ ? shouldSkipFirst
33
+ ? pageArray.filter((a, i) => i !== 0)
34
+ : pageArray
35
+ : null;
36
+
37
+ const title = newPageTitle ? `${newPageTitle.join(' | ')} | ${siteConfig.stackName}` : siteConfig.stackName;
38
+ ---
39
+
40
+ <!doctype html>
41
+ <html lang="en">
42
+ <head>
43
+ <meta charset="UTF-8" />
44
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
45
+ <meta name="description" content={description} />
46
+ <meta name="generator" content={Astro.generator} />
47
+
48
+ <!-- Favicons -->
49
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
50
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
51
+
52
+ <!-- PWA manifest -->
53
+ <link rel="manifest" href="/manifest.webmanifest" />
54
+
55
+ <title>{title}</title>
56
+
57
+ <!-- Global styles imported from external SCSS file -->
58
+ <link rel="stylesheet" href="/src/styles/global.scss" />
59
+ </head>
60
+ <body>
61
+ <slot />
62
+ </body>
63
+ </html>
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Site Configuration
3
+ * Central configuration for the application that will be rendered on screen
4
+ */
5
+
6
+ /**
7
+ * The core stack identifier - change this in ONE place to update everywhere
8
+ */
9
+ const STACK_SHORT_NAME = 'ATSDC';
10
+
11
+ export const siteConfig = {
12
+ /**
13
+ * Short name for the stack (used in PWA and compact displays)
14
+ */
15
+ stackShortName: STACK_SHORT_NAME,
16
+
17
+ /**
18
+ * The full name of the stack/framework (derived from stackShortName)
19
+ */
20
+ stackName: `${STACK_SHORT_NAME} Stack`,
21
+
22
+ /**
23
+ * Full description of the stack
24
+ */
25
+ stackDescription:
26
+ 'Full-stack application built with Astro, TypeScript, Drizzle, Clerk, and SCSS',
27
+
28
+ /**
29
+ * Docs URL
30
+ */
31
+ docsUrl: 'https://github.com/adarshrkumar/The-ATSDC-Stack',
32
+ /**
33
+ * GitHub repository URL
34
+ */
35
+ githubUrl: 'https://github.com/adarshrkumar/The-ATSDC-Stack',
36
+ } as const;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Content Converter Utilities
3
+ * Provides functions for converting between HTML and Markdown using marked and turndown
4
+ */
5
+
6
+ import { marked } from 'marked';
7
+ import TurndownService from 'turndown';
8
+
9
+ // Configure marked for markdown to HTML conversion
10
+ marked.setOptions({
11
+ gfm: true, // GitHub Flavored Markdown
12
+ breaks: true, // Convert \n to <br>
13
+ headerIds: true, // Add IDs to headings
14
+ mangle: false, // Don't escape email addresses
15
+ });
16
+
17
+ // Configure turndown for HTML to markdown conversion
18
+ const turndownService = new TurndownService({
19
+ headingStyle: 'atx', // Use # for headings
20
+ hr: '---', // Use --- for horizontal rules
21
+ bulletListMarker: '-', // Use - for bullet lists
22
+ codeBlockStyle: 'fenced', // Use ``` for code blocks
23
+ fence: '```', // Code fence marker
24
+ emDelimiter: '*', // Use * for emphasis
25
+ strongDelimiter: '**', // Use ** for strong
26
+ linkStyle: 'inlined', // Use [text](url) for links
27
+ });
28
+
29
+ /**
30
+ * Convert Markdown to HTML
31
+ * @param markdown - The markdown string to convert
32
+ * @returns HTML string
33
+ */
34
+ export async function markdownToHtml(markdown: string): Promise<string> {
35
+ try {
36
+ const html = await marked.parse(markdown);
37
+ return html;
38
+ } catch (error) {
39
+ console.error('Error converting markdown to HTML:', error);
40
+ throw new Error('Failed to convert markdown to HTML');
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Convert Markdown to HTML synchronously
46
+ * @param markdown - The markdown string to convert
47
+ * @returns HTML string
48
+ */
49
+ export function markdownToHtmlSync(markdown: string): string {
50
+ try {
51
+ const html = marked.parse(markdown) as string;
52
+ return html;
53
+ } catch (error) {
54
+ console.error('Error converting markdown to HTML:', error);
55
+ throw new Error('Failed to convert markdown to HTML');
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Convert HTML to Markdown
61
+ * @param html - The HTML string to convert
62
+ * @returns Markdown string
63
+ */
64
+ export function htmlToMarkdown(html: string): string {
65
+ try {
66
+ const markdown = turndownService.turndown(html);
67
+ return markdown;
68
+ } catch (error) {
69
+ console.error('Error converting HTML to markdown:', error);
70
+ throw new Error('Failed to convert HTML to markdown');
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Sanitize and convert Markdown to HTML
76
+ * Useful for user-generated content
77
+ * @param markdown - The markdown string to convert
78
+ * @returns Sanitized HTML string
79
+ */
80
+ export async function sanitizeMarkdown(markdown: string): Promise<string> {
81
+ try {
82
+ // Basic sanitization - remove script tags and dangerous attributes
83
+ const sanitized = markdown
84
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
85
+ .replace(/on\w+="[^"]*"/g, '')
86
+ .replace(/on\w+='[^']*'/g, '');
87
+
88
+ const html = await marked.parse(sanitized);
89
+ return html;
90
+ } catch (error) {
91
+ console.error('Error sanitizing markdown:', error);
92
+ throw new Error('Failed to sanitize markdown');
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Extract plain text from markdown
98
+ * @param markdown - The markdown string
99
+ * @returns Plain text string
100
+ */
101
+ export function markdownToPlainText(markdown: string): string {
102
+ try {
103
+ const html = marked.parse(markdown) as string;
104
+ const text = html
105
+ .replace(/<[^>]*>/g, '') // Remove HTML tags
106
+ .replace(/&nbsp;/g, ' ') // Replace &nbsp;
107
+ .replace(/&amp;/g, '&') // Replace &amp;
108
+ .replace(/&lt;/g, '<') // Replace &lt;
109
+ .replace(/&gt;/g, '>') // Replace &gt;
110
+ .replace(/&quot;/g, '"') // Replace &quot;
111
+ .trim();
112
+ return text;
113
+ } catch (error) {
114
+ console.error('Error extracting plain text:', error);
115
+ throw new Error('Failed to extract plain text');
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Generate excerpt from markdown
121
+ * @param markdown - The markdown string
122
+ * @param maxLength - Maximum length of the excerpt (default: 200)
123
+ * @returns Excerpt string
124
+ */
125
+ export function generateExcerpt(markdown: string, maxLength: number = 200): string {
126
+ const plainText = markdownToPlainText(markdown);
127
+
128
+ if (plainText.length <= maxLength) {
129
+ return plainText;
130
+ }
131
+
132
+ // Truncate at word boundary
133
+ const truncated = plainText.slice(0, maxLength);
134
+ const lastSpace = truncated.lastIndexOf(' ');
135
+
136
+ if (lastSpace > 0) {
137
+ return truncated.slice(0, lastSpace) + '...';
138
+ }
139
+
140
+ return truncated + '...';
141
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * DOM Manipulation Utilities
3
+ * Provides functions for manipulating HTML content using Cheerio
4
+ */
5
+
6
+ import * as cheerio from 'cheerio';
7
+
8
+ /**
9
+ * Extract metadata from HTML content
10
+ * @param html - The HTML string to parse
11
+ * @returns Metadata object
12
+ */
13
+ export function extractMetadata(html: string) {
14
+ const $ = cheerio.load(html);
15
+
16
+ return {
17
+ title: $('title').text() || $('h1').first().text() || '',
18
+ description: $('meta[name="description"]').attr('content') || '',
19
+ keywords: $('meta[name="keywords"]').attr('content') || '',
20
+ ogTitle: $('meta[property="og:title"]').attr('content') || '',
21
+ ogDescription: $('meta[property="og:description"]').attr('content') || '',
22
+ ogImage: $('meta[property="og:image"]').attr('content') || '',
23
+ twitterCard: $('meta[name="twitter:card"]').attr('content') || '',
24
+ canonical: $('link[rel="canonical"]').attr('href') || '',
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Extract all links from HTML
30
+ * @param html - The HTML string to parse
31
+ * @returns Array of link objects
32
+ */
33
+ export function extractLinks(html: string) {
34
+ const $ = cheerio.load(html);
35
+ const links: Array<{ text: string; href: string; title?: string }> = [];
36
+
37
+ $('a').each((_, element) => {
38
+ const $el = $(element);
39
+ const href = $el.attr('href');
40
+ if (href) {
41
+ links.push({
42
+ text: $el.text().trim(),
43
+ href,
44
+ title: $el.attr('title'),
45
+ });
46
+ }
47
+ });
48
+
49
+ return links;
50
+ }
51
+
52
+ /**
53
+ * Extract all images from HTML
54
+ * @param html - The HTML string to parse
55
+ * @returns Array of image objects
56
+ */
57
+ export function extractImages(html: string) {
58
+ const $ = cheerio.load(html);
59
+ const images: Array<{ src: string; alt?: string; title?: string }> = [];
60
+
61
+ $('img').each((_, element) => {
62
+ const $el = $(element);
63
+ const src = $el.attr('src');
64
+ if (src) {
65
+ images.push({
66
+ src,
67
+ alt: $el.attr('alt'),
68
+ title: $el.attr('title'),
69
+ });
70
+ }
71
+ });
72
+
73
+ return images;
74
+ }
75
+
76
+ /**
77
+ * Extract headings from HTML
78
+ * @param html - The HTML string to parse
79
+ * @returns Array of heading objects
80
+ */
81
+ export function extractHeadings(html: string) {
82
+ const $ = cheerio.load(html);
83
+ const headings: Array<{ level: number; text: string; id?: string }> = [];
84
+
85
+ $('h1, h2, h3, h4, h5, h6').each((_, element) => {
86
+ const $el = $(element);
87
+ const tagName = $el.prop('tagName')?.toLowerCase();
88
+ const level = parseInt(tagName?.replace('h', '') || '1');
89
+
90
+ headings.push({
91
+ level,
92
+ text: $el.text().trim(),
93
+ id: $el.attr('id'),
94
+ });
95
+ });
96
+
97
+ return headings;
98
+ }
99
+
100
+ /**
101
+ * Generate table of contents from HTML
102
+ * @param html - The HTML string to parse
103
+ * @param maxLevel - Maximum heading level to include (default: 3)
104
+ * @returns Array of TOC items
105
+ */
106
+ export function generateTableOfContents(html: string, maxLevel: number = 3) {
107
+ const headings = extractHeadings(html);
108
+ return headings
109
+ .filter((h) => h.level <= maxLevel)
110
+ .map((h) => ({
111
+ ...h,
112
+ slug: h.id || h.text.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, ''),
113
+ }));
114
+ }
115
+
116
+ /**
117
+ * Clean HTML content
118
+ * Removes scripts, styles, and other potentially dangerous elements
119
+ * @param html - The HTML string to clean
120
+ * @returns Cleaned HTML string
121
+ */
122
+ export function cleanHtml(html: string): string {
123
+ const $ = cheerio.load(html);
124
+
125
+ // Remove dangerous elements
126
+ $('script, style, iframe, object, embed').remove();
127
+
128
+ // Remove event handlers
129
+ $('*').each((_, element) => {
130
+ const $el = $(element);
131
+ const attrs = $el.attr();
132
+
133
+ if (attrs) {
134
+ Object.keys(attrs).forEach((attr) => {
135
+ if (attr.startsWith('on')) {
136
+ $el.removeAttr(attr);
137
+ }
138
+ });
139
+ }
140
+ });
141
+
142
+ return $.html();
143
+ }
144
+
145
+ /**
146
+ * Extract text content from HTML
147
+ * @param html - The HTML string to parse
148
+ * @returns Plain text string
149
+ */
150
+ export function extractTextContent(html: string): string {
151
+ const $ = cheerio.load(html);
152
+
153
+ // Remove script and style elements
154
+ $('script, style').remove();
155
+
156
+ return $('body').text().replace(/\s+/g, ' ').trim();
157
+ }
158
+
159
+ /**
160
+ * Add IDs to headings for anchor linking
161
+ * @param html - The HTML string to process
162
+ * @returns HTML with IDs added to headings
163
+ */
164
+ export function addHeadingIds(html: string): string {
165
+ const $ = cheerio.load(html);
166
+
167
+ $('h1, h2, h3, h4, h5, h6').each((_, element) => {
168
+ const $el = $(element);
169
+
170
+ if (!$el.attr('id')) {
171
+ const text = $el.text().trim();
172
+ const id = text.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '');
173
+ $el.attr('id', id);
174
+ }
175
+ });
176
+
177
+ return $.html();
178
+ }
179
+
180
+ /**
181
+ * Add target="_blank" to external links
182
+ * @param html - The HTML string to process
183
+ * @param domain - The current domain (optional)
184
+ * @returns HTML with external links updated
185
+ */
186
+ export function addExternalLinkTargets(html: string, domain?: string): string {
187
+ const $ = cheerio.load(html);
188
+
189
+ $('a').each((_, element) => {
190
+ const $el = $(element);
191
+ const href = $el.attr('href');
192
+
193
+ if (href && (href.startsWith('http://') || href.startsWith('https://'))) {
194
+ if (!domain || !href.includes(domain)) {
195
+ $el.attr('target', '_blank');
196
+ $el.attr('rel', 'noopener noreferrer');
197
+ }
198
+ }
199
+ });
200
+
201
+ return $.html();
202
+ }
203
+
204
+ /**
205
+ * Calculate reading time for HTML content
206
+ * @param html - The HTML string to analyze
207
+ * @param wordsPerMinute - Average reading speed (default: 200)
208
+ * @returns Reading time in minutes
209
+ */
210
+ export function calculateReadingTime(html: string, wordsPerMinute: number = 200): number {
211
+ const text = extractTextContent(html);
212
+ const wordCount = text.split(/\s+/).length;
213
+ return Math.ceil(wordCount / wordsPerMinute);
214
+ }
215
+
216
+ /**
217
+ * Wrap tables in a responsive container
218
+ * @param html - The HTML string to process
219
+ * @returns HTML with tables wrapped
220
+ */
221
+ export function wrapTables(html: string): string {
222
+ const $ = cheerio.load(html);
223
+
224
+ $('table').each((_, element) => {
225
+ const $el = $(element);
226
+ $el.wrap('<div class="table-wrapper" style="overflow-x: auto;"></div>');
227
+ });
228
+
229
+ return $.html();
230
+ }