diff --git a/.github/workflows/web-checks.yml b/.github/workflows/web-checks.yml new file mode 100644 index 0000000..0736816 --- /dev/null +++ b/.github/workflows/web-checks.yml @@ -0,0 +1,96 @@ +name: Web Checks + +on: + pull_request: + paths: ['web/**'] + workflow_dispatch: + +permissions: + contents: read + +jobs: + lint: + name: ๐Ÿงผ Lint + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: web/.nvmrc + cache: yarn + cache-dependency-path: web/yarn.lock + - run: yarn install --frozen-lockfile + - run: yarn lint + + format: + name: ๐Ÿ’… Format + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: web/.nvmrc + cache: yarn + cache-dependency-path: web/yarn.lock + - run: yarn install --frozen-lockfile + - run: yarn format:check + + typecheck: + name: ๐Ÿงฉ Typecheck + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: web/.nvmrc + cache: yarn + cache-dependency-path: web/yarn.lock + - run: yarn install --frozen-lockfile + - run: yarn typecheck + + test: + name: ๐Ÿงช Test + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: web/.nvmrc + cache: yarn + cache-dependency-path: web/yarn.lock + - run: yarn install --frozen-lockfile + - run: yarn test + + summary: + name: ๐Ÿ’ฌ Summary + if: always() + needs: [lint, format, typecheck, test] + runs-on: ubuntu-latest + steps: + - name: Build results table + run: | + em() { if [ "$1" = "success" ]; then echo "pass โœ…"; else echo "FAIL โŒ"; fi; } + line() { echo "| $1 | \`$2\` | $(em "$3") |"; } + + { + echo "## Web Checks Summary" + echo "" + echo "| Check | Command | Result |" + echo "|-------|---------|--------|" + line "Lint" "yarn lint" "${{ needs.lint.result }}" + line "Format" "yarn format:check" "${{ needs.format.result }}" + line "Typecheck" "yarn typecheck" "${{ needs.typecheck.result }}" + line "Test" "yarn test" "${{ needs.test.result }}" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/lib/awesome-privacy-readme-gen.py b/lib/awesome-privacy-readme-gen.py index 396006c..9c6d623 100644 --- a/lib/awesome-privacy-readme-gen.py +++ b/lib/awesome-privacy-readme-gen.py @@ -1,13 +1,13 @@ """ Reads app list from awesome-privacy.yml, -formats into markdown, and inserts into README.md +formats into markdown, and inserts into README.md """ import os import re import yaml import logging -from urllib.parse import urlparse +from urllib.parse import urlparse, quote # Configure Logging LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO").upper() @@ -44,14 +44,32 @@ def tosElement(tosdrId): return "" return f"[![Privacy Policy](https://shields.tosdr.org/en_{tosdrId}.svg)](https://tosdr.org/en/service/{tosdrId})" -def statsElement(isOpenSource, isSecurityAudited, isAcceptsCrypto): +def statsElement(app, categoryName, sectionName): statsStr = "" - if isOpenSource == True: - statsStr += "๐Ÿ“ฆ Open Source " - if isSecurityAudited == True: - statsStr += "๐Ÿ›ก๏ธ Security Audited " - if isAcceptsCrypto == True: - statsStr += "๐Ÿ’ฐ Accepts Anonymous Payment " + if app.get('openSource') == True: + github = app.get('github') + if github: + link = f"https://github.com/{github}" + elif app.get('url'): + link = app.get('url') + else: + link = f"https://awesome-privacy.xyz/{slugify(categoryName)}/{slugify(sectionName)}/{slugify(app.get('name'))}" + statsStr += ( + f"[![Open Source](https://img.shields.io/badge/-Open_Source-3DA639" + f"?style=flat&logo=opensourceinitiative&logoColor=white)]({link}) " + ) + if app.get('securityAudited') == True: + statsStr += ( + "![Security Audited](https://img.shields.io/badge/-Security_Audited-3DA639" + "?style=flat&logo=data:image/svg+xml;base64," + "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiPjxwYXRoIGQ9Ik0xMiAxTDMgNXY2YzAgNS41NSAzLjg0IDEwLjc0IDkgMTIgNS4xNi0xLjI2IDktNi40NSA5LTEyVjVsLTktNHoiLz48L3N2Zz4=" + "&logoColor=white) " + ) + if app.get('acceptsCrypto') == True: + statsStr += ( + "![Accepts Anonymous Payment](https://img.shields.io/badge/-Anon_Payment_Accepted" + "%EF%B8%8F-3DA639?style=flat&logo=bitcoincash&logoColor=white) " + ) return statsStr def slugify(title): @@ -63,6 +81,11 @@ def slugify(title): title = title.replace('?', '') return title +def shieldsEncode(text): + if not text: return '' + text = text.strip().replace('-', '--').replace('_', '__').replace(' ', '_') + return quote(text, safe='_-.') + def awesomePrivacyReport(categoryName, sectionName, serviceName): if not serviceName: return "" @@ -72,12 +95,74 @@ def awesomePrivacyReport(categoryName, sectionName, serviceName): f"(https://awesome-privacy.xyz/{slugify(categoryName)}/{slugify(sectionName)}/{slugify(serviceName)})" ) -def makeStatsCard(): - return ( - f"\t-
Stats\n\n" - f"" - f"\n\n
" - ) +def playStoreBadge(name, androidApp): + if not androidApp: return "" + encoded = shieldsEncode(name) + return ( + f"[![{name} on Google Play](https://img.shields.io/badge/-{encoded}-3bd47f" + f"?style=flat&logo=android&logoColor=white)]" + f"(https://play.google.com/store/apps/details?id={androidApp}) " + ) + +def appStoreBadge(name, iosApp): + if not iosApp: return "" + encoded = shieldsEncode(name) + return ( + f"[![{name} on App Store](https://img.shields.io/badge/-{encoded}-0D96F6" + f"?style=flat&logo=appstore&logoColor=white)]" + f"({iosApp}) " + ) + +def redditBadge(subreddit): + if not subreddit or not subreddit.strip(): return "" + sub = subreddit.strip() + return ( + f"[![r/{sub} on Reddit](https://img.shields.io/badge/-{sub}-FF4500" + f"?style=flat&logo=reddit&logoColor=white)]" + f"(https://reddit.com/r/{sub}) " + ) + +def discordBadge(name, discordInvite): + if not discordInvite or not discordInvite.strip(): return "" + invite = discordInvite.strip() + encoded = shieldsEncode(name) + link = invite if invite.startswith('https://') else f"https://discord.gg/{invite}" + return ( + f"[![{name} on Discord](https://img.shields.io/badge/-{encoded}-5865F2" + f"?style=flat&logo=discord&logoColor=white)]" + f"({link}) " + ) + +_MD_PATTERNS = [ + re.compile(r'\[([^\]]*)\]\([^)]*\)'), # [text](url) โ€” group 1 = visible text + re.compile(r'\*\*(.+?)\*\*'), # **bold** + re.compile(r'`([^`]+)`'), # `code` + re.compile(r'(?\n\t\tStats\n\n\t\t" - f"{repoElement(app.get('github'))} " - f"{tosElement(app.get('tosdrId'))} " - f"{awesomePrivacyReport(category.get('name'), section.get('name'), app.get('name'))} \n" - f"{statsElement(app.get('openSource'), app.get('securityAudited'), app.get('acceptsCrypto'))}ห™ \n" - f"\n\t\t\n" - ) - if app.get('github') or app.get('tosdrId') else '') + f"({app.get('url')})** - {description}{ellipsis} \n" ) + badges = ' '.join(filter(None, [ + repoElement(app.get('github')), + tosElement(app.get('tosdrId')), + awesomePrivacyReport(category.get('name'), section.get('name'), app.get('name')), + statsElement(app, category.get('name'), section.get('name')).rstrip(), + playStoreBadge(app.get('name'), app.get('androidApp')).rstrip(), + appStoreBadge(app.get('name'), app.get('iosApp')).rstrip(), + redditBadge(app.get('subreddit')).rstrip(), + discordBadge(app.get('name'), app.get('discordInvite')).rstrip(), + ])) + if badges: + markdown += ( + f"\t-
\n\t\tStats\n\n\t\t" + f"{badges}ใ…ค \n" + f"\n\t\t
\n" + ) markdown += "\n" # If word of warning exists, append it if section.get('wordOfWarning'): @@ -145,7 +241,7 @@ def makeAwesomePrivacy(): markdown += f"> - [{mention.get('name')}]({mention.get('url')})" + ( f" - {mention.get('description')}" if mention.get('description') else "\n" ) - else: + else: notable_mentions = section.get('notableMentions').replace('\n', '\n> ') markdown += f"> {notable_mentions}" @@ -170,7 +266,7 @@ def update_content_between_markers(content, start_marker, end_marker, new_conten logger.info(f"Updating content between {start_marker} and {end_marker} markers...") start_index = content.find(start_marker) end_index = content.find(end_marker) - + if start_index != -1 and end_index != -1: before_section = content[:start_index + len(start_marker)] after_section = content[end_index:] diff --git a/web/.editorconfig b/web/.editorconfig new file mode 100644 index 0000000..57bda5a --- /dev/null +++ b/web/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true + +[*.astro] +indent_style = tab + +[*.{ts,js,svelte,scss,json}] +indent_style = space +indent_size = 2 diff --git a/web/.gitignore b/web/.gitignore index b8a4815..d333b9f 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -19,3 +19,5 @@ dist/ # macOS crap .DS_Store + +.vscode/ diff --git a/web/.nvmrc b/web/.nvmrc new file mode 100644 index 0000000..0a49261 --- /dev/null +++ b/web/.nvmrc @@ -0,0 +1 @@ +24.11.0 diff --git a/web/.prettierignore b/web/.prettierignore new file mode 100644 index 0000000..21560d1 --- /dev/null +++ b/web/.prettierignore @@ -0,0 +1,17 @@ +dist/ +.astro/ +node_modules/ +.vercel/ +yarn.lock +public/ +*.md + +# Astro files with adjacent JSX elements that prettier-plugin-astro cannot parse +src/components/things/DockerDetailedInfo.astro +src/components/things/GitHubDetailedInfo.astro +src/components/things/IosAppDetailedInfo.astro +src/components/things/ItemGitHubMetrics.astro +src/components/things/PrivacyPolicyDetails.astro +src/components/things/WebsiteDetailedInfo.astro +src/pages/*section*.astro +src/pages/all.astro diff --git a/web/.prettierrc b/web/.prettierrc new file mode 100644 index 0000000..47f5a56 --- /dev/null +++ b/web/.prettierrc @@ -0,0 +1,22 @@ +{ + "useTabs": true, + "singleQuote": true, + "semi": true, + "trailingComma": "all", + "plugins": ["prettier-plugin-astro", "prettier-plugin-svelte"], + "overrides": [ + { + "files": "*.astro", + "options": { + "parser": "astro" + } + }, + { + "files": ["*.ts", "*.js", "*.svelte", "*.scss"], + "options": { + "useTabs": false, + "tabWidth": 2 + } + } + ] +} diff --git a/web/.vscode/extensions.json b/web/.vscode/extensions.json deleted file mode 100644 index 22a1505..0000000 --- a/web/.vscode/extensions.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "recommendations": ["astro-build.astro-vscode"], - "unwantedRecommendations": [] -} diff --git a/web/.vscode/launch.json b/web/.vscode/launch.json deleted file mode 100644 index d642209..0000000 --- a/web/.vscode/launch.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "command": "./node_modules/.bin/astro dev", - "name": "Development server", - "request": "launch", - "type": "node-terminal" - } - ] -} diff --git a/web/.yarnrc.yml b/web/.yarnrc.yml index 651c1d1..41a8cf6 100644 --- a/web/.yarnrc.yml +++ b/web/.yarnrc.yml @@ -1,9 +1,9 @@ npmScopes: fortawesome: npmAlwaysAuth: true - npmRegistryServer: "https://npm.fontawesome.com/" + npmRegistryServer: 'https://npm.fontawesome.com/' npmAuthToken: ECB95473-FBAF-463F-905C-C9ED4C00D519 awesome: npmAlwaysAuth: true - npmRegistryServer: "https://npm.fontawesome.com/" + npmRegistryServer: 'https://npm.fontawesome.com/' npmAuthToken: ECB95473-FBAF-463F-905C-C9ED4C00D519 diff --git a/web/astro.config.mjs b/web/astro.config.mjs index 496e7bc..af13b5b 100644 --- a/web/astro.config.mjs +++ b/web/astro.config.mjs @@ -25,13 +25,25 @@ const integrations = [svelte(), partytown(), sitemap()]; // Set the appropriate adapter, based on the deploy target const adapter = { - vercel: vercelAdapter, - netlify: netlifyAdapter, - cloudflare: cloudflareAdapter, - node: nodeAdapter({ - mode: 'standalone', - }), + vercel: vercelAdapter, + netlify: netlifyAdapter, + cloudflare: cloudflareAdapter, + node: nodeAdapter({ + mode: 'standalone', + }), }[deployTarget](); // Export Astro configuration -export default defineConfig({ output, integrations, site, adapter }); +export default defineConfig({ + output, + integrations, + site, + adapter, + vite: { + css: { + preprocessorOptions: { + scss: { api: 'modern' }, + }, + }, + }, +}); diff --git a/web/eslint.config.js b/web/eslint.config.js new file mode 100644 index 0000000..f33aae6 --- /dev/null +++ b/web/eslint.config.js @@ -0,0 +1,80 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import eslintPluginAstro from 'eslint-plugin-astro'; +import eslintPluginSvelte from 'eslint-plugin-svelte'; +import svelteParser from 'svelte-eslint-parser'; +import eslintConfigPrettier from 'eslint-config-prettier'; +import globals from 'globals'; + +export default [ + // Global ignores + { ignores: ['dist/', '.astro/', 'node_modules/', '.vercel/'] }, + + // Base JS config + js.configs.recommended, + + // TypeScript + ...tseslint.configs.recommended, + + // Astro + ...eslintPluginAstro.configs.recommended, + + // Svelte โ€” with TypeScript parser for - +
-

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..64fc513 100644 --- a/web/src/components/scafold/Footer.astro +++ b/web/src/components/scafold/Footer.astro @@ -1,23 +1,29 @@ +--- +const year = new Date().getFullYear(); +--- 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..3e8aa7d 100644 --- a/web/src/components/things/AddNewService.svelte +++ b/web/src/components/things/AddNewService.svelte @@ -20,33 +20,42 @@ const serviceCrypto = writable(false); const additionalInfo = writable(''); - let codeBlock: any; + let codeBlock: HTMLElement | undefined; let interactiveActivated = false; - $: yamlText, updateHighlighting(); + $: (yamlText, updateHighlighting()); + /* eslint-disable svelte/no-dom-manipulating -- hljs requires direct DOM access for syntax highlighting */ 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); + const hljs = ( + window as Window & { + hljs?: { highlightElement: (el: HTMLElement) => void }; + } + ).hljs; + if (hljs) { + hljs.highlightElement(codeBlock); interactiveActivated = true; } } } + /* eslint-enable svelte/no-dom-manipulating */ - const filterEmptyValues = (obj: Record) => { - const filteredObj: Record = {}; - Object.keys(obj).forEach(key => { + const filterEmptyValues = (obj: Record) => { + const filteredObj: Record = {}; + 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 +69,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 +90,9 @@ serviceSecurityAudited: $serviceSecurityAudited, serviceCrypto: $serviceCrypto, additionalInfo: $additionalInfo, - }, yamlText); + }, + yamlText, + ); // Form submission handler function handleSubmit() { @@ -87,29 +101,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 +150,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 +229,13 @@
- +

