diff --git a/web/src/components/Card.astro b/web/src/components/Card.astro index 7b1e813..147f7ba 100644 --- a/web/src/components/Card.astro +++ b/web/src/components/Card.astro @@ -21,19 +21,18 @@ const { href, title, body } = Astro.props; diff --git a/web/src/components/Hero.astro b/web/src/components/Hero.astro index a0e9b7c..0c32133 100644 --- a/web/src/components/Hero.astro +++ b/web/src/components/Hero.astro @@ -1,61 +1,60 @@ --- -import FontAwesome from "@components/form/FontAwesome.svelte" -import ThemeSwitcher from "@components/form/ThemeSwitcher.svelte" +import FontAwesome from '@components/form/FontAwesome.svelte'; +import ThemeSwitcher from '@components/form/ThemeSwitcher.svelte'; ---
- +
-

Awesome Privacy

-

- Your guide to finding and comparing privacy-respecting alternatives to popular software and services. -

- +

Awesome Privacy

+

+ Your guide to finding and comparing privacy-respecting alternatives to + popular software and services. +

+
- diff --git a/web/src/components/form/Button.astro b/web/src/components/form/Button.astro index a2d9a26..d449306 100644 --- a/web/src/components/form/Button.astro +++ b/web/src/components/form/Button.astro @@ -1,37 +1,30 @@ --- - -const { - text, - url, - className, - title, -} = Astro.props; - +const { text, url, className, title } = Astro.props; ---
- {text} + {text}
diff --git a/web/src/components/form/EditableTitle.svelte b/web/src/components/form/EditableTitle.svelte index ee9b999..816e9b2 100644 --- a/web/src/components/form/EditableTitle.svelte +++ b/web/src/components/form/EditableTitle.svelte @@ -41,25 +41,27 @@ } - +
-

editing = true} - on:keydown={handleKeydown} - on:blur={() => saveTitle(title)} - tabindex="0" ->{title}

+

(editing = true)} + on:keydown={handleKeydown} + on:blur={() => saveTitle(title)} + tabindex="0" + > + {title} +

