Or, just
@@ -48,28 +49,33 @@ const description = 'Privacy is a fundamental human right; '
- Awesome Privacy is a collection of privacy-respecting services and tools.
- The aim is to help you escape big tech, and choose software that respects your privacy.
+ Awesome Privacy is a collection of privacy-respecting services and
+ tools. The aim is to help you escape big tech, and choose software that
+ respects your privacy.
- Why? Because privacy is a fundamental human right; without it, we're just open books
- in a world where everyone's watching. Let's take control back.
+ Why? Because privacy is a fundamental human right; without it, we're
+ just open books in a world where everyone's watching. Let's take control
+ back.
- Noticed something that should be added / removed / amended?
- We're a community-driven resource, so welcome contributions of any nature.
- All content and code is open source .
+ Noticed something that should be added / removed / amended? We're a
+ community-driven resource, so welcome contributions of any nature. All
+ content and code is open source .
- If you've found Awesome Privacy useful, help us out by sharing it with others,
- contributing, or consider sponsoring me on GitHub.
+ If you've found Awesome Privacy useful, help us out by sharing it with
+ others, contributing, or consider sponsoring me on GitHub.
Want to learn more?
-
@@ -81,8 +87,8 @@ const description = 'Privacy is a fundamental human right; '
max-width: calc(100% - 5rem);
font-size: 20px;
line-height: 1.6;
- @media(max-width: 768px) {
- padding: 0;
+ @media (max-width: 768px) {
+ padding: 0;
}
.view-all {
text-align: center;
@@ -97,41 +103,43 @@ const description = 'Privacy is a fundamental human right; '
h2 {
font-size: 3rem;
color: var(--accent-3);
- font-family: "Lekton", sans-serif;
+ font-family: 'Lekton', sans-serif;
text-align: center;
margin: 3rem 0 1rem 0;
a {
text-decoration: none;
color: var(--accent-3);
- font-family: "Lekton", sans-serif;
+ font-family: 'Lekton', sans-serif;
position: relative;
- &:after {
+ &:after {
background: none repeat scroll 0 0 transparent;
bottom: 0;
- content: "";
+ content: '';
display: block;
height: 3px;
left: 50%;
position: absolute;
background: var(--accent);
- transition: width 0.3s ease 0s, left 0.3s ease 0s;
+ transition:
+ width 0.3s ease 0s,
+ left 0.3s ease 0s;
width: 0;
}
- &:hover:after {
- width: 100%;
- left: 0;
+ &:hover:after {
+ width: 100%;
+ left: 0;
}
}
}
.about-summary {
background: var(--accent-fg);
- border: 2px solid var(--box-outline);
- border-radius: var(--curve-sm);
- box-shadow: 4px 4px 0 var(--box-outline);
- padding: 1rem;
- width: 85%;
- margin: 0 auto;
+ border: 2px solid var(--box-outline);
+ border-radius: var(--curve-sm);
+ box-shadow: 4px 4px 0 var(--box-outline);
+ padding: 1rem;
+ width: 85%;
+ margin: 0 auto;
}
.categories {
@@ -148,7 +156,7 @@ const description = 'Privacy is a fundamental human right; '
display: inline-flex;
flex-direction: column;
margin: 1rem;
- @media(max-width: 768px) {
+ @media (max-width: 768px) {
margin: 1rem 0;
width: 90%;
}
@@ -157,7 +165,7 @@ const description = 'Privacy is a fundamental human right; '
color: var(--foreground);
}
h3 {
- font-family: "Lekton", sans-serif;
+ font-family: 'Lekton', sans-serif;
font-weight: bold;
margin: 0;
font-size: 1.8rem;
@@ -176,6 +184,4 @@ const description = 'Privacy is a fundamental human right; '
}
}
}
-
-
diff --git a/web/src/pages/inventory/[...inventoryId].astro b/web/src/pages/inventory/[...inventoryId].astro
index 01d261c..70fe320 100644
--- a/web/src/pages/inventory/[...inventoryId].astro
+++ b/web/src/pages/inventory/[...inventoryId].astro
@@ -1,5 +1,4 @@
---
-
import Layout from '@layouts/Layout.astro';
import SavedServices from '@components/things/SavedServices.svelte';
import GetSharableLink from '@components/things/GetSharableLink.svelte';
@@ -9,7 +8,7 @@ import Button from '@components/form/Button.astro';
import EditableTitle from '@components/form/EditableTitle.svelte';
import type { Category } from '../../types/Service';
-const categories = (await fetchData())?.categories || [] as Category[];
+const categories = (await fetchData())?.categories || ([] as Category[]);
export const prerender = false;
@@ -17,85 +16,92 @@ const inventoryId = Astro.params.inventoryId || 'Inventory';
let cheekyLilError = '';
function makeTitle(input: string): string {
- return (input.includes('_') ? input : `mystry_${input}`)
- .split('_')[1]
- .replace(/-/g, ' ')
- .replace(/\b\w/g, (match) => match.toUpperCase());
+ return (input.includes('_') ? input : `mystry_${input}`)
+ .split('_')[1]
+ .replace(/-/g, ' ')
+ .replace(/\b\w/g, (match) => match.toUpperCase());
}
-const serviceList = await fetch(`https://awesome-privacy-share-api.as93.net/${inventoryId}`).then((res) => res.json()) || [];
+const serviceList =
+ (await fetch(
+ `https://awesome-privacy-share-api.as93.net/${inventoryId}`,
+ ).then((res) => res.json())) || [];
if (serviceList.error) {
- cheekyLilError = serviceList.error;
+ cheekyLilError = serviceList.error;
}
-
---
-
- {makeTitle(inventoryId)}
- {cheekyLilError && (
-
-
An error occoured
-
{cheekyLilError}
-
- We're sorry about that.
- Try going back home ,
- or raising a ticket on
- GitHub.
-
-
- )}
-
-
-
+
+ {makeTitle(inventoryId)}
+ {
+ cheekyLilError && (
+
+
An error occoured
+
{cheekyLilError}
+
+ We're sorry about that.
+
+ Try going back home , or{' '}
+
+ raising a ticket
+ {' '}
+ on GitHub.
+
+
+ )
+ }
+
+
+
diff --git a/web/src/pages/inventory/index.astro b/web/src/pages/inventory/index.astro
index 2bb8f9a..196d870 100644
--- a/web/src/pages/inventory/index.astro
+++ b/web/src/pages/inventory/index.astro
@@ -1,5 +1,4 @@
---
-
import Layout from '@layouts/Layout.astro';
import SavedServices from '@components/things/SavedServices.svelte';
import GetSharableLink from '@components/things/GetSharableLink.svelte';
@@ -9,53 +8,52 @@ import Button from '@components/form/Button.astro';
import EditableTitle from '@components/form/EditableTitle.svelte';
import type { Category } from '../../types/Service';
-const categories = (await fetchData())?.categories || [] as Category[];
-
+const categories = (await fetchData())?.categories || ([] as Category[]);
---
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/web/src/pages/search/[...searchTerm].astro b/web/src/pages/search/[...searchTerm].astro
index 193e40f..e741121 100644
--- a/web/src/pages/search/[...searchTerm].astro
+++ b/web/src/pages/search/[...searchTerm].astro
@@ -1,5 +1,5 @@
---
- import Fuse from 'fuse.js';
+import Fuse from 'fuse.js';
import Layout from '@layouts/Layout.astro';
import { fetchData, slugify } from '@utils/fetch-data';
@@ -22,171 +22,178 @@ fuse = new Fuse(items, searchOptions);
const searchTerm = Astro.params.searchTerm;
-const searchResults = fuse.search(searchTerm || '').map(result => result.item);
+const searchResults = fuse
+ .search(searchTerm || '')
+ .map((result) => result.item);
-const services = searchResults.filter(result => result.type === 'Service');
+const services = searchResults.filter((result) => result.type === 'Service');
const putResultsIntoGroups = () => {
- const grouped = services.reduce((acc, item) => {
- const { category: categoryName, sectionName, ...service } = item;
+ const grouped = services.reduce((acc, item) => {
+ const { category: categoryName, sectionName, ...service } = item;
- if (!acc[categoryName]) {
- acc[categoryName] = { categoryName, sections: {} };
- }
+ if (!acc[categoryName]) {
+ acc[categoryName] = { categoryName, sections: {} };
+ }
- if (!acc[categoryName].sections[sectionName]) {
- acc[categoryName].sections[sectionName] = { sectionName, items: [] };
- }
+ if (!acc[categoryName].sections[sectionName]) {
+ acc[categoryName].sections[sectionName] = { sectionName, items: [] };
+ }
- acc[categoryName].sections[sectionName].items.push(service);
+ acc[categoryName].sections[sectionName].items.push(service);
- return acc;
- }, {});
+ return acc;
+ }, {});
- // Convert the grouped object into the desired array structure.
- // And fuck it, let's use `any`
- return Object.values(grouped).map((category: any) => ({
- categoryName: category.categoryName,
- sections: Object.values(category.sections)
- }));
+ // Convert the grouped object into the desired array structure.
+ // And fuck it, let's use `any`
+ return Object.values(grouped).map((category: any) => ({
+ categoryName: category.categoryName,
+ sections: Object.values(category.sections),
+ }));
};
const beer = putResultsIntoGroups();
-
---
-
-
-
- Deep Search
- Showing {services.length} results for "{searchTerm}" sorted by relevence
-
-
- {
- beer.map((category: any) => (
-
- ))
- }
-
+
+
+
+ Deep Search
+
+ Showing {services.length} results for "{searchTerm}" sorted by relevence
+
+
+
+ {
+ beer.map((category: any) => (
+
+
+ {category.categoryName}
+
+
+
+
+
+ {category.sections.map((section: any) => (
+
+ {section.sectionName}
+
+ {section.items.map((item: Service) => (
+
+ {item.name}
+
+ ))}
+
+
+ ))}
+
+
+ ))
+ }
+
-
diff --git a/web/src/pages/search/index.astro b/web/src/pages/search/index.astro
index 924b188..da86b36 100644
--- a/web/src/pages/search/index.astro
+++ b/web/src/pages/search/index.astro
@@ -1,57 +1,55 @@
---
-
import Layout from '@layouts/Layout.astro';
import { fetchData } from '@utils/fetch-data';
import Search from '@components/things/Search.svelte';
const categories = (await fetchData())?.categories;
-
---
-
+
-
diff --git a/web/src/pages/sitemap.astro b/web/src/pages/sitemap.astro
index 60c3d11..41024b0 100644
--- a/web/src/pages/sitemap.astro
+++ b/web/src/pages/sitemap.astro
@@ -1,234 +1,244 @@
---
-
-
import Layout from '@layouts/Layout.astro';
import type { AwesomePrivacy } from '../types/Service';
import { fetchData, slugify } from '@utils/fetch-data';
-const categories = (await fetchData() as AwesomePrivacy)?.categories || [];
-
+const categories = ((await fetchData()) as AwesomePrivacy)?.categories || [];
---
-
- Sitemap
-
- Below is a full listing of all pages on this site.
- As reflected in our sitemap.xml
-
-
-
- Press enter for deep search
-
-
-
-
+
+ Sitemap
+
+ Below is a full listing of all pages on this site.
+ As reflected in our sitemap.xml
+
+
+
+
+ Press enter for deep search
+
+
+
+
diff --git a/web/src/pages/submit.astro b/web/src/pages/submit.astro
index abae8a3..b6eff6f 100644
--- a/web/src/pages/submit.astro
+++ b/web/src/pages/submit.astro
@@ -2,142 +2,187 @@
import Layout from '@layouts/Layout.astro';
import AddNewService from '@components/things/AddNewService.svelte';
-import { fetchGitHubStats } from '@utils/fetch-repo-info'
+import { fetchGitHubStats } from '@utils/fetch-repo-info';
import { formatDate } from '@utils/dates-n-stuff';
-const commits = (await fetchGitHubStats('lissy93/awesome-privacy') || {}).commits;
-
+const commits = ((await fetchGitHubStats('lissy93/awesome-privacy')) || {})
+ .commits;
---
-
- About our Data
-
- All data on Awesome Privacy is community maintained via Git,
- this keeps everything transparent, and means anyone can submit edits.
- You can learn more about how our data is managed on our about page .
-
- You can make ammendments/additions/removals simply by editing the
- awesome-privacy.yml file.
-
- Before you proceed, please first read our Contributing Docs
-
- Awesome Privacy is a community-maintained resource, it's thanks to
- contributors like you, that it's able to grow and stay up to date 💜
-
-
-
+
+ About our Data
+
+ All data on Awesome Privacy is community maintained via Git, this keeps
+ everything transparent, and means anyone can submit edits. You can learn
+ more about how our data is managed on our about page .
+
+ You can make ammendments/additions/removals simply by editing the
+ awesome-privacy.yml file.
+
+ Before you proceed, please first read our Contributing Docs
+
+ Awesome Privacy is a community-maintained resource, it's thanks to contributors
+ like you, that it's able to grow and stay up to date 💜
+
+
+
-
- Submit a Removal Request
-
- You can submit a removal request by browsing to a given service's page,
- and clicking the "Request Removal" button.
- This will open a form where you can justify your reasoning, to get it
- deleted from the awesome-privacy.yml file.
-
-
+
+ Submit a Removal Request
+
+ You can submit a removal request by browsing to a given service's page,
+ and clicking the "Request Removal" button. This will open a form where you
+ can justify your reasoning, to get it deleted from the awesome-privacy.yml file.
+
+
-
- Edit a Listing
-
- Edits are welcome! All data is located in
- awesome-privacy.yml .
-
- To modify an entry, navigate to it's page, scroll to the bottom, and click "Edit".
- This will take you to directly to the relevant lines in the file, where you can make your changes.
-
-
+
+ Edit a Listing
+
+ Edits are welcome! All data is located in
+ awesome-privacy.yml .
+
+ To modify an entry, navigate to it's page, scroll to the bottom, and click "Edit".
+ This will take you to directly to the relevant lines in the file, where you
+ can make your changes.
+
+
-
- Checklist
-
- You must read the Contributing guidelines before proceeding
- All listing must meed our Criteria to be considered privacy-respecting
- Double check that your changes haven't already been proposed
- If you're associated with a service included, you must declare your affiliation
- Before commiting changes, ensure the YAML syntax is valid and it complies with our schema
- Please complete the issue or PR description template in full, do not remove any fields
- All submissions must be made via our GitHub , do not email/PM maintainers
-
-
+
+ Checklist
+
+
+ You must read the Contributing guidelines before proceeding
+
+
+ All listing must meed our Criteria to be considered
+ privacy-respecting
+
+ Double check that your changes haven't already been proposed
+
+ If you're associated with a service included, you must declare your
+ affiliation
+
+
+ Before commiting changes, ensure the YAML syntax is valid and it
+ complies with our schema
+
+
+ Please complete the issue or PR description template in full, do not
+ remove any fields
+
+
+ All submissions must be made via our GitHub , do not email/PM maintainers
+
+
+
- {commits && commits.length > 0 && (
-
- )}
-
+ {
+ commits && commits.length > 0 && (
+
+ )
+ }
diff --git a/web/src/site-config.ts b/web/src/site-config.ts
index 66fe4f0..1c9a440 100644
--- a/web/src/site-config.ts
+++ b/web/src/site-config.ts
@@ -31,13 +31,15 @@ export const authorProjects = [
},
{
title: 'AdGuardian',
- description: 'CLI tool for monitoring your networks traffic and AdGuard DNS stats',
+ description:
+ 'CLI tool for monitoring your networks traffic and AdGuard DNS stats',
icon: 'https://adguardian.as93.net/favicon.png',
link: 'https://github.com/lissy93/adguardian-term',
},
{
title: 'Bug-Bounties',
- description: 'Database of websites which accept responsible vulnerability disclosure',
+ description:
+ 'Database of websites which accept responsible vulnerability disclosure',
icon: 'https://bug-bounties.as93.net/favicon.png',
link: 'https://github.com/lissy93/bug-bounties',
},
@@ -82,7 +84,6 @@ export const authorSocials = [
},
];
-
export const aboutOurData = `
All data is stored in
[\`awesome-privacy.yml\`](https://github.com/lissy93/awesome-privacy/blob/main/awesome-privacy.yml).
@@ -167,18 +168,20 @@ by the community, and the drawbacks / anti-features must be clearly listed along
Usually these entries go within the "Notable Mentions" section instead._
`;
-export const appDescription = 'Privacy is a fundamental human right; '
- + 'without it, we\'re just open books in a world where everyone\'s '
- + 'watching. Let\'s take control back.\n'
- + 'Migrating open-source applications which do not collect, sell or log your data is a great first step.'
- + 'Awesome Privacy is a directory of alternative privacy-respecting software and services.';
-
+export const appDescription =
+ 'Privacy is a fundamental human right; ' +
+ "without it, we're just open books in a world where everyone's " +
+ "watching. Let's take control back.\n" +
+ 'Migrating open-source applications which do not collect, sell or log your data is a great first step.' +
+ 'Awesome Privacy is a directory of alternative privacy-respecting software and services.';
export default {
title: 'Awesome Privacy | The Ultimate List of Private Apps',
- 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',
- author: 'Alicia Sykes',
+ 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',
+ author: 'Alicia Sykes',
authorProjects,
authorSocials,
aboutOurData,
diff --git a/web/src/styles/typography.css b/web/src/styles/typography.css
index e6d2b80..43eb4ef 100644
--- a/web/src/styles/typography.css
+++ b/web/src/styles/typography.css
@@ -3,91 +3,123 @@
/* Rubik Font Faces */
@font-face {
- font-family: 'Rubik';
- font-style: normal;
- font-weight: 400;
- src: local('Rubik'), url('/fonts/Rubik/Rubik-Regular.ttf') format('truetype');
+ font-family: 'Rubik';
+ font-style: normal;
+ font-weight: 400;
+ src:
+ local('Rubik'),
+ url('/fonts/Rubik/Rubik-Regular.ttf') format('truetype');
}
@font-face {
- font-family: 'Rubik';
- font-style: italic;
- font-weight: 400;
- src: local('Rubik Italic'), url('/fonts/Rubik/Rubik-Italic.ttf') format('truetype');
+ font-family: 'Rubik';
+ font-style: italic;
+ font-weight: 400;
+ src:
+ local('Rubik Italic'),
+ url('/fonts/Rubik/Rubik-Italic.ttf') format('truetype');
}
@font-face {
- font-family: 'Rubik';
- font-style: normal;
- font-weight: 500;
- src: local('Rubik Medium'), url('/fonts/Rubik/Rubik-Medium.ttf') format('truetype');
+ font-family: 'Rubik';
+ font-style: normal;
+ font-weight: 500;
+ src:
+ local('Rubik Medium'),
+ url('/fonts/Rubik/Rubik-Medium.ttf') format('truetype');
}
@font-face {
- font-family: 'Rubik';
- font-style: italic;
- font-weight: 500;
- src: local('Rubik Medium Italic'), url('/fonts/Rubik/Rubik-MediumItalic.ttf') format('truetype');
+ font-family: 'Rubik';
+ font-style: italic;
+ font-weight: 500;
+ src:
+ local('Rubik Medium Italic'),
+ url('/fonts/Rubik/Rubik-MediumItalic.ttf') format('truetype');
}
@font-face {
- font-family: 'Rubik';
- font-style: normal;
- font-weight: 600;
- src: local('Rubik SemiBold'), url('/fonts/Rubik/Rubik-SemiBold.ttf') format('truetype');
+ font-family: 'Rubik';
+ font-style: normal;
+ font-weight: 600;
+ src:
+ local('Rubik SemiBold'),
+ url('/fonts/Rubik/Rubik-SemiBold.ttf') format('truetype');
}
@font-face {
- font-family: 'Rubik';
- font-style: italic;
- font-weight: 600;
- src: local('Rubik SemiBold Italic'), url('/fonts/Rubik/Rubik-SemiBoldItalic.ttf') format('truetype');
+ font-family: 'Rubik';
+ font-style: italic;
+ font-weight: 600;
+ src:
+ local('Rubik SemiBold Italic'),
+ url('/fonts/Rubik/Rubik-SemiBoldItalic.ttf') format('truetype');
}
/* Libre Franklin Font Faces */
@font-face {
- font-family: 'Libre Franklin';
- font-style: normal;
- font-weight: 500;
- src: local('Libre Franklin Bold'), url('/fonts/Libre_Franklin/LibreFranklin-Bold.ttf') format('truetype');
+ font-family: 'Libre Franklin';
+ font-style: normal;
+ font-weight: 500;
+ src:
+ local('Libre Franklin Bold'),
+ url('/fonts/Libre_Franklin/LibreFranklin-Bold.ttf') format('truetype');
}
/* Lekton Font Faces */
@font-face {
- font-family: 'Lekton';
- font-style: normal;
- font-weight: 700;
- src: local('Lekton Bold'), url('/fonts/Lekton/Lekton-Bold.ttf') format('truetype');
+ font-family: 'Lekton';
+ font-style: normal;
+ font-weight: 700;
+ src:
+ local('Lekton Bold'),
+ url('/fonts/Lekton/Lekton-Bold.ttf') format('truetype');
}
html {
- font-family: system-ui, sans-serif;
+ font-family: system-ui, sans-serif;
}
code {
- font-family:
- Menlo,
- Monaco,
- Lucida Console,
- Liberation Mono,
- DejaVu Sans Mono,
- Bitstream Vera Sans Mono,
- Courier New,
- monospace;
+ font-family:
+ Menlo,
+ Monaco,
+ Lucida Console,
+ Liberation Mono,
+ DejaVu Sans Mono,
+ Bitstream Vera Sans Mono,
+ Courier New,
+ monospace;
}
-.heading, h1 {
- font-family: "Libre Franklin", sans-serif;
- font-optical-sizing: auto;
- font-weight: 800;
- font-style: normal;
+.heading,
+h1 {
+ font-family: 'Libre Franklin', sans-serif;
+ font-optical-sizing: auto;
+ font-weight: 800;
+ font-style: normal;
}
-.subtitle, h2, h3, h4, h5, h6 {
- font-family: "Lekton", sans-serif;
- font-weight: 700;
- font-style: normal;
+.subtitle,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: 'Lekton', sans-serif;
+ font-weight: 700;
+ font-style: normal;
}
-html, body, p, a, ul, ol, li, blockquote, pre, strong, i {
- font-family: "Rubik", sans-serif;
+html,
+body,
+p,
+a,
+ul,
+ol,
+li,
+blockquote,
+pre,
+strong,
+i {
+ font-family: 'Rubik', sans-serif;
}
a {
- color: var(--accent);
+ color: var(--accent);
}
diff --git a/web/src/styles/values.css b/web/src/styles/values.css
index 7dca931..eaf4c67 100644
--- a/web/src/styles/values.css
+++ b/web/src/styles/values.css
@@ -1,53 +1,51 @@
+html {
+ --accent: #f45397;
+ --accent-fg: #1e1f21;
- html {
- --accent: #f45397;
- --accent-fg: #1e1f21;
+ --accent-2: #ffdf60;
+ --accent-3: #5f53f4;
+ --accent-4: #28dffd;
- --accent-2: #ffdf60;
- --accent-3: #5f53f4;
- --accent-4: #28dffd;
+ --foreground: #fff;
- --foreground: #fff;
+ --curve-sm: 4px;
+ --curve-md: 6px;
+ --curve-lg: 12px;
- --curve-sm: 4px;
- --curve-md: 6px;
- --curve-lg: 12px;
+ --danger: #ff0048;
+ --success: #00ff64;
- --danger: #ff0048;
- --success: #00ff64;
+ --transparent-accent: #5f53f482;
- --transparent-accent: #5f53f482;
+ --background: #151517;
+ --bg-gradient-comp-1: #151517;
+ --bg-gradient-comp-2: #151517;
+ --background-form: #19191c;
- --background: #151517;
- --bg-gradient-comp-1: #151517;
- --bg-gradient-comp-2: #151517;
- --background-form: #19191c;
+ --box-outline: #000;
- --box-outline: #000;
+ &[data-theme='light'] {
+ --accent: #f45397;
+ --accent-fg: #fff;
- &[data-theme='light'] {
- --accent: #f45397;
- --accent-fg: #fff;
-
- --accent-2: #ffdf60;
- --accent-3: #5f53f4;
- --accent-4: #28dffd;
-
- --foreground: #13151a;
-
- --curve-sm: 4px;
- --curve-md: 6px;
- --curve-lg: 12px;
-
- --danger: #ff0048;
- --success: #00ff64;
-
- --transparent-accent: #5f53f482;
-
- --background: #feecff;
- --bg-gradient-comp-1: #feecff;
- --bg-gradient-comp-2: #e1e4fb;
- --background-form: #fff;
- }
+ --accent-2: #ffdf60;
+ --accent-3: #5f53f4;
+ --accent-4: #28dffd;
+
+ --foreground: #13151a;
+
+ --curve-sm: 4px;
+ --curve-md: 6px;
+ --curve-lg: 12px;
+
+ --danger: #ff0048;
+ --success: #00ff64;
+
+ --transparent-accent: #5f53f482;
+
+ --background: #feecff;
+ --bg-gradient-comp-1: #feecff;
+ --bg-gradient-comp-2: #e1e4fb;
+ --background-form: #fff;
+ }
}
-
diff --git a/web/src/types/Service.ts b/web/src/types/Service.ts
index f509e1e..4a3d00a 100644
--- a/web/src/types/Service.ts
+++ b/web/src/types/Service.ts
@@ -1,5 +1,3 @@
-
-
export interface ShortService {
name: string;
description: string;
@@ -42,7 +40,6 @@ export interface Category {
sections: Section[];
}
-
export interface AwesomePrivacy {
categories: Array<{
name: string;
diff --git a/web/src/utils/config.ts b/web/src/utils/config.ts
index f2baad3..3d57197 100644
--- a/web/src/utils/config.ts
+++ b/web/src/utils/config.ts
@@ -1,11 +1,13 @@
-
function cleanUrl(inputString: string) {
return inputString.replace(/['";]+/g, '').trim();
}
+export const site = cleanUrl(
+ import.meta.env.SITE_URL || 'https://awesome-privacy.xyz',
+);
-export const site = cleanUrl(import.meta.env.SITE_URL || 'https://awesome-privacy.xyz');
+export const title =
+ 'Awesome Privacy | Compare privacy-respecting alternatives to popular software & services';
-export const title = 'Awesome Privacy | Compare privacy-respecting alternatives to popular software & services';
-
-export const description = 'Your guide to escaping big tech, protecting your privacy, and reclaiming your digital life.';
+export const description =
+ 'Your guide to escaping big tech, protecting your privacy, and reclaiming your digital life.';
diff --git a/web/src/utils/data-src-delete-n-edit.ts b/web/src/utils/data-src-delete-n-edit.ts
index 00b31f9..e289329 100644
--- a/web/src/utils/data-src-delete-n-edit.ts
+++ b/web/src/utils/data-src-delete-n-edit.ts
@@ -1,60 +1,82 @@
import { slugify } from '@utils/fetch-data';
-export const makeRemovalRequest = (categoryName: string, sectionName: string, serviceName: string, yaml?: string) => {
+export const makeRemovalRequest = (
+ categoryName: string,
+ sectionName: string,
+ serviceName: string,
+ yaml?: string,
+) => {
const title = `[REMOVAL] ${serviceName}`;
- const under = `**${serviceName}** (source: [${categoryName} ➜ ${sectionName} ➜ ${serviceName}`
- + `](https://github.com/Lissy93/awesome-privacy/tree/main#${slugify(sectionName)}))`;
- const removalData = `&title=${encodeURIComponent(title)}&removal-data=`
- + `${encodeURIComponent(yaml || '')}&service-title=${encodeURIComponent(under)}`;
- const issueCreate = 'https://github.com/Lissy93/awesome-privacy/issues/new'
- const baseOptions = '?assignees=lissy93&labels=Suggested+Removal%2CAwaiting+'
- + 'Review&projects=&template=removal.yml'
+ const under =
+ `**${serviceName}** (source: [${categoryName} ➜ ${sectionName} ➜ ${serviceName}` +
+ `](https://github.com/Lissy93/awesome-privacy/tree/main#${slugify(sectionName)}))`;
+ const removalData =
+ `&title=${encodeURIComponent(title)}&removal-data=` +
+ `${encodeURIComponent(yaml || '')}&service-title=${encodeURIComponent(under)}`;
+ const issueCreate = 'https://github.com/Lissy93/awesome-privacy/issues/new';
+ const baseOptions =
+ '?assignees=lissy93&labels=Suggested+Removal%2CAwaiting+' +
+ 'Review&projects=&template=removal.yml';
return `${issueCreate}${baseOptions}${removalData}`;
};
-export const makeEditRequest = (categoryName: string, sectionName: string, serviceName: string, yaml?: string) => {
+export const makeEditRequest = (
+ categoryName: string,
+ sectionName: string,
+ serviceName: string,
+ yaml?: string,
+) => {
const title = `[AMENDMENT] ${serviceName}`;
- const under = `**${serviceName}** (source: [${categoryName} ➜ ${sectionName} ➜ ${serviceName}`
- + `](https://github.com/Lissy93/awesome-privacy/tree/main#${slugify(sectionName)}))`;
- const removalData = `&title=${encodeURIComponent(title)}&amendment-data=`
- + `${encodeURIComponent(yaml || '')}&service-title=${encodeURIComponent(under)}`;
- const issueCreate = 'https://github.com/Lissy93/awesome-privacy/issues/new'
- const baseOptions = '?assignees=lissy93&labels=Suggested+Removal%2CAwaiting+'
- + 'Review&projects=&template=amendment.yml'
+ const under =
+ `**${serviceName}** (source: [${categoryName} ➜ ${sectionName} ➜ ${serviceName}` +
+ `](https://github.com/Lissy93/awesome-privacy/tree/main#${slugify(sectionName)}))`;
+ const removalData =
+ `&title=${encodeURIComponent(title)}&amendment-data=` +
+ `${encodeURIComponent(yaml || '')}&service-title=${encodeURIComponent(under)}`;
+ const issueCreate = 'https://github.com/Lissy93/awesome-privacy/issues/new';
+ const baseOptions =
+ '?assignees=lissy93&labels=Suggested+Removal%2CAwaiting+' +
+ 'Review&projects=&template=amendment.yml';
return `${issueCreate}${baseOptions}${removalData}`;
};
-export const makeAdditionRequest = (formData: {
- listingCategory: string;
- serviceName: string;
- serviceUrl: string;
- serviceIcon: string;
- serviceDescription: string;
- serviceGithub: string;
- serviceTosdrId: string;
- serviceIosApp: string,
- serviceAndroidApp: string,
- serviceDiscordInvite: string,
- serviceSubreddit: string,
- serviceOpenSource: boolean;
- serviceSecurityAudited: boolean;
- serviceCrypto: boolean;
- additionalInfo: string;
-}, yamlText?: string) => {
-
- const userInfo = formData.additionalInfo.split('\n').map(line => `> ${line}`).join('\n');
- const additionalInfoText: string = `\n${userInfo}`
- + `\n\n**YAML Content for Addition**\n\n\`\`\`yaml\n${yamlText || '# nothing yet'}\n\`\`\`\n`
- + `\n\n
This ticket was submitted via `
- + `awesome-privacy.xyz/submit `;
+export const makeAdditionRequest = (
+ formData: {
+ listingCategory: string;
+ serviceName: string;
+ serviceUrl: string;
+ serviceIcon: string;
+ serviceDescription: string;
+ serviceGithub: string;
+ serviceTosdrId: string;
+ serviceIosApp: string;
+ serviceAndroidApp: string;
+ serviceDiscordInvite: string;
+ serviceSubreddit: string;
+ serviceOpenSource: boolean;
+ serviceSecurityAudited: boolean;
+ serviceCrypto: boolean;
+ additionalInfo: string;
+ },
+ yamlText?: string,
+) => {
+ const userInfo = formData.additionalInfo
+ .split('\n')
+ .map((line) => `> ${line}`)
+ .join('\n');
+ const additionalInfoText: string =
+ `\n${userInfo}` +
+ `\n\n**YAML Content for Addition**\n\n\`\`\`yaml\n${yamlText || '# nothing yet'}\n\`\`\`\n` +
+ `\n\n
This ticket was submitted via ` +
+ `awesome-privacy.xyz/submit `;
const issueTitle = `[ADDITION] ${formData.serviceName} (Complete)`;
const queryParams = new URLSearchParams({
- 'assignees': 'lissy93,liss-bot',
- 'labels': '',
- 'projects': '',
- 'template': 'complete-addition.yml',
- 'title': issueTitle,
+ assignees: 'lissy93,liss-bot',
+ labels: '',
+ projects: '',
+ template: 'complete-addition.yml',
+ title: issueTitle,
'listing-category': formData.listingCategory,
'service-name': formData.serviceName,
'service-url': formData.serviceUrl,
@@ -63,38 +85,59 @@ export const makeAdditionRequest = (formData: {
'service-github': formData.serviceGithub,
'service-tosdr-id': formData.serviceTosdrId,
'service-opensource': formData.serviceOpenSource ? 'true' : 'false',
- 'service-security-audited': formData.serviceSecurityAudited ? 'true' : 'false',
+ 'service-security-audited': formData.serviceSecurityAudited
+ ? 'true'
+ : 'false',
'service-crypto': formData.serviceCrypto ? 'true' : 'false',
'additional-info': additionalInfoText,
});
- const issueCreateUrl = 'https://github.com/Lissy93/awesome-privacy/issues/new';
+ const issueCreateUrl =
+ 'https://github.com/Lissy93/awesome-privacy/issues/new';
return `${issueCreateUrl}?${queryParams.toString()}`;
};
-
-export const makeSourceYamlLink = async (categoryName: string, sectionName: string, serviceName: string) => {
+export const makeSourceYamlLink = async (
+ categoryName: string,
+ sectionName: string,
+ serviceName: string,
+) => {
const sourceData = await fetchSrcData(categoryName, sectionName, serviceName);
const lineNumbers = sourceData.lineNumbers || null;
- const numberRange = lineNumbers ? `L${lineNumbers.start}-L${lineNumbers.end}` : '';
- const yamlLink = 'https://github.com/lissy93/awesome-privacy/blob/main/awesome-privacy.yml';
+ const numberRange = lineNumbers
+ ? `L${lineNumbers.start}-L${lineNumbers.end}`
+ : '';
+ const yamlLink =
+ 'https://github.com/lissy93/awesome-privacy/blob/main/awesome-privacy.yml';
return `${yamlLink}${numberRange}`;
};
-export const fetchSrcData = async (categoryName: string, sectionName: string, serviceName: string) => {
- const lineNumberData = await fetch('/api/line-numbers.json')
- .then((res) => res.json());
+export const fetchSrcData = async (
+ categoryName: string,
+ sectionName: string,
+ serviceName: string,
+) => {
+ const lineNumberData = await fetch('/api/line-numbers.json').then((res) =>
+ res.json(),
+ );
- if ( lineNumberData
- && lineNumberData[categoryName]
- && lineNumberData[categoryName][sectionName]
- && lineNumberData[categoryName][sectionName][serviceName]
+ if (
+ lineNumberData &&
+ lineNumberData[categoryName] &&
+ lineNumberData[categoryName][sectionName] &&
+ lineNumberData[categoryName][sectionName][serviceName]
) {
return {
- lineNumbers: lineNumberData[categoryName][sectionName][serviceName].lineNumbers,
+ lineNumbers:
+ lineNumberData[categoryName][sectionName][serviceName].lineNumbers,
yamlContent: lineNumberData[categoryName][sectionName][serviceName].yaml,
};
} else {
- console.error('No line number data found for', categoryName, sectionName, serviceName);
+ console.error(
+ 'No line number data found for',
+ categoryName,
+ sectionName,
+ serviceName,
+ );
return { lineNumbers: [], yamlContent: '' };
}
};
diff --git a/web/src/utils/dates-n-stuff.test.ts b/web/src/utils/dates-n-stuff.test.ts
new file mode 100644
index 0000000..e212661
--- /dev/null
+++ b/web/src/utils/dates-n-stuff.test.ts
@@ -0,0 +1,32 @@
+import { describe, it, expect } from 'vitest';
+import { formatDate, timestampToDate } from './dates-n-stuff';
+
+describe('formatDate', () => {
+ it('formats an ISO date string to en-GB short format', () => {
+ const result = formatDate('2024-01-15');
+ expect(result).toBe('15 Jan 24');
+ });
+
+ it('formats a different date correctly', () => {
+ const result = formatDate('2023-12-25');
+ expect(result).toBe('25 Dec 23');
+ });
+
+ it('handles full ISO datetime string', () => {
+ const result = formatDate('2024-06-01T12:00:00Z');
+ expect(result).toBe('01 Jun 24');
+ });
+});
+
+describe('timestampToDate', () => {
+ it('converts a Unix timestamp (ms) to en-GB short format', () => {
+ // 2024-01-15T00:00:00Z = 1705276800000
+ const result = timestampToDate(1705276800000);
+ expect(result).toBe('15 Jan 24');
+ });
+
+ it('converts epoch 0 to 01 Jan 70', () => {
+ const result = timestampToDate(0);
+ expect(result).toBe('01 Jan 70');
+ });
+});
diff --git a/web/src/utils/dates-n-stuff.ts b/web/src/utils/dates-n-stuff.ts
index a89fb57..3313720 100644
--- a/web/src/utils/dates-n-stuff.ts
+++ b/web/src/utils/dates-n-stuff.ts
@@ -2,21 +2,22 @@ export const formatDate = (date: string): string => {
return new Date(date).toLocaleDateString('en-GB', {
day: '2-digit',
month: 'short',
- year: '2-digit'
+ year: '2-digit',
});
-}
+};
export const timestampToDate = (timestamp: number): string => {
return new Date(timestamp).toLocaleDateString('en-GB', {
day: '2-digit',
month: 'short',
- year: '2-digit'
+ year: '2-digit',
});
-
-}
+};
export const timeAgo = (dateStr: string): string => {
- const seconds = Math.floor((new Date().getTime() - new Date(dateStr).getTime()) / 1000);
+ const seconds = Math.floor(
+ (new Date().getTime() - new Date(dateStr).getTime()) / 1000,
+ );
const intervals = {
year: 31536000,
month: 2592000,
diff --git a/web/src/utils/do-searchy-searchy.test.ts b/web/src/utils/do-searchy-searchy.test.ts
new file mode 100644
index 0000000..dfdef01
--- /dev/null
+++ b/web/src/utils/do-searchy-searchy.test.ts
@@ -0,0 +1,105 @@
+import { describe, it, expect } from 'vitest';
+import { prepareSearchItems } from './do-searchy-searchy';
+import type { Category } from '../types/Service';
+
+const makeCategory = (overrides: Partial
= {}): Category =>
+ ({
+ name: 'Test Category',
+ sections: [],
+ ...overrides,
+ }) as Category;
+
+describe('prepareSearchItems', () => {
+ it('returns an empty array for no categories', () => {
+ expect(prepareSearchItems([])).toEqual([]);
+ });
+
+ it('creates a category item', () => {
+ const categories = [makeCategory({ name: 'Privacy Tools' })];
+ const items = prepareSearchItems(categories);
+ expect(items).toHaveLength(1);
+ expect(items[0]).toMatchObject({
+ type: 'Category',
+ category: 'Privacy Tools',
+ itemCount: 0,
+ });
+ });
+
+ it('creates section items with category context', () => {
+ const categories = [
+ makeCategory({
+ name: 'Comms',
+ sections: [
+ { name: 'Messaging', intro: 'Secure messaging apps', services: [] },
+ ],
+ }),
+ ] as Category[];
+ const items = prepareSearchItems(categories);
+ const section = items.find((i: { type: string }) => i.type === 'Section');
+ expect(section).toMatchObject({
+ type: 'Section',
+ sectionName: 'Messaging',
+ description: 'Secure messaging apps',
+ category: 'Comms',
+ itemCount: 0,
+ });
+ });
+
+ it('creates service items with section and category context', () => {
+ const categories = [
+ makeCategory({
+ name: 'Comms',
+ sections: [
+ {
+ name: 'Messaging',
+ services: [
+ {
+ name: 'Signal',
+ description: 'Encrypted messenger',
+ url: 'https://signal.org',
+ github: 'signalapp/Signal-Android',
+ icon: 'signal.png',
+ },
+ ],
+ },
+ ],
+ }),
+ ] as Category[];
+ const items = prepareSearchItems(categories);
+ const service = items.find((i: { type: string }) => i.type === 'Service');
+ expect(service).toMatchObject({
+ type: 'Service',
+ name: 'Signal',
+ description: 'Encrypted messenger',
+ url: 'https://signal.org',
+ github: 'signalapp/Signal-Android',
+ category: 'Comms',
+ sectionName: 'Messaging',
+ logo: 'signal.png',
+ });
+ });
+
+ it('counts services across sections for category itemCount', () => {
+ const categories = [
+ makeCategory({
+ name: 'Tools',
+ sections: [
+ {
+ name: 'A',
+ services: [
+ { name: 's1', description: '', url: '' },
+ { name: 's2', description: '', url: '' },
+ ],
+ },
+ {
+ name: 'B',
+ services: [{ name: 's3', description: '', url: '' }],
+ },
+ ],
+ }),
+ ] as Category[];
+ const items = prepareSearchItems(categories);
+ const cat = items.find((i: { type: string }) => i.type === 'Category');
+ expect(cat.itemCount).toBe(3);
+ });
+});
diff --git a/web/src/utils/do-searchy-searchy.ts b/web/src/utils/do-searchy-searchy.ts
index 053ddbc..2eec8e6 100644
--- a/web/src/utils/do-searchy-searchy.ts
+++ b/web/src/utils/do-searchy-searchy.ts
@@ -3,17 +3,17 @@ import type { Category } from '../types/Service';
export const prepareSearchItems = (categories: Category[]) => {
const items: any = [];
// Add each category
- categories.forEach(category => {
+ categories.forEach((category) => {
items.push({
type: 'Category',
category: category.name,
itemCount: (category.sections || []).reduce((acc, section) => {
- return acc + (section.services || []).length;
- }, 0),
+ return acc + (section.services || []).length;
+ }, 0),
});
// Add section with category context
- category.sections.forEach(section => {
+ category.sections.forEach((section) => {
items.push({
type: 'Section',
sectionName: section.name,
@@ -21,9 +21,9 @@ export const prepareSearchItems = (categories: Category[]) => {
category: category.name,
itemCount: (section.services || []).length,
});
-
+
// Add service with section and category context
- (section.services || []).forEach(service => {
+ (section.services || []).forEach((service) => {
items.push({
type: 'Service',
name: service.name,
@@ -53,6 +53,6 @@ export const searchOptions = {
{ name: 'description', weight: 0.1 },
{ name: 'intro', weight: 0.1 },
{ name: 'furtherInfo', weight: 0.1 },
- { name: 'wordOfWarning', weight: 0.1 },
+ { name: 'wordOfWarning', weight: 0.1 },
],
};
diff --git a/web/src/utils/fetch-android-info.ts b/web/src/utils/fetch-android-info.ts
index 522a342..8b06d33 100644
--- a/web/src/utils/fetch-android-info.ts
+++ b/web/src/utils/fetch-android-info.ts
@@ -1,9 +1,10 @@
-
const doubleCheckPackageName = (packageStr: string) => {
return packageStr.includes('id=') ? packageStr.split('id=')[1] : packageStr;
-}
+};
-export const fetchAndroidInfo = async (androidPackage: string): Promise => {
+export const fetchAndroidInfo = async (
+ androidPackage: string,
+): Promise => {
const endpoint = `https://android-app-privacy.as93.net/${doubleCheckPackageName(androidPackage)}`;
try {
return await fetch(endpoint).then((res) => res.json());
@@ -43,5 +44,3 @@ export interface AndroidInfo {
trackers: Tracker[];
permissions: string[];
}
-
-
diff --git a/web/src/utils/fetch-data.test.ts b/web/src/utils/fetch-data.test.ts
new file mode 100644
index 0000000..89eee6d
--- /dev/null
+++ b/web/src/utils/fetch-data.test.ts
@@ -0,0 +1,37 @@
+import { describe, it, expect } from 'vitest';
+import { slugify } from './fetch-data';
+
+describe('slugify', () => {
+ it('lowercases and replaces spaces with hyphens', () => {
+ expect(slugify('Hello World')).toBe('hello-world');
+ });
+
+ it('replaces & with "and"', () => {
+ expect(slugify('Privacy & Security')).toBe('privacy-and-security');
+ });
+
+ it('replaces + with "and"', () => {
+ expect(slugify('Tools + Tips')).toBe('tools-and-tips');
+ });
+
+ it('removes question marks', () => {
+ expect(slugify('What is Privacy?')).toBe('what-is-privacy');
+ });
+
+ it('handles multiple spaces', () => {
+ expect(slugify('a b c')).toBe('a--b---c');
+ });
+
+ it('returns empty string for empty input', () => {
+ expect(slugify('')).toBe('');
+ });
+
+ it('returns empty string for undefined-like input', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ expect(slugify(undefined as any)).toBe('');
+ });
+
+ it('handles combined special characters', () => {
+ expect(slugify('Q&A + FAQ?')).toBe('qanda-and-faq');
+ });
+});
diff --git a/web/src/utils/fetch-data.ts b/web/src/utils/fetch-data.ts
index 6c95986..6ef3cd4 100644
--- a/web/src/utils/fetch-data.ts
+++ b/web/src/utils/fetch-data.ts
@@ -1,17 +1,21 @@
-
import yaml from 'js-yaml';
import type { AwesomePrivacy } from '../types/Service';
-const awesomePrivacyData = 'https://raw.githubusercontent.com/Lissy93/awesome-privacy/main/awesome-privacy.yml';
+const awesomePrivacyData =
+ 'https://raw.githubusercontent.com/Lissy93/awesome-privacy/main/awesome-privacy.yml';
export const fetchData = async (): Promise => {
- return await fetch(awesomePrivacyData)
+ return (await fetch(awesomePrivacyData)
.then((res) => res.text())
.then((data) => yaml.load(data))
- .catch((err) => console.error('ah crap', err)) as AwesomePrivacy;
-}
+ .catch((err) => console.error('ah crap', err))) as AwesomePrivacy;
+};
export const slugify = (title: string) => {
- return (title || '').toLowerCase().replace(/\s/g, '-').replace(/\+|&/g, 'and').replaceAll('?', '');
-};
+ return (title || '')
+ .toLowerCase()
+ .replace(/\s/g, '-')
+ .replace(/\+|&/g, 'and')
+ .replaceAll('?', '');
+};
diff --git a/web/src/utils/fetch-discord-info.ts b/web/src/utils/fetch-discord-info.ts
index bd0c0ce..cfce3ba 100644
--- a/web/src/utils/fetch-discord-info.ts
+++ b/web/src/utils/fetch-discord-info.ts
@@ -1,5 +1,6 @@
-
-export const fetchDiscordInfo = async (discordInvite: string): Promise => {
+export const fetchDiscordInfo = async (
+ discordInvite: string,
+): Promise => {
const endpoint = `https://discord-invite-info.as93.net/${discordInvite}`;
try {
return await fetch(endpoint).then((res) => res.json());
diff --git a/web/src/utils/fetch-docker-instructions.ts b/web/src/utils/fetch-docker-instructions.ts
index 739bfe9..bee7688 100644
--- a/web/src/utils/fetch-docker-instructions.ts
+++ b/web/src/utils/fetch-docker-instructions.ts
@@ -1,6 +1,6 @@
-
-
-export const fetchDockerData = async (serviceName: string): Promise => {
+export const fetchDockerData = async (
+ serviceName: string,
+): Promise => {
const endpoint = `https://docker-info.as93.workers.dev/${serviceName}`;
try {
return await fetch(endpoint).then((res) => res.json());
diff --git a/web/src/utils/fetch-ios-info.ts b/web/src/utils/fetch-ios-info.ts
index 5e1d5fa..fa445ab 100644
--- a/web/src/utils/fetch-ios-info.ts
+++ b/web/src/utils/fetch-ios-info.ts
@@ -1,5 +1,6 @@
-
-export const fetchIosInfo = async (iosUrl: string): Promise => {
+export const fetchIosInfo = async (
+ iosUrl: string,
+): Promise => {
const endpoint = `https://ios-app-info.as93.net?appStoreUrl=${iosUrl}`;
try {
return await fetch(endpoint).then((res) => res.json());
diff --git a/web/src/utils/fetch-privacy-policy.ts b/web/src/utils/fetch-privacy-policy.ts
index 2cff5a1..bb8db11 100644
--- a/web/src/utils/fetch-privacy-policy.ts
+++ b/web/src/utils/fetch-privacy-policy.ts
@@ -1,5 +1,6 @@
-
-export const fetchTosdrPrivacy = async (serviceId: string): Promise => {
+export const fetchTosdrPrivacy = async (
+ serviceId: string,
+): Promise => {
const endpoint = `https://privacy-policies.as93.workers.dev/${serviceId}`;
try {
return await fetch(endpoint).then((res) => res.json());
diff --git a/web/src/utils/fetch-reddit-info.ts b/web/src/utils/fetch-reddit-info.ts
index 810ec5a..22e18d2 100644
--- a/web/src/utils/fetch-reddit-info.ts
+++ b/web/src/utils/fetch-reddit-info.ts
@@ -1,5 +1,6 @@
-
-export const fetchRedditInfo = async (subreddit: string): Promise => {
+export const fetchRedditInfo = async (
+ subreddit: string,
+): Promise => {
const endpoint = `https://subreddit-info.as93.net/${subreddit}`;
try {
return await fetch(endpoint).then((res) => res.json());
diff --git a/web/src/utils/fetch-repo-info.ts b/web/src/utils/fetch-repo-info.ts
index 62a650c..b863a00 100644
--- a/web/src/utils/fetch-repo-info.ts
+++ b/web/src/utils/fetch-repo-info.ts
@@ -1,7 +1,6 @@
-
-
-
-export const fetchGitHubStats = async (github: string): Promise => {
+export const fetchGitHubStats = async (
+ github: string,
+): Promise => {
const endpoint = `https://repo-info.as93.workers.dev/${github}`;
try {
return await fetch(endpoint).then((res) => res.json());
diff --git a/web/src/utils/fetch-website-info.ts b/web/src/utils/fetch-website-info.ts
index 0f236ba..6a89df1 100644
--- a/web/src/utils/fetch-website-info.ts
+++ b/web/src/utils/fetch-website-info.ts
@@ -1,5 +1,6 @@
-
-export const fetchWebsiteInfo = async (url: string): Promise => {
+export const fetchWebsiteInfo = async (
+ url: string,
+): Promise => {
const endpoint = `https://site-info-fetch.as93.workers.dev/?url=${url}`;
try {
return await fetch(endpoint).then((res) => res.json());
@@ -19,10 +20,10 @@ interface DNSRecord {
interface DNSRecords {
ns: {
- records: DNSRecord[];
+ records: DNSRecord[];
};
mx: {
- records: DNSRecord[];
+ records: DNSRecord[];
};
}
diff --git a/web/src/utils/parse-markdown.test.ts b/web/src/utils/parse-markdown.test.ts
new file mode 100644
index 0000000..64cf38a
--- /dev/null
+++ b/web/src/utils/parse-markdown.test.ts
@@ -0,0 +1,43 @@
+import { describe, it, expect } from 'vitest';
+import { formatLink } from './parse-markdown';
+
+describe('formatLink', () => {
+ it('strips https://', () => {
+ expect(formatLink('https://example.com')).toBe('example.com');
+ });
+
+ it('strips http://', () => {
+ expect(formatLink('http://example.com')).toBe('example.com');
+ });
+
+ it('strips www.', () => {
+ expect(formatLink('https://www.example.com')).toBe('example.com');
+ });
+
+ it('strips trailing slash', () => {
+ expect(formatLink('https://example.com/')).toBe('example.com');
+ });
+
+ it('strips multiple trailing slashes', () => {
+ expect(formatLink('https://example.com///')).toBe('example.com');
+ });
+
+ it('preserves path segments', () => {
+ expect(formatLink('https://example.com/path/to/page')).toBe(
+ 'example.com/path/to/page',
+ );
+ });
+
+ it('handles bare domain', () => {
+ expect(formatLink('example.com')).toBe('example.com');
+ });
+
+ it('handles empty string', () => {
+ expect(formatLink('')).toBe('');
+ });
+
+ it('handles undefined-like input', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ expect(formatLink(undefined as any)).toBe('');
+ });
+});
diff --git a/web/src/utils/parse-markdown.ts b/web/src/utils/parse-markdown.ts
index 3dffdd8..420bbb4 100644
--- a/web/src/utils/parse-markdown.ts
+++ b/web/src/utils/parse-markdown.ts
@@ -23,19 +23,24 @@ export const parseMarkdown = (text: string | undefined): string => {
// Sanitize the input to remove