Share a link to where the project's source is located.
Use the format [user]/[repo] e.g, lissy93/dashy @@ -182,18 +245,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 +277,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 +292,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 +323,95 @@

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..7b0eb84 100644 --- a/web/src/components/things/DiscordDetailedInfo.astro +++ b/web/src/components/things/DiscordDetailedInfo.astro @@ -1,110 +1,111 @@ --- - import type { DiscordInfo } from '@utils/fetch-discord-info'; -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..3b06ed1 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 @@
- +
@@ -45,6 +44,7 @@ src={service.icon || `https://icon.horse/icon/${formatLink(service.url)}`} />
+

{@html service.description}

@@ -65,5 +65,5 @@
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..657569d 100644 --- a/web/src/pages/inventory/[...inventoryId].astro +++ b/web/src/pages/inventory/[...inventoryId].astro @@ -1,15 +1,12 @@ --- - import Layout from '@layouts/Layout.astro'; import SavedServices from '@components/things/SavedServices.svelte'; -import GetSharableLink from '@components/things/GetSharableLink.svelte'; import { fetchData } from '@utils/fetch-data'; 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 +14,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..23c84d0 100644 --- a/web/src/pages/search/[...searchTerm].astro +++ b/web/src/pages/search/[...searchTerm].astro @@ -1,18 +1,17 @@ --- - import Fuse from 'fuse.js'; +import Fuse from 'fuse.js'; import Layout from '@layouts/Layout.astro'; import { fetchData, slugify } from '@utils/fetch-data'; import { prepareSearchItems, searchOptions } from '@utils/do-searchy-searchy'; +import type { SearchItem } from '@utils/do-searchy-searchy'; import Search from '@components/things/Search.svelte'; import SmartSuggestions from '@components/things/SmartSuggestions.svelte'; import FontAwesome from '@components/form/FontAwesome.svelte'; -import type { Service } from '../../types/Service'; - export const prerender = false; -let fuse: Fuse; +let fuse: Fuse; const categories = (await fetchData())?.categories; @@ -22,171 +21,187 @@ 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'); + +interface GroupedSection { + sectionName: string; + items: SearchItem[]; +} + +interface GroupedCategory { + categoryName: string; + sections: Record; +} const putResultsIntoGroups = () => { - const grouped = services.reduce((acc, item) => { - const { category: categoryName, sectionName, ...service } = item; + const grouped: Record = {}; - if (!acc[categoryName]) { - acc[categoryName] = { categoryName, sections: {} }; - } + for (const item of services) { + const categoryName = item.category; + const sectionName = item.sectionName || ''; - if (!acc[categoryName].sections[sectionName]) { - acc[categoryName].sections[sectionName] = { sectionName, items: [] }; - } + if (!grouped[categoryName]) { + grouped[categoryName] = { categoryName, sections: {} }; + } - acc[categoryName].sections[sectionName].items.push(service); + if (!grouped[categoryName].sections[sectionName]) { + grouped[categoryName].sections[sectionName] = { sectionName, items: [] }; + } - return acc; - }, {}); + grouped[categoryName].sections[sectionName].items.push(item); + } - // 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) - })); + return Object.values(grouped).map((category) => ({ + 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) => ( +
+ +

