Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export default defineConfig({
}),
pagefind(),
],
image: {
domains: ["i.ytimg.com"],
},
output: "static",
build: {
format: "file",
Expand Down
8 changes: 7 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"contactLabel": "Contact",
"aaLabel": "Apostolic Answers",
"ddLabel": "Deep Dive",
"wowLabel": "Word of Wisdom",
"wowLabel": "Words of Wisdom",
"shopLabel": "Shop",
"copyright": "© {year} Coptic Orthodox Answers. All Rights Reserved.",
"homePageTitle": "Becoming a Generation of Enlightened Christians",
Expand All @@ -27,6 +27,12 @@
"contactEmailPlaceholder": "your.email@example.com",
"contactMessagePlaceholder": "Tell us what's on your mind...",
"contactSubmitButton": "Send Message",
"searchResultsLabel": "Search results for",
"noResultsFound": "No results found.",
"perPageLabel": "Results per page",
"filterLabel": "Filter",
"filterAll": "All",
"searchingLabel": "Searching...",
"faithTitle1": "What is the Coptic Orthodox Church",
"faithBody1": "The Coptic Orthodox Church is the ancient Christian Church of Egypt. It is one-part of the worldwide Orthodox Church, established by the Apostles in the first century. The word 'Coptic' derives from the Greek word for 'Egyptian'. It is a Christian community which has its roots in Egypt, where over ten million members are found. Although the Coptic Orthodox Church may appear to be a recent presence in the West, it represents the earliest and original Apostolic Christianity. Far from being something new and alien, the Orthodox Faith is ancient and universal (catholic), and has a great deal of wealth to offer all those seeking a true and genuine relationship with God.",
"faithTitle2": "Our History",
Expand Down
8 changes: 7 additions & 1 deletion messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"contactLabel": "(FR) Contact",
"aaLabel": "(FR) Apostolic Answers",
"ddLabel": "(FR) Deep Dive",
"wowLabel": "(FR) Word of Wisdom",
"wowLabel": "(FR) Words of Wisdom",
"shopLabel": "(FR) Shop",
"copyright": "(FR) © {year} Coptic Orthodox Answers. All Rights Reserved.",
"homePageTitle": "(FR) Becoming a Generation of Enlightened Christians",
Expand All @@ -27,6 +27,12 @@
"contactEmailPlaceholder": "(FR) your.email@example.com",
"contactMessagePlaceholder": "(FR) Tell us what's on your mind...",
"contactSubmitButton": "(FR) Send Message",
"searchResultsLabel": "(FR) Search results for",
"noResultsFound": "(FR) No results found.",
"perPageLabel": "(FR) Results per page",
"filterLabel": "(FR) Filter",
"filterAll": "(FR) All",
"searchingLabel": "(FR) Searching...",
"faithTitle1": "(FR) What is the Coptic Orthodox Church",
"faithBody1": "(FR) The Coptic Orthodox Church is the ancient Christian Church of Egypt. It is one-part of the worldwide Orthodox Church, established by the Apostles in the first century. The word 'Coptic' derives from the Greek word for 'Egyptian'. It is a Christian community which has its roots in Egypt, where over ten million members are found. Although the Coptic Orthodox Church may appear to be a recent presence in the West, it represents the earliest and original Apostolic Christianity. Far from being something new and alien, the Orthodox Faith is ancient and universal (catholic), and has a great deal of wealth to offer all those seeking a true and genuine relationship with God.",
"faithTitle2": "(FR) Our History",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"start": "astro dev",
"dev": "astro dev",
"build": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide && astro build && pagefind --site dist",
"build": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide && astro build",
"preview": "astro preview",
"astro": "astro"
},
Expand Down
1 change: 1 addition & 0 deletions src/components/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const year = new Date().getFullYear();
---

