diff options
28 files changed, 553 insertions, 478 deletions
diff --git a/astro.config.mjs b/astro.config.mjs index c17d797..fd2a541 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,18 +1,20 @@ import { defineConfig, passthroughImageService } from 'astro/config'; import icon from "astro-icon"; import vercel from "@astrojs/vercel/serverless"; - import preact from "@astrojs/preact"; +import svelte from "@astrojs/svelte"; // https://astro.build/config export default defineConfig({ site: 'https://alee14.me', - integrations: [icon(), preact()], + integrations: [icon(), preact({ + compat: true + }), svelte()], output: "server", adapter: vercel({ webAnalytics: { - enabled: true, - }, + enabled: true + } }), image: { service: passthroughImageService() Binary files differdiff --git a/package.json b/package.json index cac708e..1036837 100644 --- a/package.json +++ b/package.json @@ -10,18 +10,20 @@ "astro": "astro" }, "dependencies": { - "@astrojs/preact": "^3.1.0", - "@astrojs/rss": "4.0.5", - "@astrojs/vercel": "^7.3.5", - "@iconify-json/fa6-brands": "^1.1.18", - "@iconify-json/fa6-solid": "^1.1.20", - "@supabase/supabase-js": "^2.39.7", - "astro": "^4.4.14", + "@astrojs/preact": "3.5.0", + "@astrojs/rss": "4.0.7", + "@astrojs/svelte": "^5.6.0", + "@astrojs/vercel": "7.7.2", + "@iconify-json/fa6-brands": "^1.1.19", + "@iconify-json/fa6-solid": "^1.1.21", + "astro": "4.11.3", "astro-icon": "^1.1.0", - "dompurify": "^3.0.9", - "markdown-it": "^14.0.0", - "marked": "^12.0.1", - "preact": "^10.19.3", - "sanitize-html": "^2.12.1" + "dompurify": "^3.1.5", + "markdown-it": "^14.1.0", + "marked": "^13.0.0", + "preact": "^10.22.0", + "sanitize-html": "^2.13.0", + "svelte": "^4.2.18", + "typescript": "^5.4.5" } } diff --git a/public/blog/images/2024/website-redesign/2020-redesign.png b/public/blog/images/2024/website-redesign/2020-redesign.png Binary files differnew file mode 100644 index 0000000..67c39af --- /dev/null +++ b/public/blog/images/2024/website-redesign/2020-redesign.png diff --git a/public/blog/images/2024/website-redesign/2021-2022-redesign.png b/public/blog/images/2024/website-redesign/2021-2022-redesign.png Binary files differnew file mode 100644 index 0000000..3f0eec0 --- /dev/null +++ b/public/blog/images/2024/website-redesign/2021-2022-redesign.png 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; diff --git a/src/content/blog/2024/test.md b/src/content/blog/2024/test.md new file mode 100644 index 0000000..e8d6c8b --- /dev/null +++ b/src/content/blog/2024/test.md @@ -0,0 +1,10 @@ +--- +title: Test page +description: test post +pubDate: 2024-06-11T00:30:22.112Z +tags: + - test-tag +slug: test-post +--- + +# Hello world diff --git a/src/content/blog/2024/website-redesigned.md b/src/content/blog/2024/website-redesigned.md new file mode 100644 index 0000000..48747fe --- /dev/null +++ b/src/content/blog/2024/website-redesigned.md @@ -0,0 +1,23 @@ +--- +title: Website Redesigned (Again!) +description: Decided to redesign my website again, and hopefully the final time for a while. +pubDate: 2024-06-11T00:30:22.112Z +tags: + - announcement +slug: website-redesigned +--- +# Another website redesign?!? +Yeah, I decided to redesign this site again. I actually had plans to complete the 2021 version for a while, but I decided to scrap that. I mean if you take a look at it, the website pretty much incomplete like the blog, downloads page, and about section were missing if you compare with the 2020 version (though I stripped blogs during the 2020 version). + +These were originally planned, but it became complicated to implement and integrate. The core concept of the website is that there was no navigation bar, so I wasn't sure how navigation would work. Another thing is that this website was originally meant to be a long single page, and of course I wouldn't know how to implement that either. As you can see I had pretty ambitious plans for this site in 2021, but they ended up being canned. + +[](/blog/images/2024/website-redesign/2021-2022-redesign.png) +[](/blog/images/2024/website-redesign/2020-redesign.png) + +# What's different with this new website? +This new website pretty much combined stuff that worked in the past. Things like the navbar, cards (from Projects), and the icons in the homepage. There is some added in this that was not in past websites, like the contacts page which was pretty much the solution of the icons being crowded during the 2021 website, and a guestbook, it is basically feedback on my website, you can use it to comment on my blogs, or send me nice messages. + +# Why do you continue with creating a website, why don't you just use a website builder? +The reason is that this is a **personal website**, I get to create things myself and not depend on a third party to do stuff for me. I want to write my own theme, and entire infrastructure myself without needing to use a bloated builder that would add unnecessary junk and javascript to the site. I have always wanted to make my own site, and I think I have done so, which fulfills my childhood dream to create my own website. + +Also by creating a website, I would always try to improve it and make it better than the last one, and I am happy to say that this new site is way better than any of my previous ones. diff --git a/src/content/blog/hello.md b/src/content/blog/hello.md deleted file mode 100644 index 5a16324..0000000 --- a/src/content/blog/hello.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -title: Hello world -description: This is a test post -pubDate: 2024-01-24 -tags: ["tag1", "tag2"] -slug: hello-world ---- - -# h1 Heading 8-) -## h2 Heading -### h3 Heading -#### h4 Heading -##### h5 Heading -###### h6 Heading - - -## Horizontal Rules - -___ - ---- - -*** - - -## Typographic replacements - -Enable typographer option to see result. - -(c) (C) (r) (R) (tm) (TM) (p) (P) +- - -test.. test... test..... test?..... test!.... - -!!!!!! ???? ,, -- --- - -"Smartypants, double quotes" and 'single quotes' - - -## Emphasis - -**This is bold text** - -__This is bold text__ - -*This is italic text* - -_This is italic text_ - -~~Strikethrough~~ - - -## Blockquotes - - -> Blockquotes can also be nested... ->> ...by using additional greater-than signs right next to each other... -> > > ...or with spaces between arrows. - - -## Lists - -Unordered - -+ Create a list by starting a line with `+`, `-`, or `*` -+ Sub-lists are made by indenting 2 spaces: - - Marker character change forces new list start: - * Ac tristique libero volutpat at - + Facilisis in pretium nisl aliquet - - Nulla volutpat aliquam velit -+ Very easy! - -Ordered - -1. Lorem ipsum dolor sit amet -2. Consectetur adipiscing elit -3. Integer molestie lorem at massa - - -1. You can use sequential numbers... -1. ...or keep all the numbers as `1.` - -Start numbering with offset: - -57. foo -1. bar - - -## Code - -Inline `code` - -Indented code - - // Some comments - line 1 of code - line 2 of code - line 3 of code - - -Block code "fences" - -``` -Sample text here... -``` - -Syntax highlighting - -``` js -var foo = function (bar) { - return bar++; -}; - -console.log(foo(5)); -``` - -## Tables - -| Option | Description | -| ------ | ----------- | -| data | path to data files to supply the data that will be passed into templates. | -| engine | engine to be used for processing templates. Handlebars is the default. | -| ext | extension to be used for dest files. | - -Right aligned columns - -| Option | Description | -| ------:| -----------:| -| data | path to data files to supply the data that will be passed into templates. | -| engine | engine to be used for processing templates. Handlebars is the default. | -| ext | extension to be used for dest files. | - - -## Links - -[link text](http://dev.nodeca.com) - -[link with title](http://nodeca.github.io/pica/demo/ "title text!") - -Autoconverted link https://github.com/nodeca/pica (enable linkify to see) - - -## Images - - - - -Like links, Images also have a footnote style syntax - -![Alt text][id] - -With a reference later in the document defining the URL location: - -[id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat" - - -## Plugins - -The killer feature of `markdown-it` is very effective support of -[syntax plugins](https://www.npmjs.org/browse/keyword/markdown-it-plugin). - - -### [Emojies](https://github.com/markdown-it/markdown-it-emoji) - -> Classic markup: :wink: :cry: :laughing: :yum: -> -> Shortcuts (emoticons): :-) :-( 8-) ;) - -see [how to change output](https://github.com/markdown-it/markdown-it-emoji#change-output) with twemoji. - - -### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup) - -- 19^th^ -- H~2~O - - -### [\<ins>](https://github.com/markdown-it/markdown-it-ins) - -++Inserted text++ - - -### [\<mark>](https://github.com/markdown-it/markdown-it-mark) - -==Marked text== - - -### [Footnotes](https://github.com/markdown-it/markdown-it-footnote) - -Footnote 1 link[^first]. - -Footnote 2 link[^second]. - -Inline footnote^[Text of inline footnote] definition. - -Duplicated footnote reference[^second]. - -[^first]: Footnote **can have markup** - - and multiple paragraphs. - -[^second]: Footnote text. - - -### [Definition lists](https://github.com/markdown-it/markdown-it-deflist) - -Term 1 - -: Definition 1 -with lazy continuation. - -Term 2 with *inline markup* - -: Definition 2 - - { some code, part of Definition 2 } - - Third paragraph of definition 2. - -_Compact style:_ - -Term 1 -~ Definition 1 - -Term 2 -~ Definition 2a -~ Definition 2b - - -### [Abbreviations](https://github.com/markdown-it/markdown-it-abbr) - -This is HTML abbreviation example. - -It converts "HTML", but keep intact partial entries like "xxxHTMLyyy" and so on. - -*[HTML]: Hyper Text Markup Language - -### [Custom containers](https://github.com/markdown-it/markdown-it-container) - -::: warning -*here be dragons* -::: diff --git a/src/content/blog/hello2.md b/src/content/blog/hello2.md deleted file mode 100644 index a25d639..0000000 --- a/src/content/blog/hello2.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Lorem ipsum dolor sit amet -description: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -pubDate: 2024-01-25 -tags: ["tag1", "tag2"] -slug: lorem-ipsum ---- -# Lorem ipsum dolor sit amet -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tincidunt ornare massa eget egestas purus viverra accumsan in. Id cursus metus aliquam eleifend mi in. Augue lacus viverra vitae congue eu consequat. Blandit turpis cursus in hac habitasse platea. In aliquam sem fringilla ut. Non arcu risus quis varius quam quisque id. Posuere sollicitudin aliquam ultrices sagittis orci. Leo integer malesuada nunc vel risus commodo. Amet aliquam id diam maecenas ultricies mi eget mauris. Porta nibh venenatis cras sed felis eget velit aliquet sagittis. Mollis aliquam ut porttitor leo a diam sollicitudin. Blandit cursus risus at ultrices mi tempus imperdiet nulla malesuada. Sapien nec sagittis aliquam malesuada bibendum arcu vitae. Ut sem nulla pharetra diam. - -A arcu cursus vitae congue. Sem et tortor consequat id porta nibh. Dignissim diam quis enim lobortis scelerisque. Est ultricies integer quis auctor elit sed vulputate mi sit. Fames ac turpis egestas maecenas pharetra convallis posuere morbi. Dui sapien eget mi proin sed. In fermentum et sollicitudin ac orci phasellus. Diam quam nulla porttitor massa id neque aliquam vestibulum morbi. Hendrerit gravida rutrum quisque non. Nunc vel risus commodo viverra maecenas accumsan lacus vel facilisis. Sed cras ornare arcu dui. - -Amet cursus sit amet dictum sit amet. Sed adipiscing diam donec adipiscing tristique. Vestibulum sed arcu non odio. Odio euismod lacinia at quis risus sed. Quam pellentesque nec nam aliquam sem. Rhoncus aenean vel elit scelerisque mauris. Et ligula ullamcorper malesuada proin libero nunc consequat interdum varius. Eu turpis egestas pretium aenean pharetra magna ac placerat vestibulum. Venenatis urna cursus eget nunc scelerisque. At varius vel pharetra vel turpis nunc. Imperdiet massa tincidunt nunc pulvinar. Eu facilisis sed odio morbi quis commodo odio. Ipsum dolor sit amet consectetur adipiscing elit ut. Ac placerat vestibulum lectus mauris. Ultricies integer quis auctor elit sed vulputate mi sit amet. Mauris augue neque gravida in fermentum. - -Tempus imperdiet nulla malesuada pellentesque elit eget gravida cum. Pharetra vel turpis nunc eget lorem dolor sed. Sem et tortor consequat id porta nibh venenatis. Tellus pellentesque eu tincidunt tortor aliquam nulla facilisi cras fermentum. Magna etiam tempor orci eu lobortis. Vulputate eu scelerisque felis imperdiet proin fermentum leo vel. A iaculis at erat pellentesque. Convallis a cras semper auctor neque vitae. Phasellus egestas tellus rutrum tellus pellentesque eu tincidunt. Nec feugiat in fermentum posuere. Condimentum lacinia quis vel eros donec ac odio tempor. - -At varius vel pharetra vel. Neque convallis a cras semper auctor. Pretium aenean pharetra magna ac placerat vestibulum. Donec ac odio tempor orci dapibus ultrices in iaculis. Velit laoreet id donec ultrices tincidunt arcu non. Non curabitur gravida arcu ac. Neque vitae tempus quam pellentesque nec nam. Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique. Amet porttitor eget dolor morbi non arcu risus quis. Venenatis cras sed felis eget. - -Sapien eget mi proin sed. Convallis convallis tellus id interdum velit laoreet. At ultrices mi tempus imperdiet nulla. Amet luctus venenatis lectus magna. Nunc sed velit dignissim sodales ut. Commodo viverra maecenas accumsan lacus vel facilisis. Mauris pharetra et ultrices neque ornare aenean euismod elementum. Blandit turpis cursus in hac habitasse platea dictumst quisque. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. Aliquam vestibulum morbi blandit cursus risus at ultrices. Vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tellus rutrum tellus pellentesque eu tincidunt tortor aliquam. Mattis ullamcorper velit sed ullamcorper. Urna duis convallis convallis tellus id interdum velit laoreet. Id volutpat lacus laoreet non curabitur gravida arcu ac tortor. Odio aenean sed adipiscing diam donec adipiscing. - -Et pharetra pharetra massa massa ultricies mi quis. Fermentum et sollicitudin ac orci phasellus egestas tellus rutrum. Amet est placerat in egestas erat imperdiet sed euismod nisi. Nam libero justo laoreet sit. Non tellus orci ac auctor augue mauris augue. Et pharetra pharetra massa massa ultricies mi. Mauris cursus mattis molestie a iaculis at erat. Sem fringilla ut morbi tincidunt augue. Blandit massa enim nec dui nunc mattis enim. Pharetra convallis posuere morbi leo urna molestie. - -Curabitur vitae nunc sed velit dignissim sodales ut eu. Nascetur ridiculus mus mauris vitae ultricies leo integer malesuada nunc. Quam vulputate dignissim suspendisse in est. Habitasse platea dictumst quisque sagittis purus sit amet volutpat. Tempor commodo ullamcorper a lacus vestibulum sed arcu non. Dictum fusce ut placerat orci nulla pellentesque dignissim enim sit. Velit scelerisque in dictum non consectetur a erat nam. Vitae turpis massa sed elementum tempus egestas sed sed. Et tortor at risus viverra adipiscing at. Vitae aliquet nec ullamcorper sit amet. Facilisis leo vel fringilla est ullamcorper eget nulla facilisi. Elit sed vulputate mi sit amet mauris commodo quis. Diam phasellus vestibulum lorem sed risus ultricies tristique. Pellentesque pulvinar pellentesque habitant morbi tristique senectus et. Egestas pretium aenean pharetra magna. Morbi tristique senectus et netus et malesuada. Varius morbi enim nunc faucibus a pellentesque sit amet. Lorem ipsum dolor sit amet consectetur adipiscing elit. - -Donec ac odio tempor orci dapibus ultrices in. Natoque penatibus et magnis dis parturient montes nascetur ridiculus mus. Sollicitudin ac orci phasellus egestas tellus rutrum tellus. Libero justo laoreet sit amet cursus. Enim facilisis gravida neque convallis a cras semper auctor. Convallis tellus id interdum velit laoreet id donec ultrices. Duis ut diam quam nulla porttitor massa id neque aliquam. Nullam vehicula ipsum a arcu cursus vitae congue. Sed egestas egestas fringilla phasellus. Id volutpat lacus laoreet non curabitur gravida arcu. Ullamcorper sit amet risus nullam. Eget aliquet nibh praesent tristique magna sit amet purus gravida. Nisl tincidunt eget nullam non nisi. Nunc consequat interdum varius sit amet mattis. Pretium quam vulputate dignissim suspendisse in est ante in. - -Id leo in vitae turpis massa sed elementum. Urna cursus eget nunc scelerisque viverra mauris in. Metus vulputate eu scelerisque felis. Convallis tellus id interdum velit laoreet id donec ultrices. A cras semper auctor neque vitae tempus quam pellentesque nec. Sit amet massa vitae tortor condimentum lacinia quis vel eros. Netus et malesuada fames ac turpis. Id cursus metus aliquam eleifend mi in. Iaculis eu non diam phasellus vestibulum lorem sed risus. Sed augue lacus viverra vitae congue eu consequat ac felis. Sapien eget mi proin sed libero enim sed. Turpis tincidunt id aliquet risus feugiat in. Nulla aliquet porttitor lacus luctus accumsan tortor posuere ac. diff --git a/src/data/contacts.json b/src/data/contacts.json index d8ff46c..c47ffa3 100644 --- a/src/data/contacts.json +++ b/src/data/contacts.json @@ -14,13 +14,6 @@ "mouseover": "GitHub" }, { - "platform": "PayPal", - "icon": "paypal", - "username": "alee14498", - "url": "https://paypal.me/alee14498", - "mouseover": "PayPal" - }, - { "platform": "YouTube", "icon": "youtube", "username": "AndrewLeeCAN", diff --git a/src/data/projects.json b/src/data/projects.json index 5b5b0a7..2eb4d41 100644 --- a/src/data/projects.json +++ b/src/data/projects.json @@ -2,6 +2,7 @@ { "name": "AleeBot", "description": "An all-in-one bot that's made from the Discord.JS API!", + "featured": true, "links": [ { "name": "Source Code", @@ -13,6 +14,7 @@ { "name": "DLAP", "description": "A Discord bot that lets you play local audio tracks in your server.", + "featured": true, "links": [ { "name": "Source Code", @@ -51,6 +53,7 @@ { "name": "YouTube TV Client", "description": "A electron app that goes to youtube.com/tv using a Samsung Smart TV as the user agent.", + "featured": true, "links": [ { "name": "Download", diff --git a/src/layouts/BlogPost.astro b/src/layouts/BlogPost.astro index 628215f..70562c6 100644 --- a/src/layouts/BlogPost.astro +++ b/src/layouts/BlogPost.astro @@ -1,6 +1,16 @@ --- import Layout from './Default.astro'; +import BlogComments from '../components/BlogComments.jsx'; +import "../styles/cards.css"; const { title, description, pubDate, tags } = Astro.props; +const { slug } = Astro.params; + +let disableComments; + +const response = await fetch(`${import.meta.env.PUBLIC_API_URL}/comments/${slug}/disabled`); +const data = await response.json(); +disableComments = data.postDisabled; + --- <Layout title=`${title} - Andrew Lee` description={description}> @@ -17,6 +27,12 @@ const { title, description, pubDate, tags } = Astro.props; <main> <slot /> </main> + { + disableComments ? null : + <h1>Comments</h1> + <BlogComments client:visible slug={slug} /> + } + </div> </Layout> diff --git a/src/layouts/Default.astro b/src/layouts/Default.astro index 9c0d1c7..775c94c 100644 --- a/src/layouts/Default.astro +++ b/src/layouts/Default.astro @@ -43,7 +43,7 @@ const date = new Date(); <div transition:name="main" transition:animate="fade"> <slot /> <footer> - <p>Made with {Astro.generator} and Hosted on Vercel</p> + <p class="small">Made with {Astro.generator} and Hosted on Vercel</p> <p>Copyright © {date.getFullYear()} Andrew Lee. <a href="https://github.com/Alee14/personal-website" target="_blank">View source code.</a></p> </footer> </div> @@ -74,10 +74,19 @@ const date = new Date(); } footer { + margin: 2em; text-align: center; font-size: 1.3em; } + footer p { + margin: 0.6em; + } + + .small { + font-size: 0.9em; + } + h1, h2, h3, h4, h5, h6 { font-weight: 500; } diff --git a/src/pages/blog/[...slug].astro b/src/pages/blog/[...slug].astro index 646dabb..5f1b34a 100644 --- a/src/pages/blog/[...slug].astro +++ b/src/pages/blog/[...slug].astro @@ -27,7 +27,7 @@ const { Content } = await entry.render(); <style is:global> img { - width: 20%; + width: 45%; } @media (max-width: 768px) { diff --git a/src/pages/guidelines.md b/src/pages/guidelines.md index e569203..5a257a6 100644 --- a/src/pages/guidelines.md +++ b/src/pages/guidelines.md @@ -38,7 +38,7 @@ In order to comply with the Children's Online Privacy Protection Act (COPPA), yo These rules applies when using our Minecraft servers -- Usage of hacked clients (including - but not limited to - Wurst, Wolfram, Sigma, etc.) is strictly prohibited.* +- Usage of hacked clients (including - but not limited to - Future, Impact, Meteor, etc.) is strictly prohibited.* - Griefing is not allowed.* - Unwarranted combating is not allowed unless both parties have given permission.* - Our general guidelines applies here as well. diff --git a/src/pages/index.astro b/src/pages/index.astro index edae2ab..936ce8e 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -3,40 +3,46 @@ import Layout from '../layouts/Default.astro'; import { Icon } from 'astro-icon/components' import { Image } from 'astro:assets'; import { getCollection } from "astro:content"; +import projects from "../data/projects.json"; import Profile from '../images/Alee.png'; import '../styles/index.css'; +import '../styles/Page.css'; +import "../styles/cards.css"; import { formatDate } from "../util"; const allBlogPosts = (await getCollection('blog')).sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()).slice(0, 10); +const featuredProjects = projects.filter(project => project.featured); --- <Layout title="Andrew Lee" description="Andrew Lee Website"> - <main class="home"> - <div class="headline"> - <Image src={Profile} class="avatar" alt="Andrew Lee" height={200} width={200} /> - <h1 id="title">Hey, I'm Andrew Lee!</h1> - <div class="social"> - <a href="https://linkedin.alee14.me" target="_blank" aria-label="LinkedIn"> - <Icon name="fa6-brands:linkedin" /> - </a> - <a href="https://github.alee14.me" target="_blank" aria-label="GitHub"> - <Icon name="fa6-brands:github" /> - </a> - <a href="https://youtube.alee14.me" target="_blank" aria-label="YouTube"> - <Icon name="fa6-brands:youtube" /> - </a> - <a href="https://instagram.alee14.me" target="_blank" aria-label="Instagram"> - <Icon name="fa6-brands:instagram" /> - </a> - </div> - <div class="description"> - <h2>19 Years Old</h2> - <h2>Student at Pearson Electrotechnology Centre</h2> - <h2>Living in Montreal, Quebec, Canada</h2> - </div> - <div> - <h3><a href="mailto:andrew@alee14.me" class="email-contact">andrew@alee14.me</a></h3> + <main> + <div class="home"> + <div class="headline"> + <Image src={Profile} class="avatar" loading="eager" alt="Andrew Lee" height={200} width={200} /> + <h1 id="title">Hey, I'm Andrew Lee!</h1> + <div class="social"> + <a href="https://linkedin.alee14.me" target="_blank" aria-label="LinkedIn"> + <Icon name="fa6-brands:linkedin" /> + </a> + <a href="https://github.alee14.me" target="_blank" aria-label="GitHub"> + <Icon name="fa6-brands:github" /> + </a> + <a href="https://youtube.alee14.me" target="_blank" aria-label="YouTube"> + <Icon name="fa6-brands:youtube" /> + </a> + <a href="https://instagram.alee14.me" target="_blank" aria-label="Instagram"> + <Icon name="fa6-brands:instagram" /> + </a> + </div> + <div class="description"> + <h2>19 Years Old</h2> + <h2>Student at Pearson Electrotechnology Centre</h2> + <h2>Living in Montreal, Quebec, Canada</h2> + </div> + <div> + <h3><a href="mailto:andrew@alee14.me" class="email-contact">andrew@alee14.me</a></h3> + </div> </div> </div> <div class="information"> @@ -44,7 +50,7 @@ const allBlogPosts = (await getCollection('blog')).sort((a, b) => b.data.pubDate <h1>About me</h1> <!--<a href="/history" class="link">History</a>--> <p>Hey, I am Andrew Lee, a person who has a passion with computers. - I am currently a student at a vocational school in Pearson Electrotechnology Centre. + I am currently a student at a trade school to get my DEP for IT. For years, I have been into programming, tinkering with virtual machines, and messing with Linux.</p> </div> <div class="box"> @@ -70,7 +76,7 @@ const allBlogPosts = (await getCollection('blog')).sort((a, b) => b.data.pubDate <li>Model: DS918+</li> <li>CPU: Intel Celeron J3455</li> <li>RAM: 8 GB</li> - <li>Storage: 15 TB (3x5TB, 1x4TB)</li> + <li>Storage: 15 TB (3x6TB, 1x4TB)</li> </ul> <h2>Vultr (Web Server)</h2> <ul> @@ -98,5 +104,36 @@ const allBlogPosts = (await getCollection('blog')).sort((a, b) => b.data.pubDate ))} </div> </div> + <div class="container"> + <div class="featured-projects"> + <h1>Featured Projects</h1> + <div class="grid"> + { + featuredProjects.map((project) => { + return ( + <article class="card"> + <h1>{project.name}</h1> + <p>{project.description}</p> + <div class="row"> + {project.links.map((link) => { + return ( + <a href={link.url} target={link.external ? "_blank" : "_self"}>{link.name}</a> + ) + })} + </div> + </article> + ) + }) + } + </div> + </div> + </div> </main> </Layout> +<style> + .row { + display: flex; + flex-direction: row; + gap: 1em; + } +</style> diff --git a/src/pages/projects.astro b/src/pages/projects.astro index 9b2b8a2..ee574c6 100644 --- a/src/pages/projects.astro +++ b/src/pages/projects.astro @@ -2,7 +2,7 @@ import Page from "../layouts/Page.astro"; import projects from "../data/projects.json"; import "../styles/cards.css"; -import GitHubProjects from "../components/GitHubProjects"; +import GitHubProjects from "../components/GitHubProjects.svelte"; --- <Page title="Projects" description="Things that I have been working on in the past, and present"> diff --git a/src/services/GuestbookService.js b/src/services/GuestbookService.js deleted file mode 100644 index f821c82..0000000 --- a/src/services/GuestbookService.js +++ /dev/null @@ -1,20 +0,0 @@ -import { supabase } from "./supabase"; - -export async function createMessage(data) { - try { - const { data: insertedData, error } = await supabase - .from('guestbook') - .insert([data]) - .select(); - - if (error) { - console.error(error); - throw error; - } - - return insertedData; - } catch (error) { - console.error(error); - throw error; - } -} diff --git a/src/services/supabase.ts b/src/services/supabase.ts deleted file mode 100644 index a2a9df1..0000000 --- a/src/services/supabase.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createClient } from "@supabase/supabase-js"; - -export const supabase = createClient( - import.meta.env.PUBLIC_SUPABASE_URL, - import.meta.env.PUBLIC_SUPABASE_ANON_KEY, -); diff --git a/src/styles/GuestbookForm.css b/src/styles/Form.css index 2dd3bbf..ba65d14 100644 --- a/src/styles/GuestbookForm.css +++ b/src/styles/Form.css @@ -18,6 +18,11 @@ resize: none; } +.card form textarea[name="comment"] { + height: 50px; + resize: none; +} + .card form input:disabled, .card form textarea:disabled, diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..fd26c34 --- /dev/null +++ b/vercel.json @@ -0,0 +1,12 @@ +{ + "rewrites": [ + { + "source": "/js/script.js", + "destination": "https://plausible.io/js/script.js" + }, + { + "source": "/api/event", + "destination": "https://plausible.io/api/event" + } + ] +} |