-Click the title, to edit your inventory name + Click the title, to edit your inventory name
- diff --git a/web/src/components/form/ThemeSwitcher.svelte b/web/src/components/form/ThemeSwitcher.svelte index 3f5542a..3e7f7c8 100644 --- a/web/src/components/form/ThemeSwitcher.svelte +++ b/web/src/components/form/ThemeSwitcher.svelte @@ -28,7 +28,6 @@ } -
@@ -38,7 +37,6 @@
- diff --git a/web/src/components/scafold/Footer.astro b/web/src/components/scafold/Footer.astro index 7e02fed..ed7a7ba 100644 --- a/web/src/components/scafold/Footer.astro +++ b/web/src/components/scafold/Footer.astro @@ -1,23 +1,21 @@ - diff --git a/web/src/components/scafold/MainCard.astro b/web/src/components/scafold/MainCard.astro index 18e2c8e..ed2e997 100644 --- a/web/src/components/scafold/MainCard.astro +++ b/web/src/components/scafold/MainCard.astro @@ -1,7 +1,5 @@ - -
- +
diff --git a/web/src/components/scafold/NavBar.astro b/web/src/components/scafold/NavBar.astro index 64ed300..fab5392 100644 --- a/web/src/components/scafold/NavBar.astro +++ b/web/src/components/scafold/NavBar.astro @@ -1,106 +1,111 @@ --- -import FontAwesome from "@components/form/FontAwesome.svelte" -import ThemeSwitcher from "@components/form/ThemeSwitcher.svelte" - +import FontAwesome from '@components/form/FontAwesome.svelte'; +import ThemeSwitcher from '@components/form/ThemeSwitcher.svelte'; --- diff --git a/web/src/components/things/AddNewService.svelte b/web/src/components/things/AddNewService.svelte index 48c8cc8..a77cdd0 100644 --- a/web/src/components/things/AddNewService.svelte +++ b/web/src/components/things/AddNewService.svelte @@ -23,11 +23,11 @@ let codeBlock: any; let interactiveActivated = false; - $: yamlText, updateHighlighting(); + $: (yamlText, updateHighlighting()); function updateHighlighting() { if (codeBlock) { - codeBlock.textContent = yamlText + codeBlock.textContent = yamlText; codeBlock.dataset.highlighted && delete codeBlock.dataset.highlighted; if (window && (window as any).hljs) { (window as any).hljs.highlightElement(codeBlock); @@ -38,15 +38,17 @@ const filterEmptyValues = (obj: Record) => { const filteredObj: Record = {}; - Object.keys(obj).forEach(key => { + Object.keys(obj).forEach((key) => { if (obj[key] || ['name', 'url', 'icon', 'description'].includes(key)) { filteredObj[key] = obj[key]; } }); return filteredObj; - } - - $: yamlText = yaml.dump([{ + }; + + $: yamlText = yaml.dump( + [ + { name: $serviceName, url: $serviceUrl, icon: $serviceIcon, @@ -60,9 +62,12 @@ openSource: $serviceOpenSource, securityAudited: $serviceSecurityAudited, acceptsCrypto: $serviceCrypto, - }].map(obj => filterEmptyValues(obj))); + }, + ].map((obj) => filterEmptyValues(obj)), + ); - $: issueUrl = makeAdditionRequest({ + $: issueUrl = makeAdditionRequest( + { listingCategory: $listingCategory, serviceName: $serviceName, serviceUrl: $serviceUrl, @@ -78,7 +83,9 @@ serviceSecurityAudited: $serviceSecurityAudited, serviceCrypto: $serviceCrypto, additionalInfo: $additionalInfo, - }, yamlText); + }, + yamlText, + ); // Form submission handler function handleSubmit() { @@ -87,29 +94,39 @@ - - - + + +

- Before completing this form, you must ensure that the service you are adding aligns - with the Requirements for Awesome Privacy. + Before completing this form, you must ensure that the service you are adding + aligns with the Requirements for Awesome + Privacy.
You'll need a GitHub account in order to submit this form.

-

Basics

-

- All fields here are required. -

+

All fields here are required.

- @@ -126,45 +143,78 @@

- Choose the top-level category, which should align with - the one of these. + Choose the top-level category, which should align with the one of these.

- +

Enter the name of the app, software or service

- -

Enter the fully-qualified domain name of the homepage for this listing

+ +

+ Enter the fully-qualified domain name of the homepage for this listing +

- +
- -

Paste a URL to a square logo for the service. Dimensions must be no less than 64x64, and no more than 512x512 pixels

+ +

+ Paste a URL to a square logo for the service. Dimensions must be no less + than 64x64, and no more than 512x512 pixels +

- -

Please provide a description for this listing. Keep it factual and objective. Markdown is supported.

+ +

+ Please provide a description for this listing. Keep it factual and + objective. Markdown is supported. +

Third-Party Referencing

- In order to create a comprehensive listing, we combine the data inputted above with other sources, - to give additional context and help users make informed decisions. - Metrics from these services are fetched automatically at build-time from our API. + In order to create a comprehensive listing, we combine the data inputted + above with other sources, to give additional context and help users make + informed decisions. Metrics from these services are fetched automatically at + build-time from our API.
All fields are optional, but the more information you provide, the better!

@@ -172,7 +222,13 @@
- +

Share a link to where the project's source is located.
Use the format [user]/[repo] e.g, lissy93/dashy @@ -182,18 +238,29 @@

- +

- Has the Privacy policy been documented by tosdr.org? - If so, please include the report reference below (this is a 3 or 4-digit numerical ID). - Skip section if not applicable. + Has the Privacy policy been documented by tosdr.org? If so, please include the report reference below (this is a 3 or + 4-digit numerical ID). Skip section if not applicable.

- +

Paste the link to the mobile app on the Apple App Store.
E.g. https://apps.apple.com/us/app/bitwarden-password-manager/id1137397744 @@ -203,7 +270,12 @@

- +

Paste the link to the mobile app on the Google Play Store.
E.g. https://play.google.com/store/apps/details?id=com.x8bit.bitwarden @@ -213,17 +285,28 @@

- +

Paste the invite code to the Discord server for this service.
- E.g. If the invite URL is https://discord.com/invite/4JMAauFZBq the code is 4JMAauFZBq + E.g. If the invite URL is https://discord.com/invite/4JMAauFZBq the code is + 4JMAauFZBq

- +

If the service has a subreddit, please provide the name here.
Don't include `r/` in the name, nor the full URL - just the sub name. @@ -233,70 +316,94 @@

Privacy Checklist

- Finally, check the boxes that apply to the service you are submitting, - and then provide any additional information to back this up in the text area below. + Finally, check the boxes that apply to the service you are submitting, and + then provide any additional information to back this up in the text area + below.

- +
- -

Is this service fully open source? Aka, can it be compiled from source by the user, or self-hosted?

+ +

+ Is this service fully open source? Aka, can it be compiled from source by + the user, or self-hosted? +

- -

Has this service been independently security audited by an accredited auditor?

+ +

+ Has this service been independently security audited by an accredited + auditor? +

- -

If this is a hosted and paid for service, does it accept anonymous payment methods, including crypto (e.g., Monero)?

+ +

+ If this is a hosted and paid for service, does it accept anonymous payment + methods, including crypto (e.g., Monero)? +

-

- Finally, please provide any supporting material, including: -

+

Finally, please provide any supporting material, including:

  • A justification of why this app/service should be included in the list
  • +
  • Links to any published security audit, if they exist
  • - Links to any published security audit, if they exist + Links to the services privacy policy, terms of service and other + relevant documents where applicable
  • - Links to the services privacy policy, terms of service and other relevant - documents where applicable + Your affiliation with the service. For transparency, you must disclose + if you are associated with them or any similar items in any way
  • - Your affiliation with the service. - For transparency, you must disclose if you are associated - with them or any similar items in any way + Links to relevant discussions, past issues/PRs related to this service
  • -
  • Links to relevant discussions, past issues/PRs related to this service
- +
- Open in GitHub Issues + Open in GitHub Issues
-

Below is the YAML content, which will be appended to the appropriate section - within awesome-privacy.yml +

+ Below is the YAML content, which will be appended to the appropriate section + within awesome-privacy.yml upon approval.

{#if !interactiveActivated || !codeBlock}
{@html yamlText}
{/if}
-

Your submission will need to be reviewed by a maintainer and the community before it can be merged.

+

+ Your submission will need to be reviewed by a maintainer and the community + before it can be merged. +

diff --git a/web/src/components/things/Comments.svelte b/web/src/components/things/Comments.svelte index 1a69a52..554e416 100644 --- a/web/src/components/things/Comments.svelte +++ b/web/src/components/things/Comments.svelte @@ -1,19 +1,25 @@ diff --git a/web/src/components/things/DataActions.svelte b/web/src/components/things/DataActions.svelte index 04ccad4..c519441 100644 --- a/web/src/components/things/DataActions.svelte +++ b/web/src/components/things/DataActions.svelte @@ -1,18 +1,23 @@ {#if lineNumbers} +

Edit {serviceName} Data

+

+ You can view or edit this {serviceName}'s entry in + this section + of awesome-privacy.yml in our GitHub repo. +

-

Edit {serviceName} Data

-

- You can view or edit this {serviceName}'s entry in - - this section - - of awesome-privacy.yml in our GitHub repo. -

- -

Origin Data

- - -

Modify Data

- +

Origin Data

+ +

Modify Data

+ {/if} - diff --git a/web/src/components/things/DeleteListing.svelte b/web/src/components/things/DeleteListing.svelte index 2e825d0..ac26b73 100644 --- a/web/src/components/things/DeleteListing.svelte +++ b/web/src/components/things/DeleteListing.svelte @@ -1,15 +1,17 @@ - diff --git a/web/src/components/things/DiscordDetailedInfo.astro b/web/src/components/things/DiscordDetailedInfo.astro index cbf47b8..0696a43 100644 --- a/web/src/components/things/DiscordDetailedInfo.astro +++ b/web/src/components/things/DiscordDetailedInfo.astro @@ -1,110 +1,113 @@ --- - import type { DiscordInfo } from '@utils/fetch-discord-info'; -import { formatDate, timeAgo } from '@utils/dates-n-stuff'; -import FontAwesome from "@components/form/FontAwesome.svelte" - +import { formatDate, timeAgo } from '@utils/dates-n-stuff'; +import FontAwesome from '@components/form/FontAwesome.svelte'; interface Props { - discordData: DiscordInfo; -}; + discordData: DiscordInfo; +} const { discordData } = Astro.props; - - ---
+

Discord

-

Discord

+
    +
  • + Server Name + {discordData.name} +
  • +
  • + Member Count + {discordData.memberCount} ({discordData.memberOnlineCount} online) +
  • +
  • + Initial Channel + {discordData.channel} +
  • +
  • + Inviter + {discordData.inviter || 'Anon'} +
  • +
  • + Join Link + discord.com/invite/{discordData.inviteCode} +
  • +
-
    -
  • - Server Name - {discordData.name} -
  • -
  • - Member Count - {discordData.memberCount} ({discordData.memberOnlineCount} online) -
  • -
  • - Initial Channel - {discordData.channel} -
  • -
  • - Inviter - {discordData.inviter || 'Anon'} -
  • -
  • - Join Link - discord.com/invite/{discordData.inviteCode} -
  • -
- - { discordData.banner && ()} - - + { + discordData.banner && ( + + ) + }
- diff --git a/web/src/components/things/GetSharableLink.svelte b/web/src/components/things/GetSharableLink.svelte index 0fcc694..5af0bdf 100644 --- a/web/src/components/things/GetSharableLink.svelte +++ b/web/src/components/things/GetSharableLink.svelte @@ -1,37 +1,39 @@ - - - + diff --git a/web/src/components/things/ServiceCard.svelte b/web/src/components/things/ServiceCard.svelte index d76be99..fc8d0ec 100644 --- a/web/src/components/things/ServiceCard.svelte +++ b/web/src/components/things/ServiceCard.svelte @@ -17,7 +17,10 @@
- +

{service.name}

{#if service.followWith} @@ -26,11 +29,7 @@
- +
diff --git a/web/src/components/things/ServiceList.astro b/web/src/components/things/ServiceList.astro index db4f69b..2be6a08 100644 --- a/web/src/components/things/ServiceList.astro +++ b/web/src/components/things/ServiceList.astro @@ -1,5 +1,4 @@ --- - import Button from '@components/form/Button.astro'; import { parseMarkdown, formatLink } from '@utils/parse-markdown'; import type { Service } from 'src/types/Service'; @@ -11,326 +10,387 @@ import GitHubMetrics from '@components/things/ItemGitHubMetrics.astro'; import SaveListing from '@components/things/SaveListing.svelte'; interface Props { - services: Service[]; - subHeading?: boolean; - buttonLink?: string; - noGitHubMetrics?: boolean; - sectionName: string; - categoryName: string; + services: Service[]; + subHeading?: boolean; + buttonLink?: string; + noGitHubMetrics?: boolean; + sectionName: string; + categoryName: string; } const { - services, - subHeading, - buttonLink, - noGitHubMetrics, - sectionName, - categoryName, + services, + subHeading, + buttonLink, + noGitHubMetrics, + sectionName, + categoryName, } = Astro.props; - --- - -
- {services && services.length > 0 ? ( -
    - {services.map((service: Service) => ( -
  • - -
    - -
    -
    - {`${service.name} + { + services && services.length > 0 ? ( +
      + {services.map((service: Service) => ( +
    • + +
      + +
      +
      + {`${service.name} - - {subHeading ?

      {service.name}

      :

      {service.name}

      } -
      - {service.followWith && } - {formatLink(service.url)} -
      -
      -

      -
      -
      - { service.securityAudited && ( - - Security Audited - - )} - { service.acceptsCrypto && ( - - Crypto Payments Accepted - - )} - { service.securityAudited === false && ( - - No Security Audit - - )} - { (service.openSource === false) && ( - - - Not Open Source - - )} - { service.openSource || (service.github && service.openSource !== false) ? ( - - Open Source - - ) : null } - { service.github && !noGitHubMetrics && } - { service.github && noGitHubMetrics && ( - - - {service.github} - - - ) } -
      - -
      -
      -
    • - ))} -
    - ) : ( -

    - ⚠️ This section is still a work in progress ⚠️
    - Check back soon, or help us complete it by submiting a pull request on GitHub. -
    - Or submit an entry here -

    - )} + + {subHeading ?

    {service.name}

    :

    {service.name}

    } +
    + {service.followWith && ( + + )} + + {formatLink(service.url)} + +
    +
    +

    +

    +
    + {service.securityAudited && ( + + Security + Audited + + )} + {service.acceptsCrypto && ( + + Crypto Payments + Accepted + + )} + {service.securityAudited === false && ( + + No Security + Audit + + )} + {service.openSource === false && ( + + + Not Open Source + + )} + {service.openSource || + (service.github && service.openSource !== false) ? ( + + Open Source + + ) : null} + {service.github && !noGitHubMetrics && ( + + )} + {service.github && noGitHubMetrics && ( + + + {service.github} + + + )} +
    + +
    +
    +
  • + ))} +
+ ) : ( +

+ <> + ⚠️ This section is still a work in progress ⚠️ +
+ + Check back soon, or help us complete it by submiting a pull request on + GitHub. +
+ + Or submit an entry here + +

+ ) + } - {buttonLink && ( -
- diff --git a/web/src/components/things/SmartSuggestions.svelte b/web/src/components/things/SmartSuggestions.svelte index 8dc0609..3df98b1 100644 --- a/web/src/components/things/SmartSuggestions.svelte +++ b/web/src/components/things/SmartSuggestions.svelte @@ -1,7 +1,7 @@ + src="https://no-track.as93.net/js/script.js"> - {breadcrumbs && ( - + + diff --git a/web/src/pages/api/line-numbers.json.ts b/web/src/pages/api/line-numbers.json.ts index 28ac2ef..f64d189 100644 --- a/web/src/pages/api/line-numbers.json.ts +++ b/web/src/pages/api/line-numbers.json.ts @@ -14,35 +14,47 @@ interface LineNumberData { [service: string]: { lineNumbers: LineNumberRange | null; yaml: string; - } + }; }; }; } -const awesomePrivacyYamlPath = 'https://raw.githubusercontent.com/Lissy93/awesome-privacy/main/awesome-privacy.yml'; +const awesomePrivacyYamlPath = + 'https://raw.githubusercontent.com/Lissy93/awesome-privacy/main/awesome-privacy.yml'; /** * Given a service object and an array of string lines from the raw YAML * Find the starting and ending line number for that service */ -const calculateServiceRange = (service: Service, category: Category, yamlLines: string[]): LineNumberRange | null => { +const calculateServiceRange = ( + service: Service, + category: Category, + yamlLines: string[], +): LineNumberRange | null => { const lookFor = `- name: ${service.name}`; - const categoryStart = yamlLines.findIndex(line => line.includes(category.name)); - const start = yamlLines.slice(categoryStart).findIndex(line => line.includes(lookFor)) + categoryStart + 1; + const categoryStart = yamlLines.findIndex((line) => + line.includes(category.name), + ); + const start = + yamlLines.slice(categoryStart).findIndex((line) => line.includes(lookFor)) + + categoryStart + + 1; if (start === -1) return null; const detectEnd = (line: string) => { - return line.trim().length === 0 - || line.startsWith(' - ') - || line.includes('- name:') - || line.includes('notableMentions:') - || line.includes('furtherInfo:') - || line.includes('wordOfWarning:') - } + return ( + line.trim().length === 0 || + line.startsWith(' - ') || + line.includes('- name:') || + line.includes('notableMentions:') || + line.includes('furtherInfo:') || + line.includes('wordOfWarning:') + ); + }; const remainingLines = yamlLines.slice(start); const end = start + remainingLines.findIndex(detectEnd); - + return { start, end }; -} +}; /** * Given a service object, convert it into a correctly formatted YAML string @@ -55,7 +67,10 @@ const convertJsonIntoYaml = (service: Service): string => { * Given the object representation of the YAML and the array of lines from the raw YAML * Organize the data into a format that can be returned as JSON */ -const makeResults = (yamlObject: AwesomePrivacy, yamlLines: string[]): LineNumberData => { +const makeResults = ( + yamlObject: AwesomePrivacy, + yamlLines: string[], +): LineNumberData => { const organizedData: LineNumberData = {}; (yamlObject.categories || []).forEach((category) => { organizedData[category.name] = {}; @@ -70,16 +85,18 @@ const makeResults = (yamlObject: AwesomePrivacy, yamlLines: string[]): LineNumbe }); }); return organizedData; -} +}; export const GET: APIRoute = async () => { - // Fetch the raw YAML from the awesome-privacy repository const yamlContent = await fetch(awesomePrivacyYamlPath) - .then(response => response.text()) - .catch(error => { - return JSON.stringify({ error: "Failed to fetch YAML file", details: error }); - }); + .then((response) => response.text()) + .catch((error) => { + return JSON.stringify({ + error: 'Failed to fetch YAML file', + details: error, + }); + }); // Array of lines from the raw YAML const yamlLines: string[] = yamlContent.split('\n'); @@ -90,7 +107,7 @@ export const GET: APIRoute = async () => { // Make results const results = makeResults(yamlObject, yamlLines); - return new Response( - JSON.stringify(results), { headers: { 'content-type': 'application/json' } } - ) -} + return new Response(JSON.stringify(results), { + headers: { 'content-type': 'application/json' }, + }); +}; diff --git a/web/src/pages/browse.astro b/web/src/pages/browse.astro index 6a0d031..1549b15 100644 --- a/web/src/pages/browse.astro +++ b/web/src/pages/browse.astro @@ -1,5 +1,4 @@ --- - import Layout from '@layouts/Layout.astro'; import SectionList from '@components/things/SectionList.astro'; @@ -7,210 +6,221 @@ import { fetchData } from '@utils/fetch-data'; import type { Category } from '../types/Service'; const categories: Category[] = (await fetchData())?.categories; - --- +
+

Browse

+ + +

+ Press enter for deep search +

+
+
+ -
-

Browse

- - -

Press enter for deep search

-
-
- +
    + { + categories.map((category) => ( +
  • + +
  • + )) + } +
-
    - {categories.map((category) => ( -
  • - -
  • - ))} -
- -
-

Nothing found 😢

-

Try a deep search instead

-
+
+

Nothing found 😢

+

Try a deep search instead

+
- diff --git a/web/src/pages/index.astro b/web/src/pages/index.astro index bd29faa..ef6678f 100644 --- a/web/src/pages/index.astro +++ b/web/src/pages/index.astro @@ -1,5 +1,4 @@ --- - import Layout from '@layouts/Layout.astro'; import Hero from '@components/Hero.astro'; import Search from '@components/things/Search.svelte'; @@ -10,14 +9,14 @@ import { fetchData } from '@utils/fetch-data'; import Button from '@components/form/Button.astro'; import type { Category } from 'src/types/Service'; -const categories = (await fetchData())?.categories || [] as Category[]; - -const description = '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.'; +const categories = (await fetchData())?.categories || ([] as Category[]); +const description = + '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.'; --- @@ -29,11 +28,13 @@ const description = 'Privacy is a fundamental human right; '

Browse

    - {categories.map((category) => ( -
  • - -
  • - ))} + { + categories.map((category) => ( +
  • + +
  • + )) + }
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. -

-
- )} - -
-