{category.categoryName}

+
+ + + +
    + {category.sections.map((section) => ( +
  • +

    {section.sectionName}

    +
      + {section.items.map((item) => ( +
    • + {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..e21a177 100644 --- a/web/src/site-config.ts +++ b/web/src/site-config.ts @@ -14,37 +14,59 @@ export const authorProjects = [ { title: 'Web-Check', description: 'OSINT tool for analysing any website', - icon: 'https://web-check.as93.net/web-check.png', + icon: 'https://cdn.as93.net/logo/web-check/w256', link: 'https://github.com/lissy93/web-check', }, { title: 'Dashy', description: 'Dashboard app, for organising your self-hosted services', - icon: 'https://dashy.to/img/dashy.png', + icon: 'https://cdn.as93.net/logo/dashy/w256', link: 'https://github.com/lissy93/dashy', }, + { + title: 'Domain Locker', + description: + 'All-in-one tool, for keeping track of your domain name portfolio', + icon: 'https://cdn.as93.net/logo/domain-locker/w256', + link: 'https://github.com/lissy93/domain-locker', + }, + { + title: 'Pixelflare', + description: 'Ultra high-performance privacy-respecting image CDN', + icon: 'https://cdn.as93.net/logo/pixelflare/w256', + link: 'https://github.com/Lissy93/pixelflare', + }, + { + title: 'Networking Toolbox', + description: + '100+ offline-first networking lookups, calculators and conversions', + icon: 'https://cdn.as93.net/logo/networking-toolbox/w256', + link: 'https://github.com/Lissy93/networking-toolbox', + }, { title: 'Portainer-Templates', description: 'Compiled repository of 1-click Docker apps for self-hosting', - icon: 'https://portainer-templates.as93.net/favicon.png', + icon: 'https://cdn.as93.net/logo/portainer-templates/w256', link: 'https://github.com/lissy93/portainer-templates', }, { title: 'AdGuardian', - description: 'CLI tool for monitoring your networks traffic and AdGuard DNS stats', - icon: 'https://adguardian.as93.net/favicon.png', + description: + 'CLI tool for monitoring your networks traffic and AdGuard DNS stats', + icon: 'https://cdn.as93.net/logo/adguardian/w256', link: 'https://github.com/lissy93/adguardian-term', }, { title: 'Bug-Bounties', - description: 'Database of websites which accept responsible vulnerability disclosure', - icon: 'https://bug-bounties.as93.net/favicon.png', + description: + 'Database of websites which accept responsible vulnerability disclosure', + icon: 'https://cdn.as93.net/logo/bug-bounties', link: 'https://github.com/lissy93/bug-bounties', }, { title: 'Git-In', description: 'Tools and resources to help beginners get into open source', - icon: 'https://www.git-in.to/favicon.png', + icon: 'https://cdn.as93.net/logo/git-in/w256', link: 'https://github.com/lissy93/git-in', }, ]; @@ -82,9 +104,8 @@ export const authorSocials = [ }, ]; - export const aboutOurData = ` -All data is stored in +All data is stored in [\`awesome-privacy.yml\`](https://github.com/lissy93/awesome-privacy/blob/main/awesome-privacy.yml). This file is then pulled into the website at build-time, and also used to generate @@ -132,7 +153,7 @@ Use our public instance, at: \`https://api.awesome-privacy.xyz\` or [self-host y `; export const projectRequirements = ` -For software to be included in this list, it must meet the following requirements: +For software to be included in this list, it must meet the following requirements: - **Privacy Respecting** - The project must respect users privacy, not collect more data than necessary, and store info securely @@ -148,7 +169,7 @@ For software to be included in this list, it must meet the following requirement - Ideally it should be possible for the user to build and run/deploy the software themselves from source - **Actively Maintained** - The developers should address dependency updates and security patches in a timely manner - - Ideally the source should have been updated within the last 12 months + - Ideally the source should have been updated within the last 12 months - **Transparent** - It should be clear who is behind the project, what their motives are, and what (if any) the funding model is - For hosted solutions, the privacy policy should clearly state what data is collected, how it's used and how long it's stored @@ -167,18 +188,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..d560808 --- /dev/null +++ b/web/src/utils/do-searchy-searchy.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from 'vitest'; +import { prepareSearchItems } from './do-searchy-searchy'; +import type { SearchItem } 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: SearchItem) => 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: SearchItem) => 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: SearchItem) => 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..afebe55 100644 --- a/web/src/utils/do-searchy-searchy.ts +++ b/web/src/utils/do-searchy-searchy.ts @@ -1,19 +1,31 @@ import type { Category } from '../types/Service'; -export const prepareSearchItems = (categories: Category[]) => { - const items: any = []; +export interface SearchItem { + type: 'Category' | 'Section' | 'Service'; + category: string; + itemCount?: number; + sectionName?: string; + description?: string; + name?: string; + url?: string; + github?: string; + logo?: string; +} + +export const prepareSearchItems = (categories: Category[]): SearchItem[] => { + const items: SearchItem[] = []; // 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 +33,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 +65,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..1ddeb29 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[]; }; } @@ -60,7 +61,7 @@ interface Redirection { found: boolean; external: boolean; url: string; - redirects: any[]; + redirects: string[]; } interface ResponseHeaders { 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