<div
data-pagefind-ignore
class="flex flex-col min-h-100 bg-black justify-between px-4 sm:px-8 lg:px-21 pt-8 pb-4 sm:pt-12 lg:pt-16 gap-8 sm:gap-12"
>
<div class="flex flex-col gap-2 sm:gap-4">
Expand Down
2 changes: 1 addition & 1 deletion src/components/Head.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const { title } = Astro.props;
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,orientation=portrait"
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
/>
<title data-pagefind-meta="title">{title}</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
Expand Down
116 changes: 15 additions & 101 deletions src/components/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import * as m from "@paraglide/messages.js";
import SearchIcon from "./SearchIcon.astro";
---

<header>
<div class="absolute top-4 left-4 z-50">
<header data-pagefind-ignore>
<!-- <div class="absolute top-4 left-4 z-50">
<LanguageToggle />
</div>
</div> -->
<div class="bg-no-repeat bg-cover bg-center bg-[url(/assets/images/church.webp)]">
<div class="bg-black/85 min-h-96 flex flex-col justify-center items-center">
<a href="/">
Expand All @@ -25,19 +25,12 @@ import SearchIcon from "./SearchIcon.astro";
id="search"
type="text"
placeholder={m.searchPlaceholder()}
class="w-full px-10 sm:px-12 py-2.5 sm:py-3 rounded-lg bg-white text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-jonquil text-center text-sm sm:text-base [&:focus::placeholder]:opacity-0"
class="w-full px-10 sm:px-12 py-2.5 sm:py-3 rounded-lg bg-white text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-jonquil text-center text-sm sm:text-base [&:focus::placeholder]:opacity-0"
/>
<div class="absolute left-3 sm:left-4 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none">
<SearchIcon class="w-4 h-4 sm:w-5 sm:h-5" />
</div>
</div>
<div id="results" class="fixed inset-0 hidden z-100">
<div
class="absolute w-[calc(100%-2rem)] sm:w-4/5 md:w-1/2 left-1/2 -translate-x-1/2 mt-2 max-h-[60vh] overflow-y-auto bg-white rounded-lg shadow-xl p-3 sm:p-4"
>
<div class="space-y-4"></div>
</div>
</div>
</div>
</div>
</div>
Expand All @@ -47,100 +40,21 @@ import SearchIcon from "./SearchIcon.astro";
</header>

<script is:inline>
(function () {
(() => {
const searchBox = document.querySelector("#search");
const resultsContainer = document.querySelector("#results");
const resultsContent = resultsContainer?.querySelector(".space-y-4");
if (!searchBox || !resultsContainer || !resultsContent) return;

let debounceTimer = null;
let searchCounter = 0;

function closeResults() {
resultsContainer.classList.add("hidden");
document.body.classList.remove("overflow-hidden");
}

function openResults() {
resultsContainer.classList.remove("hidden");
document.body.classList.add("overflow-hidden");

const searchRect = searchBox.getBoundingClientRect();
const resultsDiv = resultsContainer.querySelector("div");
if (resultsDiv) {
resultsDiv.style.top = searchRect.bottom + 8 + "px";
}
}

async function performSearch(searchTerm) {
if (!searchTerm.trim()) {
closeResults();
return;
}

openResults();

if (searchBox.dataset.loaded !== "true") {
searchBox.dataset.loaded = "true";
window.pagefind = await import("/pagefind/pagefind.js");
}

const currentSearch = ++searchCounter;
const search = await window.pagefind.search(searchTerm);
if (!searchBox) return;

if (currentSearch !== searchCounter) return;
const navigateToSearch = (query) => {
const q = query.trim();
if (!q) return;
const prefix = window.location.pathname.startsWith("/fr") ? "/fr" : "";
window.location.href = prefix + "/search?q=" + encodeURIComponent(q);
};

let html = "";
for (const result of search.results) {
const data = await result.data();
if (currentSearch !== searchCounter) return;
html +=
'<a href="' + data.url + '" class="block p-4 hover:bg-gray-50 rounded-lg">' +
'<h3 class="text-lg font-semibold text-gray-900">' + escapeHtml(data.meta.title) + "</h3>" +
'<p class="text-gray-600 mt-1">' + (data.excerpt || "") + "</p>" +
"</a>";
}

if (currentSearch !== searchCounter) return;

if (html) {
resultsContent.innerHTML = html;
} else {
resultsContent.innerHTML =
'<p class="text-gray-500 text-center py-4">No results found.</p>';
}
}

function escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}

searchBox.addEventListener("input", function (e) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function () {
performSearch(e.target.value);
}, 250);
});

searchBox.addEventListener("keypress", function (e) {
searchBox.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
clearTimeout(debounceTimer);
performSearch(e.target.value);
}
});

