UX improvments, fix search, display links, improve layout, semantic search results, global site theme switching, and more

pull/215/head
Alicia Sykes 1 month ago
parent daa641eb2b
commit 826dde6c84

@ -8,7 +8,7 @@
main {
margin: 2rem auto;
padding: 1rem;
width: 1000px;
width: 1200px;
max-width: calc(100% - 5rem);
border: 2px solid var(--box-outline);
box-shadow: 6px 6px 0 var(--box-outline);

@ -1,5 +1,6 @@
---
import FontAwesome from "@components/form/FontAwesome.svelte"
import ThemeSwitcher from "@components/form/ThemeSwitcher.svelte"
---
@ -13,6 +14,9 @@ import FontAwesome from "@components/form/FontAwesome.svelte"
<a href="/search">Search</a>
<a href="/about">About</a>
<a href="https://github.com/lissy93/awesome-privacy">GitHub</a>
<div class="theme-switcher">
<ThemeSwitcher client:load />
</div>
</nav>
</div>
@ -62,7 +66,6 @@ import FontAwesome from "@components/form/FontAwesome.svelte"
font-family: "Lekton", sans-serif;
font-weight: bold;
color: var(--foreground);
border-left: 2px solid var(--accent-3);
transition: background 0.3s, transform 0.3s, box-shadow 0.3s;
transition-timing-function: ease-in-out;
&:hover {
@ -85,6 +88,10 @@ import FontAwesome from "@components/form/FontAwesome.svelte"
}
}
}
.theme-switcher {
transform: scale(0.7);
margin: 0.2rem auto;
}
}
.homepage, nav {
@media(max-width: 768px) {

@ -20,6 +20,18 @@ const getIconName = (clasification: string) => {
return '';
}
// If two or more objects in privacyPolicy.points have the same title, remove the duplicates
const removeDuplicates = () => {
const seen = new Set();
return priv.points = priv.points.filter((point) => {
if (seen.has(point.title)) {
return false;
}
seen.add(point.title);
return true;
});
}
---
{priv && (
@ -29,7 +41,7 @@ const getIconName = (clasification: string) => {
<div class="left">
<h4>Privacy Policy Summary</h4>
<ul class="no-point">
{priv.points.map((point) => (
{removeDuplicates().map((point) => (
<li class={`clasification ${point.case.classification}`}>
<FontAwesome iconName={getIconName(point.case.classification)} />
{point.title}

@ -11,35 +11,35 @@ interface Props {
const { redditData } = Astro.props;
---
<div class="reddit-info-wrapper">
<h3>Reddit</h3>
<p class="website-title">
<img src={redditData.info.icon} width="16" />
{redditData.info.title || redditData.info.name}
</p>
<p class="website-description">{redditData.info.description}</p>
{redditData.info.banner && (<img class="banner" width="300" src={redditData.info.banner} alt="Banner" />)}
<ul class="list-table">
{redditData.info.dateCreated && (
<li>
<span class="lbl">Created at</span>
<span class="val">{timestampToDate(redditData.info.dateCreated * 1000)}</span>
</li>
)}
<li>
<span class="lbl">Members</span>
<span class="val">{redditData.info.subscribers}</span>
</li>
<li>
<span class="lbl">Join</span>
<span class="val"><a href={`https://reddit.com/${redditData.info.name}`}>{redditData.info.name}</a></span>
</li>
</ul>
<div class="left">
<h3>Reddit</h3>
<p class="website-title">
<img src={redditData.info.icon} width="16" />
{redditData.info.title || redditData.info.name}
</p>
<p class="website-description">{redditData.info.description}</p>
{redditData.info.banner && (<img class="banner" width="300" src={redditData.info.banner} alt="Banner" />)}
<ul class="list-table">
{redditData.info.dateCreated && (
<li>
<span class="lbl">Created at</span>
<span class="val">{timestampToDate(redditData.info.dateCreated * 1000)}</span>
</li>
)}
<li>
<span class="lbl">Members</span>
<span class="val">{redditData.info.subscribers}</span>
</li>
<li>
<span class="lbl">Join</span>
<span class="val"><a href={`https://reddit.com/${redditData.info.name}`}>{redditData.info.name}</a></span>
</li>
</ul>
</div>
<div class="right">
<h4>Posts</h4>
<ul class="posts">
{redditData.posts.map((post) => (
@ -49,14 +49,22 @@ const { redditData } = Astro.props;
</li>
))}
</ul>
</div>
</div>
<style lang="scss">
.reddit-info-wrapper {
display: flex;
flex-direction: column;
max-width: 500px;
flex-wrap: wrap;
justify-content: space-between;
gap: 1rem;
.left, .right {
width: calc(50% - 1rem);
@media screen and (max-width: 768px){
width: 100%;
}
}
}
.banner {

@ -168,7 +168,7 @@
transform: translateY(-0.5rem);
max-height: 500px;
overflow-y: scroll;
background: var(--background-form);
li.result-row {
padding: 0.5rem 1rem;
margin: 0.5rem 0;

@ -9,6 +9,11 @@ interface Props {
keywords?: string; // Overide keywords tag
hideNav?: boolean; // Don't show the navbar (just homepage)
author?: string; // Author of the content
customSchemaJson?: any; // Custom schema item
breadcrumbs?: Array<{
name: string;
item: string;
}>
}
const {
@ -16,9 +21,41 @@ const {
description='Your guide to finding privacy-respecting alternatives to popular software and services.',
keywords='security, privacy, awesome privacy, data collection, free software, open source, privacy tools, privacy respecting software',
hideNav=false,
author='Alicia Sykes'
author='Alicia Sykes',
breadcrumbs,
customSchemaJson,
} = Astro.props;
const makeBreadcrumbs = () => {
if (!breadcrumbs) return null;
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": breadcrumbs.map((breadcrumb, index) => ({
"@type": "ListItem",
"position": index + 1,
"name": breadcrumb.name,
"item": `https://awesome-privacy.xyz/${breadcrumb.item}`
}))
}
}
const makeSearchLd = () => {
return {
"@context": "https://schema.org",
"@type": "WebSite",
"url": "https://awesome-privacy.xyz/",
"potentialAction": [{
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://awesome-privacy.xyz/search?q={search_term_string}"
},
"query-input": "required name=search_term_string"
}]
}
};
---
<!doctype html>
@ -36,8 +73,8 @@ const {
<!-- Page info, viewport, Astro credit -->
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<meta name="robots" content="index, follow">
<!-- Icons and colors -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
@ -45,16 +82,20 @@ const {
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<!-- Social media meta tags (Open Graphh + Twitter) -->
<meta property="og:site_name" content="Awesome Privacy">
<meta property="og:type" content="website">
<meta property="og:url" content="https://awesome-privacy.xyz">
<meta property="og:title" content={title}>
<meta property="og:description" content={description}>
<meta property="og:image" content="/banner.png">
<meta property="og:image" content="https://awesome-privacy.xyz/banner.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" content="https://awesome-privacy.xyz">
<meta name="twitter:title" content={title}>
<meta name="twitter:description" content={description}>
<meta name="twitter:image" content="/banner.png">
<meta name="twitter:image" content="https://awesome-privacy.xyz/banner.png">
<meta name="twitter:site" content="@Lissy_Sykes">
<meta name="twitter:creator" content="@Lissy_Sykes">
<!-- Non-tracking hit counter -->
<script defer is:inline
@ -62,6 +103,15 @@ const {
data-domain="awesome-privacy.xyz"
src="https://no-track.as93.net/js/script.js">
</script>
<!-- Schema.org markup for Google -->
{breadcrumbs && (
<script type="application/ld+json" set:html={JSON.stringify(makeBreadcrumbs())} />
)}
{customSchemaJson && (
<script type="application/ld+json" set:html={JSON.stringify(customSchemaJson)} />
)}
<script type="application/ld+json" set:html={JSON.stringify(makeSearchLd)} />
</head>
<body>
{!hideNav && <NavBar /> }

@ -47,6 +47,7 @@ const {
androidApp,
icon,
followWith,
links,
securityAudited,
openSource,
acceptsCrypto,
@ -84,6 +85,15 @@ const makePageTitle = () => {
return `${name} | ${parentSection.name} | ${categoryName} | Awesome Privacy`;
};
const makeBreadcrumbs = () => {
return [
{ name: 'Home', item: '/' },
{ name: categoryName, item: slugify(categoryName) },
{ name: parentSection.name, item: `${slugify(categoryName)}/${slugify(parentSection.name)}` },
{ name: name, item: `${slugify(categoryName)}/${slugify(parentSection.name)}/${slugify(name)}` },
];
};
/**
* Make a string page intro, for the description tag
*/
@ -171,7 +181,12 @@ const getApiEndpoint = () => {
---
<Layout title={makePageTitle()} keywords={makeKeyWordTag()} description={makeDescriptionTag()} >
<Layout
title={makePageTitle()}
keywords={makeKeyWordTag()}
description={makeDescriptionTag()}
breadcrumbs={makeBreadcrumbs()}
>
<main>
<section>
<div class="service-head">
@ -259,6 +274,14 @@ const getApiEndpoint = () => {
<a href={`https://web-check.xyz/results/${formatLink(url)}`}>{`web-check.xyz/results/${formatLink(url).split('/')[0]}`}</a>
</li>
)}
{ links && links.length > 0 && (
links.map((link) => (
<li>
<b>{link.title}</b>
<a href={link.url}>{link.url}</a>
</li>
))
)}
</ul>
</div>
</div>
@ -420,7 +443,7 @@ const getApiEndpoint = () => {
main {
margin: 2rem auto;
padding: 1rem;
width: 1000px;
width: 1200px;
max-width: calc(100% - 5rem);
position: relative;
@media(max-width: 768px) {
@ -539,7 +562,7 @@ h4 {
b {
font-size: 1rem;
font-weight: 400;
min-width: 85px;
min-width: 100px;
display: inline-block;
@media(max-width: 768px) {
width: auto;

@ -54,6 +54,14 @@ const makePageTitle = () => {
return `${title} | Awesome Privacy`;
};
const makeBreadcrumbs = () => {
return [
{ name: 'Home', item: '/' },
{ name: categoryName, item: slugify(categoryName) },
{ name: title, item: `${slugify(categoryName)}/${slugify(title)}` },
];
};
/**
* Make a string page intro, for the description tag
*/
@ -72,6 +80,27 @@ const makeDescriptionTag = () => {
return description;
};
const makeCarasolData = () => {
return {
"@context": "https://schema.org",
"@type": "ItemList",
"itemListElement": [
services.map((service, index) => ({
"@type": "ListItem",
"position": index + 1,
"url": `https://awesomeprivacy.com/${slugify(categoryName)}/${slugify(title)}/${slugify(service.name)}`,
"item": {
"@type": "Service",
"name": service.name,
"description": service.description,
"image": service.icon,
"url": `https://awesomeprivacy.com/${slugify(categoryName)}/${slugify(title)}/${slugify(service.name)}`
}
})),
]
}
};
export async function getStaticPaths() {
const pages = await fetchData().then((data) => {
const results: Array<Props> = [];
@ -110,9 +139,23 @@ const { previous, next } = makePaginationLinks();
---
<Layout title={makePageTitle()} keywords={makeKeyWordTag()} description={makeDescriptionTag()} >
<Layout
title={makePageTitle()}
keywords={makeKeyWordTag()}
description={makeDescriptionTag()}
breadcrumbs={makeBreadcrumbs()}
customSchemaJson={makeCarasolData()}
>
<Main>
<div class="breadcrumbs">
<span>
<a href="/">Awesome Privacy</a>
➔ <a href={`/${slugify(categoryName)}`}>{categoryName}</a>
➔ <a href={`/${slugify(categoryName)}/${slugify(title)}`}>{title}</a>
</span>
</div>
<h2>{title}</h2>
{intro && (
@ -311,5 +354,23 @@ h2 {
opacity: 0.7;
}
.breadcrumbs {
opacity: 0.5;
font-size: 0.8rem;
position: absolute;
right: 1rem;
top: 1rem;
a {
color: var(--foreground);
transition: all 0.15s ease-in-out;
&:hover {
color: var(--accent);
}
}
@media(max-width: 768px) {
display: none;
}
}
</style>

@ -37,6 +37,33 @@ export async function getStaticPaths() {
});
}
const makeCarasolData = () => {
return {
"@context": "https://schema.org",
"@type": "ItemList",
"itemListElement": [
sections.map((section, index) => ({
"@type": "ListItem",
"position": index + 1,
"url": `https://awesomeprivacy.com/${slugify(title)}/${slugify(section.name)}`,
"item": {
"@type": "Service",
"name": section.name,
"url": `https://awesomeprivacy.com/${slugify(title)}/${slugify(section.name)}`,
}
})),
]
}
};
const makeBreadcrumbs = () => {
return [
{ name: 'Home', item: '/' },
{ name: title, item: slugify(title) },
];
};
const makePaginationLinks = () => {
const index = categories.findIndex(category => category.name === title);
const previousCategory = index > 0 ? categories[index - 1].name : null;
@ -48,8 +75,14 @@ const { previous, next } = makePaginationLinks();
---
<Layout title={`${title} | Awesome Privacy`}>
<Layout title={`${title} | Awesome Privacy`} breadcrumbs={makeBreadcrumbs()} customSchemaJson={makeCarasolData()}>
<Main>
<div class="breadcrumbs">
<span>
<a href="/">Awesome Privacy</a>
➔ <a href={`/${slugify(title)}`}>{title}</a>
</span>
</div>
<SectionList title={title} sections={sections} bigTitle={true} />
<a href={`/all#${slugify(title)}`}>Browse All</a>
<div class="pagination-navigation">
@ -100,4 +133,25 @@ const { previous, next } = makePaginationLinks();
opacity: 0.5;
}
}
.breadcrumbs {
opacity: 0.5;
font-size: 0.8rem;
position: absolute;
right: 1rem;
top: 1rem;
a {
color: var(--foreground);
transition: all 0.15s ease-in-out;
&:hover {
color: var(--accent);
}
}
@media(max-width: 768px) {
display: none;
}
}
</style>

@ -402,10 +402,6 @@ h3 {
}
}
:global(ul li:hover strong) {
transition: color 0.2s ease-in-out;
color: var(--accent-3);
}
:global(h3) {
font-size: 1.6rem;
margin-bottom: 0;

@ -36,78 +36,66 @@ const categories: Category[] = (await fetchData())?.categories;
</Layout>
<script>
// Time for some good ol' fashioned vanilla JS...
// I reccomend you not to look at this code for too long, it's not pretty.
const filterInput = document.querySelector('input');
const categories = document.querySelectorAll<HTMLElement>('.category');
const pressEnterMsg = document.getElementById('press-enter-msg') as HTMLElement | null;
const noResults = document.querySelector('.no-results') as HTMLElement | null;
if (!pressEnterMsg || !noResults) {
throw new Error('No pressEnterMsg or noResults');
};
filterInput?.addEventListener('input', (e) => {
let resultsCount = 0;
const filter = (e.target as HTMLInputElement).value.toLowerCase();
if (filter.length > 0) {
pressEnterMsg.style.visibility = 'visible';
} else {
pressEnterMsg.style.visibility = 'hidden';
}
categories.forEach((category) => {
const titleElement = category.querySelector('.category-title');
const title = titleElement ? titleElement.textContent?.toLowerCase() : '';
const sections = category.querySelectorAll<HTMLElement>('.section');
let count = 0;
sections.forEach((section) => {
const sectionTitle = section.textContent?.toLowerCase();
if (sectionTitle?.includes(filter)) {
section.style.display = 'block';
count++;
resultsCount++;
} else {
section.style.display = 'none';
}
});
if (title && title.includes(filter)) {
category.style.display = 'inline-flex';
resultsCount++;
} else if (count === 0) {
category.style.display = 'none';
// Time for some good ol' fashioned vanilla JS...
// I reccomend you not to look at this code for too long, it's not pretty.
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('searchInput');
const categories = document.querySelectorAll('.category');
const pressEnterMsg = document.getElementById('press-enter-msg');
const noResults = document.querySelector('.no-results');
if (!pressEnterMsg || !noResults || !searchInput) {
console.error('Essential element missing!');
return;
}
});
noResults.style.display = resultsCount === 0 ? 'block' : 'none';
});
if (typeof window !== 'undefined') {
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('searchInput') as HTMLInputElement | null;
if (searchInput === null) return;
searchInput.addEventListener('keydown', function(event) {
searchInput.addEventListener('input', (e) => {
let resultsCount = 0;
const filter = e.target.value.trim().toLowerCase();
pressEnterMsg.style.visibility = filter ? 'visible' : 'hidden';
categories.forEach((category) => {
const titleElement = category.querySelector('.category-title');
const title = titleElement ? titleElement.textContent?.toLowerCase() : '';
const sections = category.querySelectorAll('.section');
let sectionCount = 0;
sections.forEach((section) => {
const sectionTitle = section.textContent?.toLowerCase();
if (sectionTitle?.includes(filter)) {
section.style.display = 'block';
sectionCount++;
} else {
section.style.display = 'none';
}
});
category.style.display = title.includes(filter) || sectionCount > 0 ? 'inline-flex' : 'none';
resultsCount += title.includes(filter) ? 1 : sectionCount;
});
noResults.style.display = resultsCount === 0 ? 'block' : 'none';
});
searchInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
const searchTerm = encodeURIComponent(searchInput.value);
window.location.href = `/search/${searchTerm}`;
window.location.href = `/search/${encodeURIComponent(searchInput.value.trim())}`;
}
if (event.key === 'Escape') {
searchInput.value = '';
searchInput.blur();
pressEnterMsg.style.visibility = 'hidden';
categories.forEach((category) => {
category.style.display = 'inline-flex';
const sections = category.querySelectorAll<HTMLElement>('.section');
sections.forEach((section) => {
section.style.display = 'block';
});
});
// Reset display styles
categories.forEach(category => category.style.display = 'inline-flex');
document.querySelectorAll('.section').forEach(section => section.style.display = 'block');
}
});
});
}
</script>
<style lang="scss">
.head-wrap {

@ -47,6 +47,7 @@
--background: #feecff;
--bg-gradient-comp-1: #feecff;
--bg-gradient-comp-2: #e1e4fb;
--background-form: #fff;
}
}

@ -12,6 +12,10 @@ export interface Service {
url: string;
github?: string;
icon?: string;
links?: Array<{
title: string;
url: string;
}>;
followWith?: string;
securityAudited?: boolean;
openSource?: boolean;

@ -13,5 +13,5 @@ export const fetchData = async (): Promise<AwesomePrivacy> => {
}
export const slugify = (title: string) => {
return (title || '').toLowerCase().replace(/\s/g, '-');
};
return (title || '').toLowerCase().replace(/\s/g, '-').replace(/\+|&/g, 'and');
};

Loading…
Cancel
Save