@respectify/astro 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.
@@ -0,0 +1,373 @@
1
+ /* Respectify Comments — drop-in styles. Override via CSS variables on .rf-comments. */
2
+ .rf-comments {
3
+ --rf-accent: #2563eb;
4
+ --rf-accent-hover: #1d4ed8;
5
+ --rf-accent-soft: rgba(37, 99, 235, 0.08);
6
+ --rf-border: #d1d5db;
7
+ --rf-bg: #f9fafb;
8
+ --rf-bg-card: #ffffff;
9
+ --rf-text: #111827;
10
+ --rf-text-muted: #6b7280;
11
+ --rf-success-bg: #ecfdf5;
12
+ --rf-success-text: #065f46;
13
+ --rf-success-border: #6ee7b7;
14
+ --rf-warning-bg: #fffbeb;
15
+ --rf-warning-text: #92400e;
16
+ --rf-warning-border: #fcd34d;
17
+ --rf-error-bg: #fef2f2;
18
+ --rf-error-text: #991b1b;
19
+ --rf-error-border: #fca5a5;
20
+ --rf-radius: 0.5rem;
21
+ --rf-font: inherit;
22
+ font-family: var(--rf-font);
23
+ color: var(--rf-text);
24
+ }
25
+
26
+ @media (prefers-color-scheme: dark) {
27
+ .rf-comments {
28
+ --rf-border: #374151;
29
+ --rf-bg: #111827;
30
+ --rf-bg-card: #1f2937;
31
+ --rf-text: #f9fafb;
32
+ --rf-text-muted: #9ca3af;
33
+ --rf-accent-soft: rgba(37, 99, 235, 0.15);
34
+ --rf-success-bg: #064e3b;
35
+ --rf-success-text: #d1fae5;
36
+ --rf-success-border: #059669;
37
+ --rf-warning-bg: #78350f;
38
+ --rf-warning-text: #fef3c7;
39
+ --rf-warning-border: #d97706;
40
+ --rf-error-bg: #7f1d1d;
41
+ --rf-error-text: #fecaca;
42
+ --rf-error-border: #dc2626;
43
+ }
44
+ }
45
+
46
+ .rf-comments *,
47
+ .rf-comments *::before,
48
+ .rf-comments *::after {
49
+ box-sizing: border-box;
50
+ }
51
+
52
+ .rf-comments__section {
53
+ margin-top: 2rem;
54
+ }
55
+
56
+ .rf-comments__heading {
57
+ font-size: 1.5rem;
58
+ font-weight: 700;
59
+ margin: 0 0 1.5rem;
60
+ }
61
+
62
+ .rf-comments__loading,
63
+ .rf-comments__empty {
64
+ color: var(--rf-text-muted);
65
+ font-style: italic;
66
+ }
67
+
68
+ .rf-comments__list {
69
+ display: flex;
70
+ flex-direction: column;
71
+ gap: 1.5rem;
72
+ }
73
+
74
+ .rf-comments__item {
75
+ padding: 1rem;
76
+ border: 1px solid var(--rf-border);
77
+ border-radius: var(--rf-radius);
78
+ background: var(--rf-bg-card);
79
+ }
80
+
81
+ .rf-comments__item-header {
82
+ display: flex;
83
+ align-items: baseline;
84
+ justify-content: space-between;
85
+ gap: 1rem;
86
+ margin-bottom: 0.5rem;
87
+ }
88
+
89
+ .rf-comments__author {
90
+ font-weight: 600;
91
+ font-size: 1.125rem;
92
+ }
93
+
94
+ .rf-comments__date {
95
+ font-size: 0.875rem;
96
+ color: var(--rf-text-muted);
97
+ white-space: nowrap;
98
+ }
99
+
100
+ .rf-comments__content {
101
+ white-space: pre-wrap;
102
+ line-height: 1.6;
103
+ }
104
+
105
+ .rf-comments__form-panel {
106
+ margin-top: 2rem;
107
+ padding: 1.5rem;
108
+ border: 1px solid var(--rf-border);
109
+ border-radius: var(--rf-radius);
110
+ background: var(--rf-bg);
111
+ }
112
+
113
+ .rf-comments__form-title {
114
+ font-size: 1.5rem;
115
+ font-weight: 700;
116
+ margin: 0 0 0.25rem;
117
+ }
118
+
119
+ .rf-comments__form-note {
120
+ margin: 0 0 1rem;
121
+ font-size: 0.875rem;
122
+ color: var(--rf-text-muted);
123
+ }
124
+
125
+ .rf-comments__form-note a {
126
+ color: var(--rf-accent);
127
+ font-weight: 500;
128
+ text-decoration: underline;
129
+ }
130
+
131
+ .rf-comments__field {
132
+ margin-bottom: 1rem;
133
+ }
134
+
135
+ .rf-comments__label {
136
+ display: block;
137
+ font-size: 0.875rem;
138
+ font-weight: 500;
139
+ margin-bottom: 0.25rem;
140
+ }
141
+
142
+ .rf-comments__required {
143
+ color: #ef4444;
144
+ }
145
+
146
+ .rf-comments__input,
147
+ .rf-comments__textarea {
148
+ width: 100%;
149
+ padding: 0.5rem 0.75rem;
150
+ border: 1px solid var(--rf-border);
151
+ border-radius: var(--rf-radius);
152
+ background: var(--rf-bg-card);
153
+ color: var(--rf-text);
154
+ font: inherit;
155
+ }
156
+
157
+ .rf-comments__input:focus,
158
+ .rf-comments__textarea:focus {
159
+ outline: 2px solid var(--rf-accent);
160
+ outline-offset: 1px;
161
+ }
162
+
163
+ .rf-comments__textarea {
164
+ min-height: 8rem;
165
+ resize: vertical;
166
+ }
167
+
168
+ .rf-comments__submit {
169
+ display: inline-flex;
170
+ align-items: center;
171
+ gap: 0.5rem;
172
+ padding: 0.5rem 1.5rem;
173
+ border: none;
174
+ border-radius: var(--rf-radius);
175
+ background: var(--rf-accent);
176
+ color: #fff;
177
+ font: inherit;
178
+ font-weight: 500;
179
+ cursor: pointer;
180
+ transition: background 0.15s ease;
181
+ }
182
+
183
+ .rf-comments__submit:hover:not(:disabled) {
184
+ background: var(--rf-accent-hover);
185
+ }
186
+
187
+ .rf-comments__submit:disabled {
188
+ opacity: 0.6;
189
+ cursor: not-allowed;
190
+ }
191
+
192
+ .rf-comments__submit--analyzing {
193
+ animation: rf-pulse 2s ease-in-out infinite;
194
+ }
195
+
196
+ @keyframes rf-pulse {
197
+ 0%,
198
+ 100% {
199
+ box-shadow: 0 0 0 0 rgba(37, 99, 235, 0.4);
200
+ }
201
+ 50% {
202
+ box-shadow: 0 0 0 8px rgba(37, 99, 235, 0);
203
+ }
204
+ }
205
+
206
+ .rf-comments__spinner {
207
+ display: none;
208
+ width: 1.25rem;
209
+ height: 1.25rem;
210
+ animation: rf-spin 1s linear infinite;
211
+ }
212
+
213
+ .rf-comments__submit--analyzing .rf-comments__spinner {
214
+ display: inline-block;
215
+ }
216
+
217
+ @keyframes rf-spin {
218
+ to {
219
+ transform: rotate(360deg);
220
+ }
221
+ }
222
+
223
+ .rf-comments__message {
224
+ margin-top: 1rem;
225
+ padding: 1rem;
226
+ border-radius: var(--rf-radius);
227
+ border: 2px solid transparent;
228
+ }
229
+
230
+ .rf-comments__message--success {
231
+ background: var(--rf-success-bg);
232
+ color: var(--rf-success-text);
233
+ border-color: var(--rf-success-border);
234
+ }
235
+
236
+ .rf-comments__message--warning {
237
+ background: var(--rf-warning-bg);
238
+ color: var(--rf-warning-text);
239
+ border-color: var(--rf-warning-border);
240
+ }
241
+
242
+ .rf-comments__message--error {
243
+ background: var(--rf-error-bg);
244
+ color: var(--rf-error-text);
245
+ border-color: var(--rf-error-border);
246
+ }
247
+
248
+ .rf-comments__suggestion {
249
+ margin-top: 0.75rem;
250
+ padding: 0.75rem;
251
+ border: 1px solid var(--rf-border);
252
+ border-radius: var(--rf-radius);
253
+ background: var(--rf-accent-soft);
254
+ }
255
+
256
+ .rf-comments__brand {
257
+ margin-bottom: 1.5rem;
258
+ padding: 0.75rem 1rem;
259
+ border: 1px solid color-mix(in srgb, var(--rf-accent) 30%, transparent);
260
+ border-radius: var(--rf-radius);
261
+ background: var(--rf-accent-soft);
262
+ }
263
+
264
+ .rf-comments__brand-inner {
265
+ display: flex;
266
+ flex-wrap: wrap;
267
+ align-items: flex-start;
268
+ justify-content: space-between;
269
+ gap: 0.75rem;
270
+ }
271
+
272
+ .rf-comments__brand-title {
273
+ margin: 0;
274
+ font-size: 0.875rem;
275
+ font-weight: 600;
276
+ }
277
+
278
+ .rf-comments__brand-title a {
279
+ color: var(--rf-accent);
280
+ text-decoration: none;
281
+ display: inline-flex;
282
+ align-items: center;
283
+ gap: 0.375rem;
284
+ }
285
+
286
+ .rf-comments__brand-title a:hover {
287
+ text-decoration: underline;
288
+ }
289
+
290
+ .rf-comments__brand-desc {
291
+ margin: 0.25rem 0 0;
292
+ font-size: 0.875rem;
293
+ color: var(--rf-text-muted);
294
+ }
295
+
296
+ .rf-comments__brand-cta {
297
+ flex-shrink: 0;
298
+ padding: 0.375rem 0.75rem;
299
+ border: 1px solid color-mix(in srgb, var(--rf-accent) 40%, transparent);
300
+ border-radius: var(--rf-radius);
301
+ color: var(--rf-accent);
302
+ font-size: 0.75rem;
303
+ font-weight: 500;
304
+ text-decoration: none;
305
+ white-space: nowrap;
306
+ }
307
+
308
+ .rf-comments__brand-cta:hover {
309
+ background: var(--rf-accent-soft);
310
+ }
311
+
312
+ .rf-comments__brand-badge {
313
+ display: inline-flex;
314
+ align-items: center;
315
+ gap: 0.375rem;
316
+ margin-top: 1rem;
317
+ font-size: 0.75rem;
318
+ color: var(--rf-text-muted);
319
+ text-decoration: none;
320
+ }
321
+
322
+ .rf-comments__brand-badge:hover {
323
+ color: var(--rf-accent);
324
+ }
325
+
326
+ .rf-comments__hidden {
327
+ display: none !important;
328
+ }
329
+
330
+ .rf-comments__delete-controls {
331
+ display: none;
332
+ align-items: center;
333
+ gap: 0.25rem;
334
+ }
335
+
336
+ .rf-comments__delete-controls--visible {
337
+ display: inline-flex;
338
+ }
339
+
340
+ .rf-comments__delete-btn {
341
+ padding: 0.125rem 0.5rem;
342
+ border: none;
343
+ border-radius: 0.25rem;
344
+ background: #dc2626;
345
+ color: #fff;
346
+ font-size: 0.75rem;
347
+ cursor: pointer;
348
+ }
349
+
350
+ .rf-comments__delete-btn:hover {
351
+ background: #b91c1c;
352
+ }
353
+
354
+ .rf-comments__delete-confirm {
355
+ display: none;
356
+ align-items: center;
357
+ gap: 0.25rem;
358
+ font-size: 0.75rem;
359
+ }
360
+
361
+ .rf-comments__delete-confirm--visible {
362
+ display: inline-flex;
363
+ }
364
+
365
+ .rf-comments__delete-cancel {
366
+ padding: 0.125rem 0.5rem;
367
+ border: none;
368
+ border-radius: 0.25rem;
369
+ background: var(--rf-border);
370
+ color: var(--rf-text);
371
+ font-size: 0.75rem;
372
+ cursor: pointer;
373
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "./respectify.schema.json",
3
+ "enabled": true,
4
+ "moderation": {
5
+ "blockDisrespectful": true,
6
+ "showFeedbackToUser": true,
7
+ "allowOverride": false
8
+ },
9
+ "thresholds": {
10
+ "autoApprove": 0.7,
11
+ "warn": 0.5,
12
+ "block": 0.3
13
+ },
14
+ "feedback": {
15
+ "approved": "Thank you for your respectful comment!",
16
+ "warning": "Your comment may contain disrespectful language. Please consider revising it.",
17
+ "blocked": "Your comment does not meet our respectfulness standards. Please revise and try again."
18
+ }
19
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@respectify/astro",
3
+ "version": "0.1.0",
4
+ "description": "Respectify-powered commenting for Astro — AI moderation, spam filtering, and beautiful UX out of the box.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Respectify",
8
+ "homepage": "https://respectify.ai",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/NickHodges/Nick-Blog.git",
12
+ "directory": "packages/astro-respectify"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "keywords": [
18
+ "astro",
19
+ "astro-integration",
20
+ "comments",
21
+ "respectify",
22
+ "moderation",
23
+ "ai"
24
+ ],
25
+ "exports": {
26
+ ".": "./src/index.ts",
27
+ "./actions": "./src/actions.ts",
28
+ "./schema": "./src/schema.ts",
29
+ "./routes/comments": "./src/routes/comments.ts",
30
+ "./components/CommentSection.astro": "./components/CommentSection.astro",
31
+ "./components/PoweredByRespectify.astro": "./components/PoweredByRespectify.astro",
32
+ "./client": "./client/comments.ts",
33
+ "./styles.css": "./components/respectify-comments.css"
34
+ },
35
+ "files": [
36
+ "src",
37
+ "components",
38
+ "client",
39
+ "default-respectify.config.json",
40
+ "README.md"
41
+ ],
42
+ "peerDependencies": {
43
+ "@astrojs/db": "^0.20.0",
44
+ "@respectify/client": "^0.2.3",
45
+ "astro": "^5.0.0 || ^6.0.0",
46
+ "zod": "^3.0.0"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "@astrojs/db": {
50
+ "optional": false
51
+ }
52
+ },
53
+ "dependencies": {
54
+ "zod": "^3.24.0"
55
+ },
56
+ "scripts": {
57
+ "typecheck": "tsc --noEmit"
58
+ },
59
+ "devDependencies": {
60
+ "@astrojs/db": "^0.20.0",
61
+ "@respectify/client": "^0.2.3",
62
+ "astro": "^6.0.4",
63
+ "typescript": "^5.4.3"
64
+ }
65
+ }
package/src/actions.ts ADDED
@@ -0,0 +1,77 @@
1
+ import { defineAction, ActionError } from 'astro:actions';
2
+ import { z } from 'astro/zod';
3
+ import { db, Comment, eq } from 'astro:db';
4
+ import { analyzeComment } from './lib/service';
5
+
6
+ /**
7
+ * Merge into your src/actions/index.ts:
8
+ *
9
+ * ```ts
10
+ * import { respectifyCommentActions } from '@respectify/astro/actions';
11
+ *
12
+ * export const server = {
13
+ * comments: respectifyCommentActions,
14
+ * // ...your other actions
15
+ * };
16
+ * ```
17
+ */
18
+ export const respectifyCommentActions = {
19
+ submit: defineAction({
20
+ accept: 'json',
21
+ input: z.object({
22
+ postSlug: z.string().min(1),
23
+ author: z.string().min(1).max(100),
24
+ email: z.string().email().optional(),
25
+ content: z.string().min(1).max(5000),
26
+ parentId: z.number().optional(),
27
+ }),
28
+ handler: async ({ postSlug, author, email, content, parentId }) => {
29
+ const analysis = await analyzeComment(content, postSlug);
30
+
31
+ if (!analysis.approved) {
32
+ return {
33
+ success: false as const,
34
+ approved: false as const,
35
+ feedback: analysis.feedback,
36
+ suggestion: analysis.suggestion,
37
+ score: analysis.score,
38
+ };
39
+ }
40
+
41
+ await db.insert(Comment).values({
42
+ postSlug,
43
+ author,
44
+ email,
45
+ content,
46
+ parentId,
47
+ approved: true,
48
+ });
49
+
50
+ return {
51
+ success: true as const,
52
+ approved: true as const,
53
+ feedback: analysis.feedback,
54
+ score: analysis.score,
55
+ message: 'Comment published successfully!',
56
+ };
57
+ },
58
+ }),
59
+
60
+ delete: defineAction({
61
+ accept: 'json',
62
+ input: z.object({
63
+ commentId: z.number(),
64
+ }),
65
+ handler: async ({ commentId }, context) => {
66
+ if (!context.locals.isAuthenticated) {
67
+ throw new ActionError({
68
+ code: 'UNAUTHORIZED',
69
+ message: 'You must be logged in to delete comments',
70
+ });
71
+ }
72
+
73
+ await db.delete(Comment).where(eq(Comment.id, commentId));
74
+ return { success: true as const };
75
+ },
76
+ }),
77
+ };
package/src/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ import type { AstroIntegration } from 'astro';
2
+ import { envField } from 'astro/config';
3
+ import { setIntegrationOptions } from './runtime-config';
4
+ import type { RespectifyIntegrationOptions } from './types';
5
+
6
+ export default function respectify(userOptions: RespectifyIntegrationOptions = {}): AstroIntegration {
7
+ setIntegrationOptions(userOptions);
8
+
9
+ const commentsApiPath = userOptions.commentsApiPath ?? '/api/respectify/comments';
10
+ const routePattern = commentsApiPath.replace(/^\//, '');
11
+
12
+ return {
13
+ name: '@respectify/astro',
14
+ hooks: {
15
+ 'astro:config:setup': ({ injectRoute, updateConfig }) => {
16
+ updateConfig({
17
+ env: {
18
+ schema: {
19
+ RESPECTIFY_EMAIL: envField.string({ context: 'server', access: 'secret' }),
20
+ RESPECTIFY_API_KEY: envField.string({ context: 'server', access: 'secret' }),
21
+ },
22
+ },
23
+ });
24
+
25
+ injectRoute({
26
+ pattern: routePattern,
27
+ entrypoint: '@respectify/astro/routes/comments',
28
+ });
29
+ },
30
+ },
31
+ };
32
+ }
33
+
34
+ export type { RespectifyIntegrationOptions, RespectifyAnalysisResult } from './types';
@@ -0,0 +1,76 @@
1
+ import { z } from 'zod';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { getIntegrationOptions } from '../runtime-config';
5
+
6
+ const RespectifyConfigSchema = z.object({
7
+ enabled: z.boolean(),
8
+ apiEndpoint: z.string().url().optional(),
9
+ requiresAuth: z.boolean().optional(),
10
+ autoPublish: z
11
+ .object({
12
+ enabled: z.boolean(),
13
+ minimumScore: z.number().min(0).max(1),
14
+ })
15
+ .optional(),
16
+ moderation: z.object({
17
+ blockDisrespectful: z.boolean(),
18
+ showFeedbackToUser: z.boolean().optional(),
19
+ allowOverride: z.boolean(),
20
+ }),
21
+ thresholds: z.object({
22
+ autoApprove: z.number().min(0).max(1),
23
+ warn: z.number().min(0).max(1),
24
+ block: z.number().min(0).max(1),
25
+ }),
26
+ feedback: z.object({
27
+ approved: z.string(),
28
+ warning: z.string(),
29
+ blocked: z.string(),
30
+ }),
31
+ });
32
+
33
+ export type RespectifyConfig = z.infer<typeof RespectifyConfigSchema>;
34
+
35
+ let cachedConfig: RespectifyConfig | null = null;
36
+
37
+ export function loadRespectifyConfig(): RespectifyConfig {
38
+ const isDevelopment = import.meta.env.DEV;
39
+
40
+ if (cachedConfig && !isDevelopment) {
41
+ return cachedConfig;
42
+ }
43
+
44
+ const { configPath } = getIntegrationOptions();
45
+ const resolvedPath = path.isAbsolute(configPath) ? configPath : path.join(process.cwd(), configPath);
46
+
47
+ try {
48
+ const configFile = fs.readFileSync(resolvedPath, 'utf-8');
49
+ const config = JSON.parse(configFile) as unknown;
50
+ cachedConfig = RespectifyConfigSchema.parse(config);
51
+ return cachedConfig;
52
+ } catch (error) {
53
+ throw new Error(`Failed to load Respectify config from ${resolvedPath}: ${error}`);
54
+ }
55
+ }
56
+
57
+ export function getDefaultConfig(): RespectifyConfig {
58
+ return {
59
+ enabled: true,
60
+ moderation: {
61
+ blockDisrespectful: true,
62
+ showFeedbackToUser: true,
63
+ allowOverride: false,
64
+ },
65
+ thresholds: {
66
+ autoApprove: 0.7,
67
+ warn: 0.5,
68
+ block: 0.3,
69
+ },
70
+ feedback: {
71
+ approved: 'Thank you for your respectful comment!',
72
+ warning: 'Your comment may contain disrespectful language. Please consider revising it.',
73
+ blocked: 'Your comment does not meet our respectfulness standards. Please revise and try again.',
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,20 @@
1
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+
3
+ function log(level: LogLevel, message: string, data?: Record<string, unknown>): void {
4
+ if (level === 'debug' && !import.meta.env.DEV) return;
5
+ if (level === 'warn' && !import.meta.env.DEV) return;
6
+
7
+ const prefix = `[respectify:${level}]`;
8
+ if (data) {
9
+ console[level === 'debug' ? 'log' : level](prefix, message, data);
10
+ } else {
11
+ console[level === 'debug' ? 'log' : level](prefix, message);
12
+ }
13
+ }
14
+
15
+ export const logger = {
16
+ debug: (message: string, data?: Record<string, unknown>) => log('debug', message, data),
17
+ info: (message: string, data?: Record<string, unknown>) => log('info', message, data),
18
+ warn: (message: string, data?: Record<string, unknown>) => log('warn', message, data),
19
+ error: (message: string, data?: Record<string, unknown>) => log('error', message, data),
20
+ };