tangled
alpha
login
or
join now
safwanyp.com
/
website
0
fork
atom
Code for my personal website
0
fork
atom
overview
issues
pulls
pipelines
chore: remove kommentar integration for now
Safwan Parker
2 months ago
4d7d8824
13fa7d8e
-370
4 changed files
expand all
collapse all
unified
split
astro.config.mjs
src
pages
api
kommentar
comments
[host].ts
blog
[...slug].astro
projects
[...slug].astro
-10
astro.config.mjs
···
23
23
context: 'client',
24
24
access: 'public',
25
25
optional: false
26
26
-
}),
27
27
-
KOMMENTAR_API_KEY: envField.string({
28
28
-
context: 'server',
29
29
-
access: 'secret',
30
30
-
optional: false
31
31
-
}),
32
32
-
KOMMENTAR_API_SECRET: envField.string({
33
33
-
context: 'server',
34
34
-
access: 'secret',
35
35
-
optional: false
36
26
})
37
27
}
38
28
},
-60
src/pages/api/kommentar/comments/[host].ts
···
1
1
-
import { type APIRoute } from 'astro';
2
2
-
import { KOMMENTAR_API_KEY, KOMMENTAR_API_SECRET } from 'astro:env/server';
3
3
-
4
4
-
export const prerender = false;
5
5
-
6
6
-
export const GET: APIRoute = async ({ request, params }) => {
7
7
-
if (request.headers.get('Content-Type') === 'application/json') {
8
8
-
const hostId = params.host;
9
9
-
10
10
-
const response = await fetch(
11
11
-
`https://api.kommentar.dev/api/hosts/${hostId}/comments`,
12
12
-
{
13
13
-
headers: {
14
14
-
'Content-Type': 'application/json',
15
15
-
'x-api-key': KOMMENTAR_API_KEY,
16
16
-
'x-api-secret': KOMMENTAR_API_SECRET
17
17
-
}
18
18
-
}
19
19
-
);
20
20
-
21
21
-
const comments = await response.json();
22
22
-
23
23
-
return new Response(JSON.stringify(comments), {
24
24
-
status: 200,
25
25
-
headers: {
26
26
-
'Content-Type': 'application/json'
27
27
-
}
28
28
-
});
29
29
-
}
30
30
-
31
31
-
return new Response(null, { status: 400 });
32
32
-
};
33
33
-
34
34
-
export const POST: APIRoute = async ({ params, request, cookies }) => {
35
35
-
const hostId = params.host;
36
36
-
const body = await request.json();
37
37
-
38
38
-
const response = await fetch(
39
39
-
`https://api.kommentar.dev/api/hosts/${hostId}/comments`,
40
40
-
{
41
41
-
method: 'POST',
42
42
-
headers: {
43
43
-
'Content-Type': 'application/json',
44
44
-
'x-api-key': KOMMENTAR_API_KEY,
45
45
-
'x-api-secret': KOMMENTAR_API_SECRET,
46
46
-
Cookie: `sessionId=${cookies.get('sessionId')!.value}`
47
47
-
},
48
48
-
body: JSON.stringify(body)
49
49
-
}
50
50
-
);
51
51
-
52
52
-
const comments = await response.json();
53
53
-
54
54
-
return new Response(JSON.stringify(comments), {
55
55
-
status: 200,
56
56
-
headers: {
57
57
-
'Content-Type': 'application/json'
58
58
-
}
59
59
-
});
60
60
-
};
-150
src/pages/blog/[...slug].astro
···
7
7
import { ghost } from '@/lib/ghost';
8
8
import type { PostOrPage } from '@tryghost/content-api';
9
9
import Socials from '@/components/Socials.astro';
10
10
-
import Comment from '@/components/Comment.astro';
11
11
-
import { getComments, postComment } from '@/lib/kommentar';
12
10
import { v7 as uuidV7 } from 'uuid';
13
11
14
12
export const prerender = false;
···
28
26
}
29
27
30
28
type Props = PostOrPage;
31
31
-
32
32
-
const errors = {
33
33
-
displayName: '',
34
34
-
realName: '',
35
35
-
comment: ''
36
36
-
};
37
37
-
38
38
-
const formValues = {
39
39
-
displayName: '',
40
40
-
realName: '',
41
41
-
comment: ''
42
42
-
};
43
43
-
44
44
-
let commentSubmitted = false;
45
45
-
46
46
-
if (Astro.request.method === 'POST') {
47
47
-
try {
48
48
-
const data = await Astro.request.formData();
49
49
-
50
50
-
const displayName = String(data.get('displayName'));
51
51
-
const realName = String(data.get('realName'));
52
52
-
const comment = String(data.get('comment'));
53
53
-
54
54
-
formValues.displayName = displayName;
55
55
-
formValues.realName = realName;
56
56
-
formValues.comment = comment;
57
57
-
58
58
-
// Display name validation: required, minimum 3 characters
59
59
-
if (
60
60
-
!displayName ||
61
61
-
typeof displayName !== 'string' ||
62
62
-
displayName.trim().length < 3
63
63
-
) {
64
64
-
errors.displayName +=
65
65
-
'Please enter a valid display name. At least 3 characters.';
66
66
-
}
67
67
-
68
68
-
// Real name validation: optional, but if provided, minimum 5 characters
69
69
-
if (
70
70
-
realName &&
71
71
-
realName !== '' &&
72
72
-
(typeof realName !== 'string' || realName.trim().length < 5)
73
73
-
) {
74
74
-
errors.realName +=
75
75
-
'Real name must be a string with at least 5 characters.';
76
76
-
}
77
77
-
78
78
-
// Comment validation: required, between 10 and 255 characters
79
79
-
if (
80
80
-
!comment ||
81
81
-
typeof comment !== 'string' ||
82
82
-
comment.trim().length < 10 ||
83
83
-
comment.trim().length > 255
84
84
-
) {
85
85
-
errors.comment += 'Comment must have between 10 and 255 characters.';
86
86
-
}
87
87
-
88
88
-
const hasErrors = Object.values(errors).some((msg) => msg);
89
89
-
90
90
-
if (!hasErrors && Astro.cookies.has('userSessionId')) {
91
91
-
await postComment({
92
92
-
baseUrl: String(Astro.url.origin),
93
93
-
comment: {
94
94
-
hostId: String(`blog-${slug}`),
95
95
-
content: String(comment.trim()),
96
96
-
commenter: {
97
97
-
displayName: String(displayName.trim()),
98
98
-
realName: String((realName || '').trim())
99
99
-
}
100
100
-
},
101
101
-
sessionId: Astro.cookies.get('userSessionId')!.value
102
102
-
});
103
103
-
104
104
-
formValues.displayName = '';
105
105
-
formValues.realName = '';
106
106
-
formValues.comment = '';
107
107
-
108
108
-
commentSubmitted = true;
109
109
-
}
110
110
-
} catch (error) {
111
111
-
if (error instanceof Error) {
112
112
-
console.error(error.message);
113
113
-
}
114
114
-
}
115
115
-
}
116
116
-
117
117
-
const comments = await getComments({
118
118
-
baseUrl: String(Astro.url.origin),
119
119
-
hostId: `blog-${slug}`
120
120
-
});
121
29
---
122
30
123
31
<Layout title={String(blogPost.title)} description={String(blogPost.excerpt)}>
···
144
52
</article>
145
53
<div class='animate'>
146
54
<Socials />
147
147
-
</div>
148
148
-
<div class='animate w-full h-auto flex flex-col gap-3 mt-6'>
149
149
-
<span class='text-xl font-bold'>Comments</span>
150
150
-
151
151
-
{
152
152
-
commentSubmitted && (
153
153
-
<div class='border border-lime-600 text-lime-600 px-4 py-3 rounded'>
154
154
-
Comment posted successfully!
155
155
-
</div>
156
156
-
)
157
157
-
}
158
158
-
159
159
-
<form class='w-full flex flex-col gap-2' method='POST'>
160
160
-
<span>Post a comment</span>
161
161
-
<div class='w-full flex flex-col sm:flex-row justify-between gap-2'>
162
162
-
<label class='w-full text-xs flex flex-col gap-2'>
163
163
-
Display Name *
164
164
-
<input
165
165
-
type='text'
166
166
-
name='displayName'
167
167
-
value={formValues.displayName}
168
168
-
required
169
169
-
/>
170
170
-
{
171
171
-
errors.displayName && (
172
172
-
<span class='text-red-500'>{errors.displayName}</span>
173
173
-
)
174
174
-
}
175
175
-
</label>
176
176
-
<label class='w-full text-xs flex flex-col gap-2'>
177
177
-
Real Name (overrides Display Name)
178
178
-
<input type='text' name='realName' value={formValues.realName} />
179
179
-
{
180
180
-
errors.realName && (
181
181
-
<span class='text-red-500'>{errors.realName}</span>
182
182
-
)
183
183
-
}
184
184
-
</label>
185
185
-
</div>
186
186
-
<label class='text-xs flex flex-col gap-2'>
187
187
-
Comment *
188
188
-
<textarea name='comment' value={formValues.comment} required
189
189
-
></textarea>
190
190
-
{errors.comment && <span class='text-red-500'>{errors.comment}</span>}
191
191
-
</label>
192
192
-
<button
193
193
-
type='submit'
194
194
-
class='flex flex-nowrap py-2 px-4 pr-10 rounded-lg border border-black/15 dark:border-white/20
195
195
-
hover:bg-lime-500/30 dark:hover:bg-lime-300/5
196
196
-
hover:text-black dark:hover:text-white
197
197
-
hover:border-lime-700 dark:hover:border-lime-300
198
198
-
hover:shadow-lg hover:shadow-lime-600/30 dark:hover:shadow-lime-300/10
199
199
-
transition-all duration-300 ease-in-out
200
200
-
font-bold'
201
201
-
>Submit</button
202
202
-
>
203
203
-
</form>
204
204
-
{comments.map((comment) => <Comment comment={comment} />)}
205
55
</div>
206
56
</Container>
207
57
</Layout>
-150
src/pages/projects/[...slug].astro
···
7
7
import { ghost } from '@/lib/ghost';
8
8
import type { PostOrPage } from '@tryghost/content-api';
9
9
import Socials from '@/components/Socials.astro';
10
10
-
import Comment from '@/components/Comment.astro';
11
11
-
import { getComments, postComment } from '@/lib/kommentar';
12
10
import { v7 as uuidV7 } from 'uuid';
13
11
14
12
export const prerender = false;
···
31
29
}
32
30
33
31
type Props = PostOrPage;
34
34
-
35
35
-
const errors = {
36
36
-
displayName: '',
37
37
-
realName: '',
38
38
-
comment: ''
39
39
-
};
40
40
-
41
41
-
const formValues = {
42
42
-
displayName: '',
43
43
-
realName: '',
44
44
-
comment: ''
45
45
-
};
46
46
-
47
47
-
let commentSubmitted = false;
48
48
-
49
49
-
if (Astro.request.method === 'POST') {
50
50
-
try {
51
51
-
const data = await Astro.request.formData();
52
52
-
53
53
-
const displayName = String(data.get('displayName'));
54
54
-
const realName = String(data.get('realName'));
55
55
-
const comment = String(data.get('comment'));
56
56
-
57
57
-
formValues.displayName = displayName;
58
58
-
formValues.realName = realName;
59
59
-
formValues.comment = comment;
60
60
-
61
61
-
// Display name validation: required, minimum 3 characters
62
62
-
if (
63
63
-
!displayName ||
64
64
-
typeof displayName !== 'string' ||
65
65
-
displayName.trim().length < 3
66
66
-
) {
67
67
-
errors.displayName +=
68
68
-
'Please enter a valid display name. At least 3 characters.';
69
69
-
}
70
70
-
71
71
-
// Real name validation: optional, but if provided, minimum 5 characters
72
72
-
if (
73
73
-
realName &&
74
74
-
realName !== '' &&
75
75
-
(typeof realName !== 'string' || realName.trim().length < 5)
76
76
-
) {
77
77
-
errors.realName +=
78
78
-
'Real name must be a string with at least 5 characters.';
79
79
-
}
80
80
-
81
81
-
// Comment validation: required, between 10 and 255 characters
82
82
-
if (
83
83
-
!comment ||
84
84
-
typeof comment !== 'string' ||
85
85
-
comment.trim().length < 10 ||
86
86
-
comment.trim().length > 255
87
87
-
) {
88
88
-
errors.comment += 'Comment must have between 10 and 255 characters.';
89
89
-
}
90
90
-
91
91
-
const hasErrors = Object.values(errors).some((msg) => msg);
92
92
-
93
93
-
if (!hasErrors && Astro.cookies.has('userSessionId')) {
94
94
-
await postComment({
95
95
-
baseUrl: String(Astro.url.origin),
96
96
-
comment: {
97
97
-
hostId: String(`projects-${slug}`),
98
98
-
content: String(comment.trim()),
99
99
-
commenter: {
100
100
-
displayName: String(displayName.trim()),
101
101
-
realName: String((realName || '').trim())
102
102
-
}
103
103
-
},
104
104
-
sessionId: Astro.cookies.get('userSessionId')!.value
105
105
-
});
106
106
-
107
107
-
formValues.displayName = '';
108
108
-
formValues.realName = '';
109
109
-
formValues.comment = '';
110
110
-
111
111
-
commentSubmitted = true;
112
112
-
}
113
113
-
} catch (error) {
114
114
-
if (error instanceof Error) {
115
115
-
console.error(error.message);
116
116
-
}
117
117
-
}
118
118
-
}
119
119
-
120
120
-
const comments = await getComments({
121
121
-
baseUrl: String(Astro.url.origin),
122
122
-
hostId: `projects-${slug}`
123
123
-
});
124
32
---
125
33
126
34
<Layout
···
150
58
</article>
151
59
<div class='animate'>
152
60
<Socials />
153
153
-
</div>
154
154
-
<div class='animate w-full h-auto flex flex-col gap-3 mt-6'>
155
155
-
<span class='text-xl font-bold'>Comments</span>
156
156
-
157
157
-
{
158
158
-
commentSubmitted && (
159
159
-
<div class='border border-lime-600 text-lime-600 px-4 py-3 rounded'>
160
160
-
Comment posted successfully!
161
161
-
</div>
162
162
-
)
163
163
-
}
164
164
-
165
165
-
<form class='w-full flex flex-col gap-2' method='POST'>
166
166
-
<span>Post a comment</span>
167
167
-
<div class='w-full flex flex-col sm:flex-row justify-between gap-2'>
168
168
-
<label class='w-full text-xs flex flex-col gap-2'>
169
169
-
Display Name *
170
170
-
<input
171
171
-
type='text'
172
172
-
name='displayName'
173
173
-
value={formValues.displayName}
174
174
-
required
175
175
-
/>
176
176
-
{
177
177
-
errors.displayName && (
178
178
-
<span class='text-red-500'>{errors.displayName}</span>
179
179
-
)
180
180
-
}
181
181
-
</label>
182
182
-
<label class='w-full text-xs flex flex-col gap-2'>
183
183
-
Real Name (overrides Display Name)
184
184
-
<input type='text' name='realName' value={formValues.realName} />
185
185
-
{
186
186
-
errors.realName && (
187
187
-
<span class='text-red-500'>{errors.realName}</span>
188
188
-
)
189
189
-
}
190
190
-
</label>
191
191
-
</div>
192
192
-
<label class='text-xs flex flex-col gap-2'>
193
193
-
Comment *
194
194
-
<textarea name='comment' value={formValues.comment} required
195
195
-
></textarea>
196
196
-
{errors.comment && <span class='text-red-500'>{errors.comment}</span>}
197
197
-
</label>
198
198
-
<button
199
199
-
type='submit'
200
200
-
class='flex flex-nowrap py-2 px-4 pr-10 rounded-lg border border-black/15 dark:border-white/20
201
201
-
hover:bg-lime-500/30 dark:hover:bg-lime-300/5
202
202
-
hover:text-black dark:hover:text-white
203
203
-
hover:border-lime-700 dark:hover:border-lime-300
204
204
-
hover:shadow-lg hover:shadow-lime-600/30 dark:hover:shadow-lime-300/10
205
205
-
transition-all duration-300 ease-in-out
206
206
-
font-bold'
207
207
-
>Submit</button
208
208
-
>
209
209
-
</form>
210
210
-
{comments.map((comment) => <Comment comment={comment} />)}
211
61
</div>
212
62
</Container>
213
63
</Layout>