Skip to content

Commit 2748dec

Browse files
committed
Episode 20 - comments updating, better components
1 parent fe4d01e commit 2748dec

File tree

11 files changed

+483
-325
lines changed

11 files changed

+483
-325
lines changed

.prettierrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"useTabs": false,
3+
"singleQuote": true,
4+
"printWidth": 100
5+
}

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@
44
npm run dev -- --port 5000
55
```
66

7+
## EPISODE 20 -
8+
9+
```
10+
new file: .prettierrc
11+
modified: README.md
12+
new file: src/lib/app/comments/CommentCard.svelte
13+
new file: src/lib/app/comments/CommentForm.svelte
14+
new file: src/lib/app/posts/PostCard.svelte
15+
new file: src/lib/app/posts/PostForm.svelte
16+
modified: src/lib/shared/helpers.js
17+
modified: src/routes/index.svelte
18+
modified: src/routes/posts/[slug].svelte
19+
modified: src/routes/users/sign-in.svelte
20+
modified: src/routes/users/sign-up.svelte
21+
```
22+
723
## EPISODE 19 - Post Slug and Display Comments
824

925
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script>
2+
// import DOMPurify from 'isomorphic-dompurify';
3+
import snarkdown from 'snarkdown';
4+
5+
export let comment;
6+
</script>
7+
8+
<div>
9+
{@html snarkdown(comment.attributes.body)}
10+
</div>
11+
<div>
12+
commented by
13+
<a href="/users/{comment.attributes.user.slug}">
14+
{comment.attributes.user.displayName}
15+
</a>
16+
</div>
17+
<div>
18+
{comment.attributes.updatedAt}
19+
</div>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<script>
2+
import { createEventDispatcher } from 'svelte';
3+
import { slide } from 'svelte/transition';
4+
import { session } from '$app/stores';
5+
import * as api from '$lib/shared/apis';
6+
import { aud, user } from '$lib/shared/stores';
7+
import SubmitButton from '$lib/components/buttons/Submit.svelte';
8+
9+
export let comment = undefined,
10+
commentable,
11+
type,
12+
errors = [],
13+
success = '';
14+
let submitting = false;
15+
let body;
16+
17+
const dispatch = createEventDispatcher();
18+
19+
if (comment) {
20+
body = comment.attributes.body;
21+
}
22+
23+
async function handleSubmit() {
24+
submitting = true;
25+
errors = [];
26+
let response, json;
27+
let sess = { jwt: $user.jwt, aud: $aud };
28+
let data = {
29+
comment: {
30+
body,
31+
commentable_id: commentable.id,
32+
commentable_type: commentable.type,
33+
},
34+
};
35+
if (type === 'new') {
36+
const ret = await api.post($session.API_ENDPOINT, `api/v1/comments/`, data, sess);
37+
response = ret.response;
38+
json = ret.json;
39+
} else {
40+
const ret = await api.put($session.API_ENDPOINT, `api/v1/comments/${comment.id}`, data, sess);
41+
response = ret.response;
42+
json = ret.json;
43+
}
44+
if (response.status === 200) {
45+
body = undefined;
46+
dispatch('saved', json.data);
47+
success = json.meta.message;
48+
} else if (response.status === 401 || response.status === 404 || response.status === 406) {
49+
errors = [json.meta.message];
50+
} else if (response.status === 500) {
51+
errors = ['Oops, something went wrong! How embarrassing, try again soon.'];
52+
}
53+
submitting = false;
54+
}
55+
</script>
56+
57+
<form
58+
action="#"
59+
method="POST"
60+
class="mb-6"
61+
transition:slide
62+
on:submit|preventDefault={handleSubmit}
63+
>
64+
<!-- These classes only if new -->
65+
<div class={type === 'new' ? 'shadow sm:rounded-md sm:overflow-hidden' : ''}>
66+
<div class="px-4 py-5 bg-white space-y-6 sm:p-6">
67+
<div>
68+
<label for="content" class="block text-sm font-medium text-gray-700"> Comment Body </label>
69+
<div class="mt-1">
70+
<textarea
71+
id="content"
72+
name="content"
73+
rows="3"
74+
class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
75+
placeholder="Write something magnificent"
76+
bind:value={body}
77+
/>
78+
</div>
79+
<p class="mt-2 text-sm text-gray-500">Markdown supported</p>
80+
</div>
81+
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
82+
<SubmitButton {submitting}>{type === 'new' ? 'Save' : 'Update'}</SubmitButton>
83+
<input
84+
type="button"
85+
value="cancel"
86+
on:click={() => {
87+
dispatch('cancel');
88+
}}
89+
/>
90+
</div>
91+
</div>
92+
</div>
93+
</form>

src/lib/app/posts/PostCard.svelte

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<script>
2+
// import DOMPurify from 'isomorphic-dompurify';
3+
import snarkdown from 'snarkdown';
4+
import { createEventDispatcher } from 'svelte';
5+
import { session } from '$app/stores';
6+
import * as api from '$lib/shared/apis';
7+
import { pluralize } from '$lib/shared/helpers';
8+
import { aud, user } from '$lib/shared/stores';
9+
10+
export let post, errors = [], success = '';
11+
12+
const dispatch = createEventDispatcher();
13+
14+
async function handleDestroy(post) {
15+
errors = [];
16+
const { response, json } = await api.del(
17+
$session.API_ENDPOINT,
18+
`api/v1/posts/${post.id}`,
19+
{},
20+
{ jwt: $user.jwt, aud: $aud }
21+
);
22+
if (response.status === 200) {
23+
dispatch('destroy', post);
24+
success = json.meta.message;
25+
} else if (response.status === 401 || response.status === 404 || response.status === 406) {
26+
errors = json.meta.message;
27+
} else if (response.status === 500) {
28+
errors = ['Oops, something went wrong! How embarrassing, try again soon.'];
29+
}
30+
}
31+
</script>
32+
33+
<div class="flex justify-between space-x-3">
34+
<div class="min-w-0 flex-1">
35+
<a href="/posts/{post.attributes.slug}" class="block focus:outline-none" sveltekit:prefetch>
36+
<p class="text-sm font-medium text-gray-900 truncate">
37+
{post.attributes.title}
38+
</p>
39+
</a>
40+
<p class="text-sm text-gray-500 truncate">
41+
<a href="/users/{post.attributes.user.slug}" sveltekit:prefetch>
42+
Posted by {post.attributes.user.displayName}
43+
</a>
44+
{#if $user?.user?.id === post.attributes.user.id}
45+
<input
46+
type="button"
47+
value="edit"
48+
on:click={() => {
49+
post.edit = !post.edit;
50+
}}
51+
/>
52+
<input type="button" value="delete" on:click={handleDestroy(post)} />
53+
{/if}
54+
</p>
55+
</div>
56+
<time
57+
datetime={post.attributes.publishedAt}
58+
class="flex-shrink-0 whitespace-nowrap text-sm text-gray-500"
59+
>
60+
{post.attributes.publishedAt}
61+
</time>
62+
</div>
63+
<div class="mt-1">
64+
<p
65+
class="{post.expand
66+
? ''
67+
: 'line-clamp-2'} text-sm text-gray-600 overflow-x-hidden cursor-pointer markdown"
68+
on:click|preventDefault={() => {
69+
post.expand = !post.expand;
70+
}}
71+
>
72+
{#if post.expand}
73+
<!-- {@html DOMPurify.sanitize(snarkdown(post.attributes.content))} -->
74+
{@html snarkdown(post.attributes.content)}
75+
{:else}
76+
<!-- {@html DOMPurify.sanitize(snarkdown(post.attributes.content.substring(0, 80)))} -->
77+
{@html snarkdown(post.attributes.content.substring(0, 80))}
78+
{/if}
79+
</p>
80+
</div>
81+
<div class="mt-1 text-sm text-gray-500">
82+
{pluralize('comment', post.attributes.commentsCount)}
83+
</div>

src/lib/app/posts/PostForm.svelte

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<script>
2+
import { createEventDispatcher } from 'svelte';
3+
import { slide } from 'svelte/transition';
4+
import { session } from '$app/stores';
5+
import * as api from '$lib/shared/apis';
6+
import { aud, user } from '$lib/shared/stores';
7+
import SubmitButton from '$lib/components/buttons/Submit.svelte';
8+
9+
export let post = undefined,
10+
type,
11+
errors,
12+
success;
13+
let submitting = false;
14+
let published_at;
15+
let title, content;
16+
17+
const dispatch = createEventDispatcher();
18+
19+
if (post) {
20+
title = post.attributes.title;
21+
content = post.attributes.content;
22+
published_at = post.attributes.publishedAt;
23+
}
24+
25+
async function handleSubmit() {
26+
submitting = true;
27+
errors = [];
28+
let response, json;
29+
let sess = { jwt: $user.jwt, aud: $aud };
30+
let data = {
31+
post: {
32+
title,
33+
content,
34+
published_at,
35+
},
36+
};
37+
if (type === 'new') {
38+
const ret = await api.post($session.API_ENDPOINT, `api/v1/posts/`, data, sess);
39+
response = ret.response;
40+
json = ret.json;
41+
} else {
42+
const ret = await api.put($session.API_ENDPOINT, `api/v1/posts/${post.id}`, data, sess);
43+
response = ret.response;
44+
json = ret.json;
45+
}
46+
if (response.status === 200) {
47+
title = undefined;
48+
content = undefined;
49+
if (post) {
50+
post.attributes = json.data.attributes
51+
}
52+
dispatch('saved', json.data);
53+
success = json.meta.message;
54+
} else if (response.status === 401 || response.status === 404 || response.status === 406) {
55+
errors = [json.meta.message];
56+
} else if (response.status === 500) {
57+
errors = ['Oops, something went wrong! How embarrassing, try again soon.'];
58+
}
59+
submitting = false;
60+
}
61+
</script>
62+
63+
<form
64+
action="#"
65+
method="POST"
66+
class="mb-6"
67+
transition:slide
68+
on:submit|preventDefault={handleSubmit}
69+
>
70+
<!-- These classes only if new -->
71+
<div class={type === 'new' ? 'shadow sm:rounded-md sm:overflow-hidden' : ''}>
72+
<div class="px-4 py-5 bg-white space-y-6 sm:p-6">
73+
<div class="grid grid-cols-3 gap-6">
74+
<div class="col-span-3 sm:col-span-2">
75+
<label for="title" class="block text-sm font-medium text-gray-700"> Title </label>
76+
<div class="mt-1 flex rounded-md shadow-sm">
77+
<input
78+
type="text"
79+
name="title"
80+
id="title"
81+
class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-md sm:text-sm border-gray-300"
82+
placeholder="Title for your Post"
83+
bind:value={title}
84+
/>
85+
</div>
86+
</div>
87+
</div>
88+
89+
<div>
90+
<label for="content" class="block text-sm font-medium text-gray-700"> Post Body </label>
91+
<div class="mt-1">
92+
<textarea
93+
id="content"
94+
name="content"
95+
rows="3"
96+
class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
97+
placeholder="Write something magnificent"
98+
bind:value={content}
99+
/>
100+
</div>
101+
<p class="mt-2 text-sm text-gray-500">Markdown supported</p>
102+
</div>
103+
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
104+
<SubmitButton {submitting}>{type === 'new' ? 'Save' : 'Update'}</SubmitButton>
105+
<input
106+
type="button"
107+
value="cancel"
108+
on:click={() => {
109+
dispatch('cancel');
110+
}}
111+
/>
112+
</div>
113+
</div>
114+
</div>
115+
</form>

src/lib/shared/helpers.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,12 @@ export function browserDetector(navigator, window) {
8484
};
8585
return module;
8686
}
87+
88+
/*
89+
* Given a word and count, pluralize the word.
90+
* Optionally, include withCount = false to not include the # in the returned string
91+
*/
92+
export function pluralize(word, count, withCount = true) {
93+
const out = (count > 1 || count === 0) ? `${word}s` : word;
94+
return withCount ? `${count} ${out}` : out;
95+
}

0 commit comments

Comments
 (0)