aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/BlogComments.jsx89
-rw-r--r--src/components/BlogCommentsForm.jsx124
-rw-r--r--src/components/GitHubProjects.jsx86
-rw-r--r--src/components/GitHubProjects.svelte81
-rw-r--r--src/components/Guestbook.jsx61
-rw-r--r--src/components/GuestbookForm.jsx74
-rw-r--r--src/components/Navbar.jsx2
7 files changed, 387 insertions, 130 deletions
diff --git a/src/components/BlogComments.jsx b/src/components/BlogComments.jsx
new file mode 100644
index 0000000..183fb94
--- /dev/null
+++ b/src/components/BlogComments.jsx
@@ -0,0 +1,89 @@
+import { Component } from 'preact';
+import { formatDate } from "../util";
+import sanitizeHtml from 'sanitize-html';
+import BlogCommentsForm from "./BlogCommentsForm.jsx";
+
+class BlogComments extends Component {
+ state = {
+ message: null,
+ error: null,
+ page: 1,
+ };
+
+ fetchMessages = async (page) => {
+ try {
+ const response = await fetch(`${import.meta.env.PUBLIC_API_URL}/comments/${this.props.slug}?page=${page}`);
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ this.setState({ error: errorData.message });
+ console.error('Failed to fetch data:', errorData.message);
+ return;
+ }
+
+ const { messages, totalPages } = await response.json();
+
+ this.setState({
+ message: messages,
+ totalPages: totalPages,
+ error: null // clear any previous error
+ });
+
+ } catch (error) {
+ this.setState({ error: `${error.message}` });
+ console.error('Failed to fetch data:', error);
+ }
+ }
+
+ refresh = async () => {
+ await this.fetchMessages(this.state.page);
+ }
+
+ async componentDidMount() {
+ await this.fetchMessages(this.state.page);
+ }
+
+ handleNext = async () => {
+ const nextPage = this.state.page + 1;
+ if (nextPage > this.state.totalPages) {
+ return;
+ }
+ this.setState({ page: nextPage });
+ await this.fetchMessages(nextPage);
+ }
+
+ handlePrevious = async () => {
+ const previousPage = this.state.page - 1;
+ this.setState({ page: previousPage });
+ await this.fetchMessages(previousPage);
+ }
+
+ render() {
+ const { message, error, page, totalPages } = this.state;
+
+ return (
+ <div>
+ <BlogCommentsForm onMessageSent={this.refresh} slug={this.props.slug} />
+ {error ? (
+ <p>{error}</p>
+ ) : message ? (
+ <div>
+ {message.map((g) => (
+ <article className="card">
+ <h1>{g.author}</h1>
+ <div dangerouslySetInnerHTML={{__html: sanitizeHtml(g.comment)}}/>
+ <small>{formatDate(g.created_at)}</small>
+ </article>
+ ))}
+ {page > 1 && <button class="button margin" onClick={this.handlePrevious}>Previous</button>}
+ {page < totalPages && <button class="button margin" onClick={this.handleNext}>Next</button>}
+ </div>
+ ) : (
+ <p>Loading comments...</p>
+ )}
+ </div>
+ );
+ }
+}
+
+export default BlogComments;
diff --git a/src/components/BlogCommentsForm.jsx b/src/components/BlogCommentsForm.jsx
new file mode 100644
index 0000000..b48236e
--- /dev/null
+++ b/src/components/BlogCommentsForm.jsx
@@ -0,0 +1,124 @@
+import { Component } from 'preact';
+import '../styles/Form.css';
+import { marked } from "marked";
+import DOMPurify from 'dompurify';
+
+class BlogCommentsForm extends Component {
+ state = {
+ author: '',
+ comment: '',
+ isMessageSent: false,
+ commentId: ''
+ };
+
+ componentDidMount() {
+ // Ensure the Turnstile script is loaded with explicit rendering
+ const script = document.createElement('script');
+ script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=onloadTurnstileCallback';
+ script.defer = true;
+ document.body.appendChild(script);
+
+ // Callback function to handle Turnstile token
+ window.onloadTurnstileCallback = () => {
+ window.turnstile.render('#turnstile-container', {
+ sitekey: '0x4AAAAAAAdb4uvxFFzNEDxB',
+ callback: (token) => {
+ // Here you can handle the token, e.g., by storing it in a hidden form field
+ this.setState({turnstileToken: token});
+ },
+ });
+ };
+
+ // Cleanup function to remove the script when the component unmounts
+ return () => {
+ document.body.removeChild(script);
+ };
+ }
+
+ handleChange = (e) => {
+ this.setState({ [e.target.name]: e.target.value });
+ }
+
+ handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (this.state.isMessageSent) {
+ this.setState({
+ errorMessage: 'You have already sent a comment.',
+ });
+ return;
+ }
+
+ try {
+ const messageHtml = marked(DOMPurify.sanitize(this.state.comment));
+ const { isMessageSent, errorMessage, ...messageData } = this.state; // Exclude isMessageSent from the data
+
+ const response = await fetch(`${import.meta.env.PUBLIC_API_URL}/comments/${this.props.slug}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ ...messageData, comment: messageHtml }),
+ });
+
+ if (!response.ok) {
+ const errorBody = await response.json();
+ const errorMessage = `HTTP Error ${response.status}: ${response.statusText}<br>${errorBody.message}`;
+
+ this.setState({
+ errorMessage: `There was an error submitting your comment.<br>Details:<br>${errorMessage}`,
+ isMessageSent: false,
+ });
+ return;
+ }
+
+ const responseBody = await response.json();
+
+ this.setState({
+ author: '',
+ comment: '',
+ isMessageSent: true,
+ errorMessage: '',
+ commentId: responseBody.id,
+ });
+
+ if (this.props.onMessageSent) {
+ this.props.onMessageSent();
+ }
+ } catch (error) {
+ this.setState({
+ errorMessage: `There was an error submitting your message.<br>Details:<br>${error.message}<br>Check the console for more details.`,
+ isMessageSent: false,
+ });
+ }
+ }
+
+ render() {
+ return (
+ <div className="card">
+ <form onSubmit={this.handleSubmit}>
+ <h2>Submit Comment</h2>
+ <label htmlFor="author">Author *</label>
+ <input type="text" name="author" placeholder="John Doe" required value={this.state.author} onChange={this.handleChange} disabled={this.state.isMessageSent}/>
+ <label htmlFor="comment">Comment * (Supports <a href="https://www.markdownguide.org/cheat-sheet/" target="_blank">Markdown</a>)</label>
+ <textarea name="comment" placeholder="Enter your comment here." required value={this.state.comment}
+ onChange={this.handleChange} disabled={this.state.isMessageSent}></textarea>
+ <div id="turnstile-container"></div>
+ <button class="button" type="submit" disabled={this.state.isMessageSent}>Send</button>
+ </form>
+ {this.state.errorMessage && <p dangerouslySetInnerHTML={{__html: this.state.errorMessage}}/>}
+ {this.state.isMessageSent && !this.state.errorMessage &&
+ <div>
+ <p>Sent successfully!</p>
+ <p>(Optional)</p>
+ <p>Save this message ID: {this.state.commentId}</p>
+ <p>Then send it to me, to verify that you sent this comment.</p>
+ <p><a href="mailto:andrew@alee14.me">Email me</a> or tag/message me on these <a href="/contacts">platforms</a>.</p>
+ </div>
+ }
+ </div>
+ );
+ }
+}
+
+export default BlogCommentsForm;
diff --git a/src/components/GitHubProjects.jsx b/src/components/GitHubProjects.jsx
deleted file mode 100644
index 33b4e64..0000000
--- a/src/components/GitHubProjects.jsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { useState, useEffect } from 'preact/hooks';
-
-const GitHubProjects = ({ username, isOrganization }) => {
- const [repos, setRepos] = useState([]);
- const [error, setError] = useState(null);
- const [isLoading, setIsLoading] = useState(true);
- const [currentPage, setCurrentPage] = useState(1);
- const reposPerPage = 10;
-
- const fetchRepos = async (page = 1, allRepos = []) => {
- const baseUrl = 'https://api.github.com';
- const url = isOrganization
- ? `${baseUrl}/orgs/${username}/repos?page=${page}&per_page=100`
- : `${baseUrl}/users/${username}/repos?page=${page}&per_page=100`;
-
- try {
- const response = await fetch(url);
- const data = await response.json();
-
- if (response.ok) {
- if (Array.isArray(data)) {
- allRepos = allRepos.concat(data.filter(repo => !repo.fork));
- // If the data length is 100, there might be more repositories to fetch
- if (data.length === 100) {
- return fetchRepos(page + 1, allRepos);
- }
- } else {
- console.error('Unexpected data format:', data);
- setError('Unexpected data format');
- setRepos([]);
- }
- } else {
- console.error('API error:', data);
- setError(data.message);
- setRepos([]);
- }
- } catch (err) {
- console.error('Failed to fetch projects:', err);
- setError(err.message);
- setRepos([]);
- }
-
- setIsLoading(false);
- return allRepos;
- };
-
- useEffect(() => {
- fetchRepos().then(setRepos);
- }, [username, isOrganization]);
-
- const indexOfLastRepo = currentPage * reposPerPage;
- const indexOfFirstRepo = indexOfLastRepo - reposPerPage;
- const currentRepos = repos.slice(indexOfFirstRepo, indexOfLastRepo);
-
- const nextPage = () => setCurrentPage(currentPage + 1);
- const prevPage = () => setCurrentPage(currentPage - 1);
-
- return (
- <div>
- {isLoading ? (
- <div>Loading...</div>
- ) : (
- <>
- {error && <div class="error">{error}</div>}
- <div class="grid">
- {currentRepos.map((repo) => (
- <article class="card">
- <h1>{repo.name}</h1>
- <p>{repo.description}</p>
- <div class="row">
- <a href={repo.html_url} target="_blank">Repository</a>
- </div>
- </article>
- ))}
- </div>
- <div>
- {currentPage > 1 && <button class="button margin" onClick={prevPage}>Previous</button>}
- {currentPage < Math.ceil(repos.length / reposPerPage) && <button class="button margin" onClick={nextPage}>Next</button>}
- </div>
- </>
- )}
- </div>
- );
-};
-
-export default GitHubProjects;
diff --git a/src/components/GitHubProjects.svelte b/src/components/GitHubProjects.svelte
new file mode 100644
index 0000000..2753b89
--- /dev/null
+++ b/src/components/GitHubProjects.svelte
@@ -0,0 +1,81 @@
+<script>
+ import { onMount } from 'svelte';
+ export let username;
+ export let isOrganization;
+ let repos = [];
+ let error = null;
+ let isLoading = true;
+ let currentPage = 1;
+ const reposPerPage = 10;
+
+ const fetchRepos = async (page = 1, allRepos = []) => {
+ const baseUrl = 'https://api.github.com';
+ const url = isOrganization
+ ? `${baseUrl}/orgs/${username}/repos?page=${page}&per_page=100`
+ : `${baseUrl}/users/${username}/repos?page=${page}&per_page=100`;
+
+ try {
+ const response = await fetch(url);
+ const data = await response.json();
+
+ if (response.ok) {
+ if (Array.isArray(data)) {
+ allRepos = allRepos.concat(data.filter(repo => !repo.fork));
+ if (data.length === 100) {
+ return fetchRepos(page + 1, allRepos);
+ }
+ } else {
+ console.error('Unexpected data format:', data);
+ error = 'Unexpected data format';
+ repos = [];
+ }
+ } else {
+ console.error('API error:', data);
+ error = data.message;
+ repos = [];
+ }
+ } catch (err) {
+ console.error('Failed to fetch projects:', err);
+ error = err.message;
+ repos = [];
+ }
+
+ isLoading = false;
+ return allRepos;
+ };
+
+ onMount(() => {
+ fetchRepos().then(r => repos = r);
+ });
+
+ const nextPage = () => currentPage++;
+ const prevPage = () => currentPage--;
+</script>
+
+<div>
+ {#if isLoading}
+ <div>Loading...</div>
+ {:else}
+ {#if error}
+ <div class="error">{error}</div>
+ {:else}
+ <div class="grid">
+ {#each repos.slice((currentPage - 1) * reposPerPage, currentPage * reposPerPage) as repo (repo.id)}
+ <article class="card">
+ <h1>{repo.name}</h1>
+ <p>{repo.description || 'No description provided'}</p>
+ <div class="row">
+ <a href={repo.html_url} target="_blank">Repository</a>
+ </div>
+ </article>
+ {/each}
+ </div>
+ {#if currentPage > 1}
+ <button class="button margin" on:click={prevPage}>Previous</button>
+ {/if}
+ {#if currentPage < Math.ceil(repos.length / reposPerPage)}
+ <button class="button margin" on:click={nextPage}>Next</button>
+ {/if}
+ {/if}
+ {/if}
+</div>
diff --git a/src/components/Guestbook.jsx b/src/components/Guestbook.jsx
index 584a4eb..e3238f9 100644
--- a/src/components/Guestbook.jsx
+++ b/src/components/Guestbook.jsx
@@ -1,5 +1,4 @@
import { Component } from 'preact';
-import { supabase } from '../services/supabase';
import { formatDate } from "../util";
import sanitizeHtml from 'sanitize-html';
import GuestbookForm from "./GuestbookForm.jsx";
@@ -12,23 +11,27 @@ class Guestbook extends Component {
};
fetchMessages = async (page) => {
- const perPage = 10;
- const start = (page - 1) * perPage;
- const end = start + perPage - 1;
+ try {
+ const response = await fetch(`${import.meta.env.PUBLIC_API_URL}/guestbook?page=${page}`);
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ this.setState({ error: `Failed to fetch data: ${errorData.message}` });
+ console.error('Failed to fetch data:', errorData.message);
+ return;
+ }
+
+ const { messages, totalPages } = await response.json();
- let { data: messages, error, count } = await supabase
- .from('guestbook')
- .select('*', { count: 'exact' })
- .range(start, end)
- .order('created_at', { ascending: false });
- if (error) {
- this.setState({ error: `Failed to fetch data: ${error.message}` });
- console.error('Failed to fetch data:', error);
- } else {
this.setState({
message: messages,
- totalPages: Math.ceil(count / perPage)
+ totalPages: totalPages,
+ error: null // clear any previous error
});
+
+ } catch (error) {
+ this.setState({ error: `Failed to fetch data: ${error.message}` });
+ console.error('Failed to fetch data:', error);
}
}
@@ -63,22 +66,24 @@ class Guestbook extends Component {
<GuestbookForm onMessageSent={this.refresh} />
{error ? (
<p>{error}</p>
- ) : !message ? (
- <p>Loading messages...</p>
- ) : (
- <div class="grid">
- {message.map((g) => (
- <article class="card">
- <h1>Message from: {g.name}</h1>
- <small>{formatDate(g.created_at)}</small>
- <div dangerouslySetInnerHTML={{__html: sanitizeHtml(g.message)}}/>
- {g.website && <a href={g.website} target="_blank">My Website</a>}
- </article>
- ))}
+ ) : message ? (
+ <div>
+ <div className="grid">
+ {message.map((g) => (
+ <article className="card">
+ <h1>Message from: {g.name}</h1>
+ <small>{formatDate(g.created_at)}</small>
+ <div dangerouslySetInnerHTML={{__html: sanitizeHtml(g.message)}}/>
+ {g.website && <a href={g.website} target="_blank">My Website</a>}
+ </article>
+ ))}
+ </div>
+ {page > 1 && <button class="button margin" onClick={this.handlePrevious}>Previous</button>}
+ {page < totalPages && <button class="button margin" onClick={this.handleNext}>Next</button>}
</div>
+ ) : (
+ <p>Loading messages...</p>
)}
- {page > 1 && <button class="button margin" onClick={this.handlePrevious}>Previous</button>}
- {page < totalPages && <button class="button margin" onClick={this.handleNext}>Next</button>}
</div>
);
}
diff --git a/src/components/GuestbookForm.jsx b/src/components/GuestbookForm.jsx
index 613c9d3..1f1d3c9 100644
--- a/src/components/GuestbookForm.jsx
+++ b/src/components/GuestbookForm.jsx
@@ -1,6 +1,5 @@
import { Component } from 'preact';
-import { createMessage } from '../services/GuestbookService';
-import '../styles/GuestbookForm.css';
+import '../styles/Form.css';
import { marked } from "marked";
import DOMPurify from 'dompurify';
@@ -10,8 +9,33 @@ class GuestbookForm extends Component {
website: '',
message: '',
isMessageSent: false,
+ messageId: ''
};
+ componentDidMount() {
+ // Ensure the Turnstile script is loaded with explicit rendering
+ const script = document.createElement('script');
+ script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=onloadTurnstileCallback';
+ script.defer = true;
+ document.body.appendChild(script);
+
+ // Callback function to handle Turnstile token
+ window.onloadTurnstileCallback = () => {
+ window.turnstile.render('#turnstile-container', {
+ sitekey: '0x4AAAAAAAdb4uvxFFzNEDxB',
+ callback: (token) => {
+ // Here you can handle the token, e.g., by storing it in a hidden form field
+ this.setState({turnstileToken: token});
+ },
+ });
+ };
+
+ // Cleanup function to remove the script when the component unmounts
+ return () => {
+ document.body.removeChild(script);
+ };
+ }
+
handleChange = (e) => {
this.setState({ [e.target.name]: e.target.value });
}
@@ -26,20 +50,30 @@ class GuestbookForm extends Component {
return;
}
- const urlRegex = /(https?:\/\/\S+)/g;
- const imageRegex = /!\[.*]\(.*\)/g;
-
- if (urlRegex.test(this.state.message) || imageRegex.test(this.state.message)) {
- this.setState({
- errorMessage: 'Links and images are not allowed.',
- });
- return;
- }
-
try {
const messageHtml = marked(DOMPurify.sanitize(this.state.message));
const { isMessageSent, errorMessage, ...messageData } = this.state; // Exclude isMessageSent from the data
- await createMessage({ ...messageData, message: messageHtml });
+
+ const response = await fetch(`${import.meta.env.PUBLIC_API_URL}/guestbook`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ ...messageData, message: messageHtml }),
+ });
+
+ if (!response.ok) {
+ const errorBody = await response.json();
+ const errorMessage = `HTTP Error ${response.status}: ${response.statusText}<br>${errorBody.message}`;
+
+ this.setState({
+ errorMessage: `There was an error submitting your message.<br>Details:<br>${errorMessage}`,
+ isMessageSent: false,
+ });
+ return;
+ }
+
+ const responseBody = await response.json();
this.setState({
name: '',
@@ -47,6 +81,7 @@ class GuestbookForm extends Component {
message: '',
isMessageSent: true,
errorMessage: '',
+ messageId: responseBody.id,
});
if (this.props.onMessageSent) {
@@ -54,7 +89,7 @@ class GuestbookForm extends Component {
}
} catch (error) {
this.setState({
- errorMessage: `There was an error submitting your message.<br>Details: ${error}<br>Check the console for more details.`,
+ errorMessage: `There was an error submitting your message.<br>Details:<br>${error.message}<br>Check the console for more details.`,
isMessageSent: false,
});
}
@@ -72,10 +107,19 @@ class GuestbookForm extends Component {
<label htmlFor="message">Message * (Supports <a href="https://www.markdownguide.org/cheat-sheet/" target="_blank">Markdown</a>)</label>
<textarea name="message" placeholder="Enter your message here." required value={this.state.message}
onChange={this.handleChange} disabled={this.state.isMessageSent}></textarea>
+ <div id="turnstile-container"></div>
<button class="button" type="submit" disabled={this.state.isMessageSent}>Send</button>
</form>
{this.state.errorMessage && <p dangerouslySetInnerHTML={{__html: this.state.errorMessage}}/>}
- {this.state.isMessageSent && !this.state.errorMessage && <p>Sent successfully!</p>}
+ {this.state.isMessageSent && !this.state.errorMessage &&
+ <div>
+ <p>Sent successfully!</p>
+ <p>(Optional)</p>
+ <p>Save this message ID: {this.state.messageId}</p>
+ <p>Then send it to me, to verify that you sent this message.</p>
+ <p><a href="mailto:andrew@alee14.me">Email me</a> or tag/message me on these <a href="/contacts">platforms</a>.</p>
+ </div>
+ }
</div>
);
}
diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx
index cda6b67..f76a5f2 100644
--- a/src/components/Navbar.jsx
+++ b/src/components/Navbar.jsx
@@ -26,4 +26,4 @@ class Navbar extends Component {
}
}
-export default Navbar; \ No newline at end of file
+export default Navbar;