Not found what you're looking for?

- -
-
+
+

{makeTitle(inventoryId)}

+ { + cheekyLilError && ( +
+

An error occoured

+

{cheekyLilError}

+

+ We're sorry about that. +
+ Try going back home, or{' '} + + raising a ticket + {' '} + on GitHub. +

+
+ ) + } + +
+

Not found what you're looking for?

+ +
+
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[]); --- -
-
- - - -
- -
-

Not found what you're looking for?

- -
-
+
+
+ + + +
+ +
+

Not found what you're looking for?

+ +
+
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(); - --- -
-

Search

- -
- -
-

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} -
    • - ))} -
    -
  • - ))} -
-
- )) - } -
+
+

Search

+ +
+ +
+

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; - --- -
-

Search

- - -
+
+

Search

+ + +
- 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 💜 -

-
-
-

Submit an Addition

- -
+
+

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 an Addition

+ +
-
-

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 && ( -
-

Recent Changes

-

- You can view a full ledger of all updates made - at github.com/lissy93/awesome-privacy -

- -
- )} - + { + commits && commits.length > 0 && ( +
+

Recent Changes

+

+ You can view a full ledger of all updates made at{' '} + + github.com/lissy93/awesome-privacy + +

+ +
+ ) + }
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\nThis 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\nThis 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