resultsContainer.addEventListener("click", function (e) {
if (e.target === e.currentTarget) {
closeResults();
}
});

document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !resultsContainer.classList.contains("hidden")) {
closeResults();
searchBox.blur();
e.preventDefault();
navigateToSearch(e.target.value);
}
});
})();
Expand Down
13 changes: 7 additions & 6 deletions src/components/PodsGrid.astro
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
import { Image } from "astro:assets";
import type { PodcastItem } from "@utils/podcasts";

interface Props {
Expand All @@ -12,23 +13,23 @@ const { podcasts } = Astro.props;
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-14 sm:gap-8 lg:gap-8 px-4 pt-8 sm:px-8 lg:px-12 lg:pt-12 xl:px-19 xl:pt-19"
>
{
podcasts?.map((podcast) => (
podcasts?.map((podcast, i) => (
<a class="group block" href={`/podcasts/${podcast.slug}`}>
<div class="aspect-video overflow-hidden rounded-lg">
<img
<Image
src={podcast.thumbnail}
alt={podcast.title}
width={480}
height={270}
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
loading="lazy"
loading={i < 8 ? "eager" : "lazy"}
/>
</div>
<div class="flex flex-col pt-4">
<span class="group-hover:underline group-hover:underline-offset-4 font-barlow text-lg sm:text-xl text-pearl line-clamp-2">
{podcast.title}
</span>
<span class="font-barlow text-xs text-jonquil mt-2">
{podcast.publishedAt}
</span>
<span class="font-barlow text-xs text-jonquil mt-2">{podcast.publishedAt}</span>
</div>
</a>
))
Expand Down
22 changes: 14 additions & 8 deletions src/components/PostsGrid.astro
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
import { Image } from "astro:assets";
import { type InferEntrySchema, type RenderedContent } from "astro:content";
const { posts } = Astro.props;
type Post = {
Expand All @@ -15,15 +16,20 @@ type Post = {
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-14 sm:gap-8 lg:gap-8 px-4 pt-8 sm:px-8 lg:px-12 lg:pt-12 xl:px-19 xl:pt-19"
>
{
posts.map((post: Post) => (
posts.map((post: Post, i: number) => (
<a class="group block" href={`/${post.id}`}>
<div class="aspect-video overflow-hidden rounded-lg">
<img
src={post.data.imgSrc}
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
alt={post.data.title}
/>
</div>
{post.data.imgSrc && (
<div class="aspect-video overflow-hidden rounded-lg">
<Image
src={post.data.imgSrc}
alt={post.data.title}
width={480}
height={270}
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
loading={i < 8 ? "eager" : "lazy"}
/>
</div>
)}
<div class="flex flex-col pt-4">
<span class="group-hover:underline group-hover:underline-offset-4 font-barlow text-lg sm:text-xl text-pearl line-clamp-2">
{post.data.title}
Expand Down
22 changes: 14 additions & 8 deletions src/components/PostsGridWithTags.astro
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
import { Image } from "astro:assets";
import { type InferEntrySchema, type RenderedContent } from "astro:content";
const { posts } = Astro.props;
type Post = {
Expand Down Expand Up @@ -28,15 +29,20 @@ const getTagColor = (tag: string): string => {
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-14 sm:gap-8 lg:gap-8 px-4 pt-8 sm:px-8 lg:px-12 lg:pt-12 xl:px-19 xl:pt-19"
>
{
posts.map((post: Post) => (
posts.map((post: Post, i: number) => (
<a class="group block" href={`/${post.id}`}>
<div class="aspect-video overflow-hidden rounded-lg">
<img
src={post.data.imgSrc}
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
alt={post.data.title}
/>
</div>
{post.data.imgSrc && (
<div class="aspect-video overflow-hidden rounded-lg">
<Image
src={post.data.imgSrc}
alt={post.data.title}
width={480}
height={270}
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
loading={i < 8 ? "eager" : "lazy"}
/>
</div>
)}
<div class="flex flex-col pt-4">
<div class="flex flex-col sm:flex-row sm:justify-between gap-2 sm:gap-0">
<span class="group-hover:underline group-hover:underline-offset-4 font-barlow text-lg sm:text-xl text-pearl line-clamp-2">
Expand Down
35 changes: 35 additions & 0 deletions src/layouts/Catalog.astro
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,39 @@ const hasCustomGrid = Astro.slots.has("default");
{hasCustomGrid ? <slot /> : generateTags ? <PostsGridWithTags posts={posts} /> : <PostsGrid posts={posts} />}
<Pagination currentPage={page.currentPage} totalPages={page.lastPage} baseUrl={baseUrl} />
</div>

<button
id="back-to-top"
aria-label="Back to top"
class="fixed bottom-6 right-4 z-40 hidden sm:!hidden p-2 rounded-full bg-pearl/10 backdrop-blur-sm text-pearl hover:bg-madder transition-all duration-300 opacity-0 translate-y-4 cursor-pointer"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" />
</svg>
</button>
</Main>

<script>
const backToTop = document.getElementById("back-to-top");
if (backToTop) {
window.addEventListener("scroll", () => {
if (window.scrollY > 300) {
backToTop.classList.remove("hidden");
requestAnimationFrame(() => {
backToTop.classList.remove("opacity-0", "translate-y-4");
backToTop.classList.add("opacity-100", "translate-y-0");
});
} else {
backToTop.classList.add("opacity-0", "translate-y-4");
backToTop.classList.remove("opacity-100", "translate-y-0");
backToTop.addEventListener("transitionend", () => {
if (window.scrollY <= 300) backToTop.classList.add("hidden");
}, { once: true });
}
}, { passive: true });

backToTop.addEventListener("click", () => {
window.scrollTo({ top: 0, behavior: "smooth" });
});
}
</script>
10 changes: 7 additions & 3 deletions src/layouts/Post.astro
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
---
import { type CollectionEntry, render } from "astro:content";

const post = Astro.props;
interface Props extends CollectionEntry<"posts"> {
category?: string;
}

type Props = CollectionEntry<"posts">;
const { Content } = await render(post);
const { category, ...post } = Astro.props;
const { Content } = await render(post as CollectionEntry<"posts">);
---

<div class="bg-gray w-full">
<div
class="flex flex-col justify-center text-justify px-4 sm:px-8 md:px-16 lg:px-42 py-8 sm:py-12 md:py-16 lg:py-21"
data-pagefind-body
>
{post.data.imgSrc && <meta data-pagefind-meta={`image:${post.data.imgSrc}`} />}
{category && <meta data-pagefind-filter={`category:${category}`} />}
<h4
data-pagefind-weight="9"
class="font-noto text-2xl sm:text-3xl md:text-4xl text-pearl text-center pb-4 sm:pb-8 md:pb-12 lg:pb-21 max-w-full sm:max-w-[90%] md:max-w-[80%] lg:max-w-[66%] place-self-center"
Expand Down
Loading