mirror of
https://github.com/Lissy93/awesome-privacy.git
synced 2026-03-11 08:55:33 +00:00
commit
714a87f31a
87 changed files with 8789 additions and 6332 deletions
96
.github/workflows/web-checks.yml
vendored
Normal file
96
.github/workflows/web-checks.yml
vendored
Normal file
|
|
@ -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"
|
||||
|
|
@ -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"[](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"[]({link}) "
|
||||
)
|
||||
if app.get('securityAudited') == True:
|
||||
statsStr += (
|
||||
" "
|
||||
)
|
||||
if app.get('acceptsCrypto') == True:
|
||||
statsStr += (
|
||||
" "
|
||||
)
|
||||
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- <details><summary>Stats</summary>\n\n"
|
||||
f""
|
||||
f"\n\n</details>"
|
||||
)
|
||||
def playStoreBadge(name, androidApp):
|
||||
if not androidApp: return ""
|
||||
encoded = shieldsEncode(name)
|
||||
return (
|
||||
f"[]"
|
||||
f"(https://play.google.com/store/apps/details?id={androidApp}) "
|
||||
)
|
||||
|
||||
def appStoreBadge(name, iosApp):
|
||||
if not iosApp: return ""
|
||||
encoded = shieldsEncode(name)
|
||||
return (
|
||||
f"[]"
|
||||
f"({iosApp}) "
|
||||
)
|
||||
|
||||
def redditBadge(subreddit):
|
||||
if not subreddit or not subreddit.strip(): return ""
|
||||
sub = subreddit.strip()
|
||||
return (
|
||||
f"[]"
|
||||
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"[]"
|
||||
f"({link}) "
|
||||
)
|
||||
|
||||
_MD_PATTERNS = [
|
||||
re.compile(r'\[([^\]]*)\]\([^)]*\)'), # [text](url) — group 1 = visible text
|
||||
re.compile(r'\*\*(.+?)\*\*'), # **bold**
|
||||
re.compile(r'`([^`]+)`'), # `code`
|
||||
re.compile(r'(?<!\*)\*([^*]+)\*(?!\*)'), # *italic*
|
||||
]
|
||||
|
||||
def truncateMarkdown(text, maxLen=200):
|
||||
"""Returns (truncated_text, was_truncated) preserving markdown constructs."""
|
||||
if len(text) <= maxLen:
|
||||
return text, False
|
||||
|
||||
result = []
|
||||
visible = 0
|
||||
i = 0
|
||||
|
||||
while i < len(text) and visible < maxLen:
|
||||
for pattern in _MD_PATTERNS:
|
||||
m = pattern.match(text, i)
|
||||
if m:
|
||||
result.append(m.group(0))
|
||||
visible += len(m.group(1))
|
||||
i = m.end()
|
||||
break
|
||||
else:
|
||||
result.append(text[i])
|
||||
visible += 1
|
||||
i += 1
|
||||
|
||||
return ''.join(result).rstrip(), True
|
||||
|
||||
def makeHref(text):
|
||||
if not text: return "#"
|
||||
|
|
@ -116,21 +201,32 @@ def makeAwesomePrivacy():
|
|||
)
|
||||
# For each service, list it's name, icon, url, and description
|
||||
for app in section.get('services') or []:
|
||||
description, was_truncated = truncateMarkdown(app.get('description', ''))
|
||||
ap_link = (
|
||||
f"https://awesome-privacy.xyz/"
|
||||
f"{slugify(category.get('name'))}/{slugify(section.get('name'))}/{slugify(app.get('name'))}"
|
||||
)
|
||||
ellipsis = f"[…]({ap_link} \"View full {app.get('name')} report\")" if was_truncated else ""
|
||||
markdown += (
|
||||
f"- **[{iconElement(app.get('url'), app.get('icon'))} {app.get('name')}]"
|
||||
f"({app.get('url')})** - {app.get('description')}"
|
||||
f"[…](https://awesome-privacy.xyz/"
|
||||
f"{slugify(category.get('name'))}/{slugify(section.get('name'))}/{slugify(app.get('name'))} \"View full {app.get('name')} report\") \n"
|
||||
+ ((
|
||||
f"\t- <details>\n\t\t<summary>Stats</summary>\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</details>\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- <details>\n\t\t<summary>Stats</summary>\n\n\t\t"
|
||||
f"{badges}ㅤ \n"
|
||||
f"\n\t\t</details>\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:]
|
||||
|
|
|
|||
14
web/.editorconfig
Normal file
14
web/.editorconfig
Normal file
|
|
@ -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
|
||||
2
web/.gitignore
vendored
2
web/.gitignore
vendored
|
|
@ -19,3 +19,5 @@ dist/
|
|||
|
||||
# macOS crap
|
||||
.DS_Store
|
||||
|
||||
.vscode/
|
||||
|
|
|
|||
1
web/.nvmrc
Normal file
1
web/.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
24.11.0
|
||||
17
web/.prettierignore
Normal file
17
web/.prettierignore
Normal file
|
|
@ -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
|
||||
22
web/.prettierrc
Normal file
22
web/.prettierrc
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
4
web/.vscode/extensions.json
vendored
4
web/.vscode/extensions.json
vendored
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
11
web/.vscode/launch.json
vendored
11
web/.vscode/launch.json
vendored
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
80
web/eslint.config.js
Normal file
80
web/eslint.config.js
Normal file
|
|
@ -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 <script lang="ts">
|
||||
...eslintPluginSvelte.configs['flat/recommended'].map((config) =>
|
||||
config.files
|
||||
? {
|
||||
...config,
|
||||
languageOptions: {
|
||||
...config.languageOptions,
|
||||
parser: svelteParser,
|
||||
parserOptions: {
|
||||
...config.languageOptions?.parserOptions,
|
||||
parser: tseslint.parser,
|
||||
},
|
||||
},
|
||||
}
|
||||
: config,
|
||||
),
|
||||
|
||||
// Global settings
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
'no-console': 'off',
|
||||
'no-case-declarations': 'off',
|
||||
'no-useless-assignment': 'warn',
|
||||
},
|
||||
},
|
||||
|
||||
// Allow triple-slash references in env.d.ts (Astro convention)
|
||||
{
|
||||
files: ['src/env.d.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/triple-slash-reference': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Lenient Svelte rules — existing code uses these patterns intentionally
|
||||
{
|
||||
files: ['**/*.svelte'],
|
||||
rules: {
|
||||
'svelte/no-at-html-tags': 'warn',
|
||||
'svelte/require-each-key': 'warn',
|
||||
'svelte/no-dom-manipulating': 'warn',
|
||||
},
|
||||
},
|
||||
|
||||
// Prettier must be last to override conflicting rules
|
||||
eslintConfigPrettier,
|
||||
];
|
||||
103
web/package.json
103
web/package.json
|
|
@ -1,37 +1,70 @@
|
|||
{
|
||||
"name": "web",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro preview",
|
||||
"build": "astro check && astro build",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.5.4",
|
||||
"@astrojs/netlify": "^5.1.2",
|
||||
"@astrojs/partytown": "^2.0.4",
|
||||
"@astrojs/sitemap": "^3.1.0",
|
||||
"@astrojs/svelte": "^5.0.3",
|
||||
"@astrojs/vercel": "^7.3.2",
|
||||
"@fortawesome/fontawesome-pro": "^6.5.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.1",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/svelte-fontawesome": "^0.2.2",
|
||||
"astro": "^4.3.6",
|
||||
"fuse.js": "^7.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"marked": "^12.0.0",
|
||||
"svelte": "^4.2.11",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/cloudflare": "^9.0.1",
|
||||
"@astrojs/node": "^8.2.1",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^20.11.19",
|
||||
"sass": "^1.70.0"
|
||||
}
|
||||
"name": "web",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro preview",
|
||||
"build": "astro check && astro build",
|
||||
"astro": "astro",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"typecheck": "astro check",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"check:all": "astro check && eslint . && prettier --check . && vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.6",
|
||||
"@astrojs/netlify": "^5.5.4",
|
||||
"@astrojs/partytown": "^2.1.4",
|
||||
"@astrojs/sitemap": "^3.7.0",
|
||||
"@astrojs/svelte": "^5.7.3",
|
||||
"@astrojs/vercel": "^7.8.2",
|
||||
"@fortawesome/fontawesome-pro": "^6.5.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.1",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/svelte-fontawesome": "^0.2.2",
|
||||
"astro": "^4.16.19",
|
||||
"fuse.js": "^7.0.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"marked": "^12.0.2",
|
||||
"svelte": "^4.2.19",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/cloudflare": "^11.2.0",
|
||||
"@astrojs/node": "^8.3.4",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^20.11.19",
|
||||
"eslint": "^10.0.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-astro": "^1.6.0",
|
||||
"eslint-plugin-svelte": "^3.15.0",
|
||||
"globals": "^17.3.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-svelte": "^3.5.0",
|
||||
"sass": "^1.97.3",
|
||||
"svelte-eslint-parser": "^1.5.1",
|
||||
"typescript-eslint": "^8.56.1",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"resolutions": {
|
||||
"braces": ">=3.0.3",
|
||||
"micromatch": ">=4.0.8",
|
||||
"minimatch": ">=3.1.3",
|
||||
"prismjs": ">=1.30.0",
|
||||
"mdast-util-to-hast": ">=13.2.1",
|
||||
"dset": ">=3.1.4",
|
||||
"esbuild": ">=0.25.0",
|
||||
"undici": ">=6.23.0",
|
||||
"lodash": ">=4.17.23",
|
||||
"**/js-yaml": ">=4.1.1",
|
||||
"brace-expansion": ">=2.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,19 +21,18 @@ const { href, title, body } = Astro.props;
|
|||
</li>
|
||||
<style lang="scss">
|
||||
.link-card {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
border: 2px solid var(--box-outline);
|
||||
box-shadow: 6px 6px 0 var(--box-outline);
|
||||
font-family: "Lekton", sans-serif;
|
||||
font-weight: 700;
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
border: 2px solid var(--box-outline);
|
||||
box-shadow: 6px 6px 0 var(--box-outline);
|
||||
font-family: 'Lekton', sans-serif;
|
||||
font-weight: 700;
|
||||
transition: all ease-in-out 0.1s;
|
||||
list-style: none;
|
||||
&:hover {
|
||||
box-shadow: 8px 8px 0 var(--box-outline);
|
||||
background: var(--accent);
|
||||
color: var(--background);
|
||||
|
||||
}
|
||||
a {
|
||||
box-sizing: border-box;
|
||||
|
|
@ -45,5 +44,4 @@ const { href, title, body } = Astro.props;
|
|||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
---
|
||||
|
||||
<div class="theme-switcher">
|
||||
<ThemeSwitcher client:load />
|
||||
<ThemeSwitcher client:load />
|
||||
</div>
|
||||
|
||||
<div class="hero">
|
||||
<h1>Awesome Privacy</h1>
|
||||
<p class="intro">
|
||||
Your guide to finding and comparing privacy-respecting alternatives to popular software and services.
|
||||
</p>
|
||||
<div class="github-link-wrap">
|
||||
<a href="https://github.com/lissy93/awesome-privacy">
|
||||
<FontAwesome iconName="github" />
|
||||
View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
<h1>Awesome Privacy</h1>
|
||||
<p class="intro">
|
||||
Your guide to finding and comparing privacy-respecting alternatives to
|
||||
popular software and services.
|
||||
</p>
|
||||
<div class="github-link-wrap">
|
||||
<a href="https://github.com/lissy93/awesome-privacy">
|
||||
<FontAwesome iconName="github" />
|
||||
View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="top-right">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/search">Search</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/browse">Browse</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/about">About</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/lissy93/awesome-privacy">Source</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://as93.net">More Apps</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/search">Search</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/browse">Browse</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/about">About</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/lissy93/awesome-privacy">Source</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://as93.net">More Apps</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.hero {
|
||||
color: var(--accent-fg);
|
||||
border-radius: var(--curve-sm);
|
||||
padding: 2rem 4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
@media(max-width: 768px) {
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
color: var(--accent-fg);
|
||||
border-radius: var(--curve-sm);
|
||||
padding: 2rem 4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
@media (max-width: 768px) {
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
}
|
||||
svg {
|
||||
position: absolute;
|
||||
|
|
@ -65,98 +64,97 @@ import ThemeSwitcher from "@components/form/ThemeSwitcher.svelte"
|
|||
width: 220px;
|
||||
height: auto;
|
||||
opacity: 0.6;
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
margin: 0;
|
||||
font-size: 5rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
font-family: 'Libre Franklin', sans-serif;
|
||||
color: var(--accent-3);
|
||||
-webkit-text-fill-color: var(--accent-3);
|
||||
-webkit-text-stroke-width: 2px;
|
||||
-webkit-text-stroke-color: var(--box-outline);
|
||||
text-shadow: 3px 3px 0 var(--box-outline);
|
||||
@media(max-width: 768px) {
|
||||
font-size: 4rem;
|
||||
}
|
||||
font-family: 'Libre Franklin', sans-serif;
|
||||
color: var(--accent-3);
|
||||
-webkit-text-fill-color: var(--accent-3);
|
||||
-webkit-text-stroke-width: 2px;
|
||||
-webkit-text-stroke-color: var(--box-outline);
|
||||
text-shadow: 3px 3px 0 var(--box-outline);
|
||||
@media (max-width: 768px) {
|
||||
font-size: 4rem;
|
||||
}
|
||||
}
|
||||
.intro {
|
||||
text-align: center;
|
||||
font-size: 1.6rem;
|
||||
padding: 0.5rem 1rem;
|
||||
color: var(--accent-fg);
|
||||
background: var(--accent-3);
|
||||
border: 2px solid var(--box-outline);
|
||||
border-radius: var(--curve-sm);
|
||||
box-shadow: 6px 6px 0 var(--box-outline);
|
||||
font-family: 'Lekton', sans-serif;
|
||||
font-weight: 700;
|
||||
max-width: 735px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.github-link-wrap {
|
||||
font-family: 'Lekton', sans-serif;
|
||||
max-width: 735px;
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
border: 1px solid var(--box-outline);
|
||||
box-shadow: 3px 3px 0 var(--box-outline);
|
||||
background: var(--accent);
|
||||
border-radius: 18px;
|
||||
padding: 0.5rem 1rem;
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--accent-fg);
|
||||
font-size: 1.2rem;
|
||||
font-family: 'Lekton', sans-serif;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
:global(svg) {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
color: var(--accent-fg);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.intro {
|
||||
text-align: center;
|
||||
font-size: 1.6rem;
|
||||
padding: 0.5rem 1rem;
|
||||
color: var(--accent-fg);
|
||||
background: var(--accent-3);
|
||||
border: 2px solid var(--box-outline);
|
||||
border-radius: var(--curve-sm);
|
||||
box-shadow: 6px 6px 0 var(--box-outline);
|
||||
font-family: "Lekton", sans-serif;
|
||||
font-weight: 700;
|
||||
max-width: 735px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.github-link-wrap {
|
||||
font-family: "Lekton", sans-serif;
|
||||
max-width: 735px;
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
border: 1px solid var(--box-outline);
|
||||
box-shadow: 3px 3px 0 var(--box-outline);
|
||||
background: var(--accent);
|
||||
border-radius: 18px;
|
||||
padding: 0.5rem 1rem;
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--accent-fg);;
|
||||
font-size: 1.2rem;
|
||||
font-family: "Lekton", sans-serif;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
:global(svg) {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
color: var(--accent-fg);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme-switcher {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
}
|
||||
.theme-switcher {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
.top-right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 1rem;
|
||||
opacity: 0.8;
|
||||
display: none;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
gap: 0.5rem;
|
||||
li {
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid var(--accent);
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
li a {
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 1rem;
|
||||
opacity: 0.8;
|
||||
display: none;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
gap: 0.5rem;
|
||||
li {
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid var(--accent);
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
li a {
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,37 +1,30 @@
|
|||
---
|
||||
|
||||
const {
|
||||
text,
|
||||
url,
|
||||
className,
|
||||
title,
|
||||
} = Astro.props;
|
||||
|
||||
const { text, url, className, title } = Astro.props;
|
||||
---
|
||||
|
||||
<div class={`button ${className || ''}`} title={title}>
|
||||
<a href={url}>{text}<slot /></a>
|
||||
<a href={url}>{text}<slot /></a>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.button {
|
||||
font-family: "Lekton", sans-serif;
|
||||
text-align: center;
|
||||
border: 1px solid var(--box-outline);
|
||||
box-shadow: 3px 3px 0 var(--box-outline);
|
||||
background: var(--accent);
|
||||
border-radius: 18px;
|
||||
padding: 0.5rem 1rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
box-shadow: 4px 4px 0 var(--box-outline);
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--accent-fg);;
|
||||
font-size: 1.2rem;
|
||||
font-family: "Lekton", sans-serif;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.button {
|
||||
font-family: 'Lekton', sans-serif;
|
||||
text-align: center;
|
||||
border: 1px solid var(--box-outline);
|
||||
box-shadow: 3px 3px 0 var(--box-outline);
|
||||
background: var(--accent);
|
||||
border-radius: 18px;
|
||||
padding: 0.5rem 1rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
box-shadow: 4px 4px 0 var(--box-outline);
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--accent-fg);
|
||||
font-size: 1.2rem;
|
||||
font-family: 'Lekton', sans-serif;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -41,25 +41,27 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:click={handleClickOutside}/>
|
||||
<svelte:window on:click={handleClickOutside} />
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<div>
|
||||
<h2
|
||||
contenteditable={true}
|
||||
class:editable={editing}
|
||||
on:click={() => editing = true}
|
||||
on:keydown={handleKeydown}
|
||||
on:blur={() => saveTitle(title)}
|
||||
tabindex="0"
|
||||
>{title}</h2>
|
||||
<h2
|
||||
contenteditable={true}
|
||||
class:editable={editing}
|
||||
on:click={() => (editing = true)}
|
||||
on:keydown={handleKeydown}
|
||||
on:blur={() => saveTitle(title)}
|
||||
tabindex="0"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<small>Click the title, to edit your inventory name</small>
|
||||
<small>Click the title, to edit your inventory name</small>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
h2 {
|
||||
font-family: "Lekton", sans-serif;
|
||||
font-family: 'Lekton', sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 3rem;
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
import * as brands from '@fortawesome/free-brands-svg-icons';
|
||||
import type { IconDefinition } from '@fortawesome/fontawesome-svg-core';
|
||||
|
||||
|
||||
export const iconMap: Record<string, IconDefinition> = {
|
||||
// Branding
|
||||
logo: solidIcons.faEyeSlash,
|
||||
|
|
@ -83,13 +82,8 @@
|
|||
};
|
||||
|
||||
export let iconName: string;
|
||||
|
||||
</script>
|
||||
|
||||
{#if iconMap[iconName]}
|
||||
<FontAwesomeIcon
|
||||
class="fa-icon"
|
||||
icon={iconMap[iconName]} />
|
||||
<FontAwesomeIcon class="fa-icon" icon={iconMap[iconName]} />
|
||||
{/if}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,73 +1,84 @@
|
|||
---
|
||||
|
||||
interface IconProps {
|
||||
icon: string;
|
||||
color?: string;
|
||||
class?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
icon: string;
|
||||
color?: string;
|
||||
class?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const getSvgPath = (icon: string) => {
|
||||
switch (icon) {
|
||||
case 'star':
|
||||
return {
|
||||
vb: "0 0 24 24",
|
||||
path: "M10 15l-5.5 3 1-5.5L0 7.5l5.6-0.5L10 2l2 5 5.5 0.5-4 4 1 5.5z",
|
||||
};
|
||||
case 'mastodon':
|
||||
return {
|
||||
vb: "0 0 512 512",
|
||||
path: "M433 179.1c0-97.2-63.7-125.7-63.7-125.7-62.5-28.7-228.6-28.4-290.5 0 0 0-63.7 28.5-63.7 125.7 0 115.7-6.6 259.4 105.6 289.1 40.5 10.7 75.3 13 103.3 11.4 50.8-2.8 79.3-18.1 79.3-18.1l-1.7-36.9s-36.3 11.4-77.1 10.1c-40.4-1.4-83-4.4-89.6-54a102.5 102.5 0 0 1 -.9-13.9c85.6 20.9 158.7 9.1 178.8 6.7 56.1-6.7 105-41.3 111.2-72.9 9.8-49.8 9-121.5 9-121.5zm-75.1 125.2h-46.6v-114.2c0-49.7-64-51.6-64 6.9v62.5h-46.3V197c0-58.5-64-56.6-64-6.9v114.2H90.2c0-122.1-5.2-147.9 18.4-175 25.9-28.9 79.8-30.8 103.8 6.1l11.6 19.5 11.6-19.5c24.1-37.1 78.1-34.8 103.8-6.1 23.7 27.3 18.4 53 18.4 175z",
|
||||
};
|
||||
case 'twitter':
|
||||
return {
|
||||
vb: "0 0 512 512",
|
||||
path: "M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z",
|
||||
};
|
||||
case 'hub':
|
||||
return {
|
||||
vb: "0 0 512 512",
|
||||
path: "M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z",
|
||||
};
|
||||
case 'dev':
|
||||
return {
|
||||
vb: "0 0 512 512",
|
||||
path: "M120.1 208.3c-3.9-2.9-7.8-4.4-11.7-4.4H91v104.5h17.5c3.9 0 7.8-1.5 11.7-4.4 3.9-2.9 5.8-7.3 5.8-13.1v-69.7c0-5.8-2-10.2-5.8-13.1zM404.1 32H43.9C19.7 32 .1 51.6 0 75.8v360.4C.1 460.4 19.7 480 43.9 480h360.2c24.2 0 43.8-19.6 43.9-43.8V75.8c-.1-24.2-19.7-43.8-43.9-43.8zM154.2 291.2c0 18.8-11.6 47.3-48.4 47.3h-46.4V173h47.4c35.4 0 47.4 28.5 47.4 47.3l0 70.9zm100.7-88.7H201.6v38.4h32.6v29.6H201.6v38.4h53.3v29.6h-62.2c-11.2 .3-20.4-8.5-20.7-19.7V193.7c-.3-11.2 8.6-20.4 19.7-20.7h63.2l0 29.5zm103.6 115.3c-13.2 30.8-36.9 24.6-47.4 0l-38.5-144.8h32.6l29.7 113.7 29.6-113.7h32.6l-38.5 144.8z",
|
||||
};
|
||||
case 'linkedin':
|
||||
return {
|
||||
vb: "0 0 512 512",
|
||||
path: "M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z",
|
||||
};
|
||||
case 'essentials':
|
||||
return {
|
||||
vb: "0 0 512 512",
|
||||
path: "M208 176c0-70.7 57.3-128 128-128s128 57.3 128 128s-57.3 128-128 128c-10.4 0-20.5-1.2-30.1-3.6c-8.1-2-16.7 .4-22.6 6.4L254.1 336H200c-13.3 0-24 10.7-24 24v40H136c-13.3 0-24 10.7-24 24v40H48V385.9L205.2 228.7c5.9-5.9 8.3-14.5 6.4-22.6c-2.3-9.6-3.6-19.7-3.6-30.1zM336 0C238.8 0 160 78.8 160 176c0 9.5 .7 18.8 2.2 27.9L7 359c-4.5 4.5-7 10.6-7 17V488c0 13.3 10.7 24 24 24H136c13.3 0 24-10.7 24-24V448h40c13.3 0 24-10.7 24-24V384h40c6.4 0 12.5-2.5 17-7l27.2-27.2c9.1 1.4 18.4 2.2 27.9 2.2c97.2 0 176-78.8 176-176S433.2 0 336 0zm32 176a32 32 0 1 0 0-64 32 32 0 1 0 0 64z",
|
||||
};
|
||||
case 'communication':
|
||||
return {
|
||||
vb: "0 0 640 512",
|
||||
path: "M48 72c0-13.3 10.7-24 24-24H344c13.3 0 24 10.7 24 24V248c0 13.3-10.7 24-24 24H216c-4.7 0-9.4 1.4-13.3 4L144 315.2V296c0-13.3-10.7-24-24-24H72c-13.3 0-24-10.7-24-24V72zM72 0C32.2 0 0 32.2 0 72V248c0 39.8 32.2 72 72 72H96v40c0 8.9 4.9 17 12.7 21.2s17.3 3.7 24.6-1.2l90-60H344c39.8 0 72-32.2 72-72V72c0-39.8-32.2-72-72-72H72zM256 376c0 39.8 32.2 72 72 72h88.7l90 60c7.4 4.9 16.8 5.4 24.6 1.2S544 496.9 544 488V448h24c39.8 0 72-32.2 72-72V200c0-39.8-32.2-72-72-72H448v48H568c13.3 0 24 10.7 24 24V376c0 13.3-10.7 24-24 24H520c-13.3 0-24 10.7-24 24v19.2L437.3 404c-3.9-2.6-8.6-4-13.3-4H328c-13.3 0-24-10.7-24-24V352H256v24z",
|
||||
};
|
||||
case 'security-tools':
|
||||
return {
|
||||
vp: "0 0 512 512",
|
||||
path: "M232 60.8V447.4c-66.9-37.8-108.8-94.3-134.1-152.6C71 232.9 63.1 169.5 64.1 126L232 60.8zm48 386.5V60.8L448 126c1 43.5-6.9 106.9-33.8 168.8C388.8 353.1 346.9 409.5 280 447.3zM495.5 113l-1.2-20.5L475.1 85 267.6 4.5 256 0 244.4 4.5 36.9 85 17.8 92.5 16.6 113c-2.9 49.9 4.9 126.3 37.3 200.9c32.7 75.2 91 150 189.4 192.6L256 512l12.7-5.5c98.4-42.6 156.7-117.3 189.4-192.6c32.4-74.7 40.2-151 37.3-200.9z"
|
||||
};
|
||||
// Add more icons as needed...
|
||||
default:
|
||||
return { vb: "", path: "" }; // Default path or a placeholder icon
|
||||
}
|
||||
switch (icon) {
|
||||
case 'star':
|
||||
return {
|
||||
vb: '0 0 24 24',
|
||||
path: 'M10 15l-5.5 3 1-5.5L0 7.5l5.6-0.5L10 2l2 5 5.5 0.5-4 4 1 5.5z',
|
||||
};
|
||||
case 'mastodon':
|
||||
return {
|
||||
vb: '0 0 512 512',
|
||||
path: 'M433 179.1c0-97.2-63.7-125.7-63.7-125.7-62.5-28.7-228.6-28.4-290.5 0 0 0-63.7 28.5-63.7 125.7 0 115.7-6.6 259.4 105.6 289.1 40.5 10.7 75.3 13 103.3 11.4 50.8-2.8 79.3-18.1 79.3-18.1l-1.7-36.9s-36.3 11.4-77.1 10.1c-40.4-1.4-83-4.4-89.6-54a102.5 102.5 0 0 1 -.9-13.9c85.6 20.9 158.7 9.1 178.8 6.7 56.1-6.7 105-41.3 111.2-72.9 9.8-49.8 9-121.5 9-121.5zm-75.1 125.2h-46.6v-114.2c0-49.7-64-51.6-64 6.9v62.5h-46.3V197c0-58.5-64-56.6-64-6.9v114.2H90.2c0-122.1-5.2-147.9 18.4-175 25.9-28.9 79.8-30.8 103.8 6.1l11.6 19.5 11.6-19.5c24.1-37.1 78.1-34.8 103.8-6.1 23.7 27.3 18.4 53 18.4 175z',
|
||||
};
|
||||
case 'twitter':
|
||||
return {
|
||||
vb: '0 0 512 512',
|
||||
path: 'M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z',
|
||||
};
|
||||
case 'hub':
|
||||
return {
|
||||
vb: '0 0 512 512',
|
||||
path: 'M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z',
|
||||
};
|
||||
case 'dev':
|
||||
return {
|
||||
vb: '0 0 512 512',
|
||||
path: 'M120.1 208.3c-3.9-2.9-7.8-4.4-11.7-4.4H91v104.5h17.5c3.9 0 7.8-1.5 11.7-4.4 3.9-2.9 5.8-7.3 5.8-13.1v-69.7c0-5.8-2-10.2-5.8-13.1zM404.1 32H43.9C19.7 32 .1 51.6 0 75.8v360.4C.1 460.4 19.7 480 43.9 480h360.2c24.2 0 43.8-19.6 43.9-43.8V75.8c-.1-24.2-19.7-43.8-43.9-43.8zM154.2 291.2c0 18.8-11.6 47.3-48.4 47.3h-46.4V173h47.4c35.4 0 47.4 28.5 47.4 47.3l0 70.9zm100.7-88.7H201.6v38.4h32.6v29.6H201.6v38.4h53.3v29.6h-62.2c-11.2 .3-20.4-8.5-20.7-19.7V193.7c-.3-11.2 8.6-20.4 19.7-20.7h63.2l0 29.5zm103.6 115.3c-13.2 30.8-36.9 24.6-47.4 0l-38.5-144.8h32.6l29.7 113.7 29.6-113.7h32.6l-38.5 144.8z',
|
||||
};
|
||||
case 'linkedin':
|
||||
return {
|
||||
vb: '0 0 512 512',
|
||||
path: 'M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z',
|
||||
};
|
||||
case 'essentials':
|
||||
return {
|
||||
vb: '0 0 512 512',
|
||||
path: 'M208 176c0-70.7 57.3-128 128-128s128 57.3 128 128s-57.3 128-128 128c-10.4 0-20.5-1.2-30.1-3.6c-8.1-2-16.7 .4-22.6 6.4L254.1 336H200c-13.3 0-24 10.7-24 24v40H136c-13.3 0-24 10.7-24 24v40H48V385.9L205.2 228.7c5.9-5.9 8.3-14.5 6.4-22.6c-2.3-9.6-3.6-19.7-3.6-30.1zM336 0C238.8 0 160 78.8 160 176c0 9.5 .7 18.8 2.2 27.9L7 359c-4.5 4.5-7 10.6-7 17V488c0 13.3 10.7 24 24 24H136c13.3 0 24-10.7 24-24V448h40c13.3 0 24-10.7 24-24V384h40c6.4 0 12.5-2.5 17-7l27.2-27.2c9.1 1.4 18.4 2.2 27.9 2.2c97.2 0 176-78.8 176-176S433.2 0 336 0zm32 176a32 32 0 1 0 0-64 32 32 0 1 0 0 64z',
|
||||
};
|
||||
case 'communication':
|
||||
return {
|
||||
vb: '0 0 640 512',
|
||||
path: 'M48 72c0-13.3 10.7-24 24-24H344c13.3 0 24 10.7 24 24V248c0 13.3-10.7 24-24 24H216c-4.7 0-9.4 1.4-13.3 4L144 315.2V296c0-13.3-10.7-24-24-24H72c-13.3 0-24-10.7-24-24V72zM72 0C32.2 0 0 32.2 0 72V248c0 39.8 32.2 72 72 72H96v40c0 8.9 4.9 17 12.7 21.2s17.3 3.7 24.6-1.2l90-60H344c39.8 0 72-32.2 72-72V72c0-39.8-32.2-72-72-72H72zM256 376c0 39.8 32.2 72 72 72h88.7l90 60c7.4 4.9 16.8 5.4 24.6 1.2S544 496.9 544 488V448h24c39.8 0 72-32.2 72-72V200c0-39.8-32.2-72-72-72H448v48H568c13.3 0 24 10.7 24 24V376c0 13.3-10.7 24-24 24H520c-13.3 0-24 10.7-24 24v19.2L437.3 404c-3.9-2.6-8.6-4-13.3-4H328c-13.3 0-24-10.7-24-24V352H256v24z',
|
||||
};
|
||||
case 'security-tools':
|
||||
return {
|
||||
vp: '0 0 512 512',
|
||||
path: 'M232 60.8V447.4c-66.9-37.8-108.8-94.3-134.1-152.6C71 232.9 63.1 169.5 64.1 126L232 60.8zm48 386.5V60.8L448 126c1 43.5-6.9 106.9-33.8 168.8C388.8 353.1 346.9 409.5 280 447.3zM495.5 113l-1.2-20.5L475.1 85 267.6 4.5 256 0 244.4 4.5 36.9 85 17.8 92.5 16.6 113c-2.9 49.9 4.9 126.3 37.3 200.9c32.7 75.2 91 150 189.4 192.6L256 512l12.7-5.5c98.4-42.6 156.7-117.3 189.4-192.6c32.4-74.7 40.2-151 37.3-200.9z',
|
||||
};
|
||||
// Add more icons as needed...
|
||||
default:
|
||||
return { vb: '', path: '' }; // Default path or a placeholder icon
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Props are defined in the component's signature
|
||||
const { icon, color = 'currentcolor', class: className = '', width = 80, height = 50 } = Astro.props as IconProps;
|
||||
const {
|
||||
icon,
|
||||
color = 'currentcolor',
|
||||
class: className = '',
|
||||
width = 80,
|
||||
height = 50,
|
||||
} = Astro.props as IconProps;
|
||||
const svgStyle = { fill: color };
|
||||
const { vb, path } = getSvgPath(icon);
|
||||
---
|
||||
|
||||
<svg class={className} style={svgStyle} xmlns="http://www.w3.org/2000/svg" viewBox={vb} width={width} height={height ?? width}>
|
||||
<path d={path} />
|
||||
<svg
|
||||
class={className}
|
||||
style={svgStyle}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox={vb}
|
||||
width={width}
|
||||
height={height ?? width}
|
||||
>
|
||||
<path d={path}></path>
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
---
|
||||
|
||||
import Icon from '@components/form/FontAwesome.svelte';
|
||||
import { site, title as defaultTitle, description as defaultDescription } from '@utils/config';
|
||||
import {
|
||||
site,
|
||||
title as defaultTitle,
|
||||
description as defaultDescription,
|
||||
} from '@utils/config';
|
||||
|
||||
interface Props {
|
||||
url?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
url?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const url = Astro.props.url || site;
|
||||
|
|
@ -18,109 +21,114 @@ const encodedTitle = encodeURIComponent(title);
|
|||
const encodedDescription = encodeURIComponent(description);
|
||||
|
||||
const socialMedias = {
|
||||
mastodon: {
|
||||
url: `https://mastodon.social/share?text=${encodeURIComponent(`${title} ${description}`)}&url=${encodedUrl}`,
|
||||
title: 'Mastodon',
|
||||
icon: 'mastodon',
|
||||
color: '#6364FF',
|
||||
},
|
||||
twitter: {
|
||||
url: `https://twitter.com/intent/tweet?text=${encodeURIComponent(`${title} ${description}`)}&url=${url}`,
|
||||
title: 'Twitter',
|
||||
icon: 'twitter',
|
||||
color: '#444343'
|
||||
},
|
||||
reddit: {
|
||||
url: `https://reddit.com/submit?url=${encodedUrl}&title=${encodedTitle}`,
|
||||
title: 'Reddit',
|
||||
icon: 'reddit',
|
||||
color: '#FF4500',
|
||||
},
|
||||
linkedIn: {
|
||||
url: `https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}&summary=${encodedDescription}`,
|
||||
title: 'LinkedIn',
|
||||
icon: 'linkedin',
|
||||
color: '#0A66C2'
|
||||
},
|
||||
pinterest: {
|
||||
url: `https://pinterest.com/pin/create/button/?url=${encodedUrl}&description=${encodedTitle}`,
|
||||
title: 'Pinterest',
|
||||
icon: 'pinterest',
|
||||
color: '#BD081C',
|
||||
},
|
||||
telegram: {
|
||||
url: `https://t.me/share/url?url=${encodedUrl}&text=${encodeURIComponent(`${title} ${description}`)}`,
|
||||
title: 'Telegram',
|
||||
icon: 'telegram',
|
||||
color: '#26A5E4',
|
||||
},
|
||||
whatsapp: {
|
||||
url: `https://wa.me/?text=${encodedTitle} ${encodedUrl}`,
|
||||
title: 'WhatsApp',
|
||||
icon: 'whatsapp',
|
||||
color: '#25D366',
|
||||
},
|
||||
signal: {
|
||||
url: `https://signal.me/#p/+${encodedUrl}`,
|
||||
title: 'Signal',
|
||||
icon: 'signal',
|
||||
color: '#3A76F0',
|
||||
},
|
||||
pocket: {
|
||||
url: `https://getpocket.com/save?url=${encodedUrl}&title=${encodedTitle}`,
|
||||
title: 'Pocket',
|
||||
icon: 'pocket',
|
||||
color: '#EF3F56',
|
||||
},
|
||||
mastodon: {
|
||||
url: `https://mastodon.social/share?text=${encodeURIComponent(`${title} ${description}`)}&url=${encodedUrl}`,
|
||||
title: 'Mastodon',
|
||||
icon: 'mastodon',
|
||||
color: '#6364FF',
|
||||
},
|
||||
twitter: {
|
||||
url: `https://twitter.com/intent/tweet?text=${encodeURIComponent(`${title} ${description}`)}&url=${url}`,
|
||||
title: 'Twitter',
|
||||
icon: 'twitter',
|
||||
color: '#444343',
|
||||
},
|
||||
reddit: {
|
||||
url: `https://reddit.com/submit?url=${encodedUrl}&title=${encodedTitle}`,
|
||||
title: 'Reddit',
|
||||
icon: 'reddit',
|
||||
color: '#FF4500',
|
||||
},
|
||||
linkedIn: {
|
||||
url: `https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}&summary=${encodedDescription}`,
|
||||
title: 'LinkedIn',
|
||||
icon: 'linkedin',
|
||||
color: '#0A66C2',
|
||||
},
|
||||
pinterest: {
|
||||
url: `https://pinterest.com/pin/create/button/?url=${encodedUrl}&description=${encodedTitle}`,
|
||||
title: 'Pinterest',
|
||||
icon: 'pinterest',
|
||||
color: '#BD081C',
|
||||
},
|
||||
telegram: {
|
||||
url: `https://t.me/share/url?url=${encodedUrl}&text=${encodeURIComponent(`${title} ${description}`)}`,
|
||||
title: 'Telegram',
|
||||
icon: 'telegram',
|
||||
color: '#26A5E4',
|
||||
},
|
||||
whatsapp: {
|
||||
url: `https://wa.me/?text=${encodedTitle} ${encodedUrl}`,
|
||||
title: 'WhatsApp',
|
||||
icon: 'whatsapp',
|
||||
color: '#25D366',
|
||||
},
|
||||
signal: {
|
||||
url: `https://signal.me/#p/+${encodedUrl}`,
|
||||
title: 'Signal',
|
||||
icon: 'signal',
|
||||
color: '#3A76F0',
|
||||
},
|
||||
pocket: {
|
||||
url: `https://getpocket.com/save?url=${encodedUrl}&title=${encodedTitle}`,
|
||||
title: 'Pocket',
|
||||
icon: 'pocket',
|
||||
color: '#EF3F56',
|
||||
},
|
||||
};
|
||||
|
||||
---
|
||||
|
||||
<ul class="social-share">
|
||||
{Object.entries(socialMedias).map(([platform, shareUrl]) => (
|
||||
<li style={`--color: ${shareUrl.color}`}>
|
||||
<a title={`Share on ${platform}`} href={shareUrl.url} target="_blank" rel="noopener noreferrer">
|
||||
<Icon iconName={shareUrl.icon} />
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
{
|
||||
Object.entries(socialMedias).map(([platform, shareUrl]) => (
|
||||
<li style={`--color: ${shareUrl.color}`}>
|
||||
<a
|
||||
title={`Share on ${platform}`}
|
||||
href={shareUrl.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon iconName={shareUrl.icon} />
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
|
||||
<style lang="scss">
|
||||
.social-share {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
opacity: 0.8;
|
||||
border: 1px solid var(--box-outline);
|
||||
box-shadow: 2px 2px 0 var(--box-outline);
|
||||
border-radius: var(--curve-sm);
|
||||
transition: all 0.2s ease-in-out;
|
||||
background: var(--background-form);
|
||||
a {
|
||||
display: flex;
|
||||
color: var(--foreground);
|
||||
transition: all 0.2s ease-in-out;
|
||||
padding: 4px;
|
||||
:global(svg) {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
box-shadow: 3px 3px 0 var(--box-outline);
|
||||
border-radius: var(--curve-md);
|
||||
opacity: 1;
|
||||
a {
|
||||
color: var(--color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.social-share {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
opacity: 0.8;
|
||||
border: 1px solid var(--box-outline);
|
||||
box-shadow: 2px 2px 0 var(--box-outline);
|
||||
border-radius: var(--curve-sm);
|
||||
transition: all 0.2s ease-in-out;
|
||||
background: var(--background-form);
|
||||
a {
|
||||
display: flex;
|
||||
color: var(--foreground);
|
||||
transition: all 0.2s ease-in-out;
|
||||
padding: 4px;
|
||||
:global(svg) {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
box-shadow: 3px 3px 0 var(--box-outline);
|
||||
border-radius: var(--curve-md);
|
||||
opacity: 1;
|
||||
a {
|
||||
color: var(--color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="theme-switcher" on:click={toggleTheme}>
|
||||
|
|
@ -38,7 +37,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.theme-switcher {
|
||||
cursor: pointer;
|
||||
|
|
@ -52,7 +50,7 @@
|
|||
transition: background-color 0.3s ease;
|
||||
border: 2px solid var(--box-outline);
|
||||
box-shadow: 3px 3px 0 var(--box-outline);
|
||||
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
|
@ -96,5 +94,4 @@
|
|||
display: flex;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,29 @@
|
|||
---
|
||||
const year = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<footer>
|
||||
<a href="/about">Awesome Privacy</a> is licensed
|
||||
under <a href="https://github.com/Lissy93/awesome-privacy/blob/main/LICENSE">MIT</a>
|
||||
© <a href="https://aliciasykes.com">Alicia Sykes</a> 2024 |
|
||||
Source code available on <a href="https://github.com/Lissy93/awesome-privacy">GitHub</a>
|
||||
<a href="/about">Awesome Privacy</a> is licensed under <a
|
||||
href="https://github.com/Lissy93/awesome-privacy/blob/main/LICENSE"
|
||||
>CC0 1.0 Universal</a
|
||||
>
|
||||
© <a href="https://aliciasykes.com">Alicia Sykes</a> 2018 - {
|
||||
year || 'Present'
|
||||
} | Source code available on <a
|
||||
href="https://github.com/Lissy93/awesome-privacy">GitHub</a
|
||||
>
|
||||
</footer>
|
||||
|
||||
<style lang="scss">
|
||||
footer {
|
||||
font-family: "Lekton", sans-serif;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0.5rem 0;
|
||||
margin: 0 auto;
|
||||
a {
|
||||
font-family: "Lekton", sans-serif;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
footer {
|
||||
font-family: 'Lekton', sans-serif;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 0.5rem 0;
|
||||
margin: 0 auto;
|
||||
a {
|
||||
font-family: 'Lekton', sans-serif;
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
|
||||
|
||||
<main>
|
||||
<slot />
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
@ -11,8 +9,8 @@
|
|||
width: 1200px;
|
||||
max-width: calc(100% - 5rem);
|
||||
border: 2px solid var(--box-outline);
|
||||
box-shadow: 6px 6px 0 var(--box-outline);
|
||||
box-shadow: 6px 6px 0 var(--box-outline);
|
||||
background: var(--accent-fg);
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
---
|
||||
|
||||
<div class="nav">
|
||||
<a href="/" class="homepage">
|
||||
<FontAwesome iconName="logo" />
|
||||
<h1>Awesome Privacy</h1>
|
||||
</a>
|
||||
<nav>
|
||||
<a href="/browse">Browse</a>
|
||||
<a href="/search">Search</a>
|
||||
<a href="/about">About</a>
|
||||
<a href="https://github.com/lissy93/awesome-privacy">GitHub</a>
|
||||
<div class="theme-switcher">
|
||||
<ThemeSwitcher client:load />
|
||||
</div>
|
||||
</nav>
|
||||
<a href="/" class="homepage">
|
||||
<FontAwesome iconName="logo" />
|
||||
<h1>Awesome Privacy</h1>
|
||||
</a>
|
||||
<nav>
|
||||
<a href="/browse">Browse</a>
|
||||
<a href="/search">Search</a>
|
||||
<a href="/about">About</a>
|
||||
<a href="https://github.com/lissy93/awesome-privacy">GitHub</a>
|
||||
<div class="theme-switcher">
|
||||
<ThemeSwitcher client:load />
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.nav {
|
||||
background: var(--accent-fg);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
border-bottom: 2px solid var(--accent-3);
|
||||
|
||||
.homepage {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.5rem;
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 2.4rem;
|
||||
padding: 0 1rem;
|
||||
color: var(--foreground);
|
||||
font-family: "Lekton", sans-serif;
|
||||
}
|
||||
:global(svg) {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
color: var(--accent-3);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
&:hover {
|
||||
:global(svg) {
|
||||
color: var(--accent);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
.nav {
|
||||
background: var(--accent-fg);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
border-bottom: 2px solid var(--accent-3);
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 3rem;
|
||||
a {
|
||||
padding: 1rem;
|
||||
font-size: 1.2rem;
|
||||
font-family: "Lekton", sans-serif;
|
||||
font-weight: bold;
|
||||
color: var(--foreground);
|
||||
transition: background 0.3s, transform 0.3s, box-shadow 0.3s;
|
||||
transition-timing-function: ease-in-out;
|
||||
&:hover {
|
||||
background: var(--accent-3);
|
||||
color: var(--accent-fg);
|
||||
border-bottom: 2px solid var(--accent-3);
|
||||
&:nth-child(4n+1) {
|
||||
background-color: var(--accent);
|
||||
}
|
||||
&:nth-child(4n+2) {
|
||||
background-color: var(--accent-2);
|
||||
color: var(--foreground);
|
||||
}
|
||||
&:nth-child(4n+3) {
|
||||
background-color: var(--accent-3);
|
||||
}
|
||||
&:nth-child(4n+4) {
|
||||
background-color: var(--accent-4);
|
||||
color: var(--foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
.theme-switcher {
|
||||
transform: scale(0.7);
|
||||
margin: 0.2rem auto;
|
||||
@media(max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.homepage, nav {
|
||||
@media(max-width: 768px) {
|
||||
margin: 0 auto;
|
||||
a { border: none; }
|
||||
.homepage {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.5rem;
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 2.4rem;
|
||||
padding: 0 1rem;
|
||||
color: var(--foreground);
|
||||
font-family: 'Lekton', sans-serif;
|
||||
}
|
||||
}
|
||||
}
|
||||
:global(svg) {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
color: var(--accent-3);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
&:hover {
|
||||
:global(svg) {
|
||||
color: var(--accent);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 3rem;
|
||||
a {
|
||||
padding: 1rem;
|
||||
font-size: 1.2rem;
|
||||
font-family: 'Lekton', sans-serif;
|
||||
font-weight: bold;
|
||||
color: var(--foreground);
|
||||
transition:
|
||||
background 0.3s,
|
||||
transform 0.3s,
|
||||
box-shadow 0.3s;
|
||||
transition-timing-function: ease-in-out;
|
||||
&:hover {
|
||||
background: var(--accent-3);
|
||||
color: var(--accent-fg);
|
||||
border-bottom: 2px solid var(--accent-3);
|
||||
&:nth-child(4n + 1) {
|
||||
background-color: var(--accent);
|
||||
}
|
||||
&:nth-child(4n + 2) {
|
||||
background-color: var(--accent-2);
|
||||
color: var(--foreground);
|
||||
}
|
||||
&:nth-child(4n + 3) {
|
||||
background-color: var(--accent-3);
|
||||
}
|
||||
&:nth-child(4n + 4) {
|
||||
background-color: var(--accent-4);
|
||||
color: var(--foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
.theme-switcher {
|
||||
transform: scale(0.7);
|
||||
margin: 0.2rem auto;
|
||||
@media (max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.homepage,
|
||||
nav {
|
||||
@media (max-width: 768px) {
|
||||
margin: 0 auto;
|
||||
a {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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<string, any>) => {
|
||||
const filteredObj: Record<string, any> = {};
|
||||
Object.keys(obj).forEach(key => {
|
||||
const filterEmptyValues = (obj: Record<string, unknown>) => {
|
||||
const filteredObj: Record<string, unknown> = {};
|
||||
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 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/an-old-hope.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/yaml.min.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/an-old-hope.min.css"
|
||||
/>
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"
|
||||
></script>
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/yaml.min.js"
|
||||
></script>
|
||||
</svelte:head>
|
||||
|
||||
<p>
|
||||
Before completing this form, you must ensure that the service you are adding aligns
|
||||
with the <a href="/about#creteria">Requirements</a> for Awesome Privacy.
|
||||
Before completing this form, you must ensure that the service you are adding
|
||||
aligns with the <a href="/about#creteria">Requirements</a> for Awesome
|
||||
Privacy.
|
||||
<br />
|
||||
You'll need a GitHub account in order to submit this form.
|
||||
</p>
|
||||
|
||||
<form on:submit|preventDefault={handleSubmit}>
|
||||
|
||||
<h3>Basics</h3>
|
||||
<p class="sub-title-description">
|
||||
All fields here are required.
|
||||
</p>
|
||||
<p class="sub-title-description">All fields here are required.</p>
|
||||
|
||||
<!-- Category Dropdown -->
|
||||
<div class="form-row">
|
||||
<label for="listing-category">Category</label>
|
||||
<select bind:value={$listingCategory} id="listing-category" required autocomplete="off">
|
||||
<select
|
||||
bind:value={$listingCategory}
|
||||
id="listing-category"
|
||||
required
|
||||
autocomplete="off"
|
||||
>
|
||||
<option value="">--Please choose an option--</option>
|
||||
<option value="Essentials">Essentials</option>
|
||||
<option value="Communication">Communication</option>
|
||||
|
|
@ -126,45 +150,78 @@
|
|||
<option value="Creativity">Creativity</option>
|
||||
</select>
|
||||
<p>
|
||||
Choose the top-level category, which should align with
|
||||
the <a href="/browse">one of these</a>.
|
||||
Choose the top-level category, which should align with the <a
|
||||
href="/browse">one of these</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Listing Name -->
|
||||
<div class="form-row">
|
||||
<label for="service-name">Listing Name</label>
|
||||
<input type="text" bind:value={$serviceName} id="service-name" required autocomplete="off">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={$serviceName}
|
||||
id="service-name"
|
||||
required
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p>Enter the name of the app, software or service</p>
|
||||
</div>
|
||||
|
||||
<!-- Listing URL -->
|
||||
<div class="form-row">
|
||||
<label for="service-url">Listing URL</label>
|
||||
<input type="url" bind:value={$serviceUrl} id="service-url" required autocomplete="off">
|
||||
<p>Enter the fully-qualified domain name of the homepage for this listing</p>
|
||||
<input
|
||||
type="url"
|
||||
bind:value={$serviceUrl}
|
||||
id="service-url"
|
||||
required
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p>
|
||||
Enter the fully-qualified domain name of the homepage for this listing
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Listing Icon -->
|
||||
<div class="form-row">
|
||||
<label for="service-icon">Listing Icon</label>
|
||||
<input type="url" bind:value={$serviceIcon} id="service-icon" required autocomplete="off">
|
||||
<p>Paste a URL to a square logo for the service. Dimensions must be no less than 64x64, and no more than 512x512 pixels</p>
|
||||
<input
|
||||
type="url"
|
||||
bind:value={$serviceIcon}
|
||||
id="service-icon"
|
||||
required
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p>
|
||||
Paste a URL to a square logo for the service. Dimensions must be no less
|
||||
than 64x64, and no more than 512x512 pixels
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Listing Description -->
|
||||
<div class="form-row">
|
||||
<label for="service-description">Listing Description</label>
|
||||
<textarea bind:value={$serviceDescription} id="service-description" required autocomplete="off"></textarea>
|
||||
<p>Please provide a description for this listing. Keep it factual and objective. Markdown is supported.</p>
|
||||
<textarea
|
||||
bind:value={$serviceDescription}
|
||||
id="service-description"
|
||||
required
|
||||
autocomplete="off"
|
||||
></textarea>
|
||||
<p>
|
||||
Please provide a description for this listing. Keep it factual and
|
||||
objective. Markdown is supported.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Section 2 -->
|
||||
<h3>Third-Party Referencing</h3>
|
||||
<p class="sub-title-description">
|
||||
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.
|
||||
<br />
|
||||
All fields are optional, but the more information you provide, the better!
|
||||
</p>
|
||||
|
|
@ -172,7 +229,13 @@
|
|||
<!-- GitHub Repository -->
|
||||
<div class="form-row">
|
||||
<label for="service-github">GitHub Repository</label>
|
||||
<input type="text" bind:value={$serviceGithub} id="service-github" required autocomplete="off">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={$serviceGithub}
|
||||
id="service-github"
|
||||
required
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p>
|
||||
Share a link to where the project's source is located.<br />
|
||||
Use the format [user]/[repo] e.g, lissy93/dashy
|
||||
|
|
@ -182,18 +245,29 @@
|
|||
<!-- ToS;DR ID -->
|
||||
<div class="form-row">
|
||||
<label for="service-tosdr-id">ToS;DR ID</label>
|
||||
<input type="number" bind:value={$serviceTosdrId} id="service-tosdr-id" autocomplete="off">
|
||||
<input
|
||||
type="number"
|
||||
bind:value={$serviceTosdrId}
|
||||
id="service-tosdr-id"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p>
|
||||
Has the Privacy policy been documented by <a href="https://tosdr.org/">tosdr.org</a>?
|
||||
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 <a href="https://tosdr.org/"
|
||||
>tosdr.org</a
|
||||
>? If so, please include the report reference below (this is a 3 or
|
||||
4-digit numerical ID). Skip section if not applicable.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Apple App Store URL -->
|
||||
<div class="form-row">
|
||||
<label for="service-tosdr-id">iOS App</label>
|
||||
<input type="url" bind:value={$serviceIosApp} id="service-ios-app" autocomplete="off">
|
||||
<input
|
||||
type="url"
|
||||
bind:value={$serviceIosApp}
|
||||
id="service-ios-app"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p>
|
||||
Paste the link to the mobile app on the Apple App Store.<br />
|
||||
E.g. https://apps.apple.com/us/app/bitwarden-password-manager/id1137397744
|
||||
|
|
@ -203,7 +277,12 @@
|
|||
<!-- Google Play App Store URL -->
|
||||
<div class="form-row">
|
||||
<label for="service-tosdr-id">Android App</label>
|
||||
<input type="url" bind:value={$serviceAndroidApp} id="service-android-app" autocomplete="off">
|
||||
<input
|
||||
type="url"
|
||||
bind:value={$serviceAndroidApp}
|
||||
id="service-android-app"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p>
|
||||
Paste the link to the mobile app on the Google Play Store.<br />
|
||||
E.g. https://play.google.com/store/apps/details?id=com.x8bit.bitwarden
|
||||
|
|
@ -213,17 +292,28 @@
|
|||
<!-- Discord Server Invite Code -->
|
||||
<div class="form-row">
|
||||
<label for="service-tosdr-id">Discord Invite</label>
|
||||
<input type="text" bind:value={$serviceDiscordInvite} id="service-discord-invite" autocomplete="off">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={$serviceDiscordInvite}
|
||||
id="service-discord-invite"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p>
|
||||
Paste the invite code to the Discord server for this service.<br />
|
||||
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
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Reddit sub name -->
|
||||
<div class="form-row">
|
||||
<label for="service-tosdr-id">Subreddit</label>
|
||||
<input type="text" bind:value={$serviceSubreddit} id="service-subreddit" autocomplete="off">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={$serviceSubreddit}
|
||||
id="service-subreddit"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p>
|
||||
If the service has a subreddit, please provide the name here.<br />
|
||||
Don't include `r/` in the name, nor the full URL - just the sub name.
|
||||
|
|
@ -233,70 +323,95 @@
|
|||
<!-- Section 3 - Checklist and details -->
|
||||
<h3>Privacy Checklist</h3>
|
||||
<p class="sub-title-description">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<!-- Open Source Checkbox -->
|
||||
<!-- Open Source Checkbox -->
|
||||
<div class="form-row">
|
||||
<label for="service-open-source">Is Open Source?</label>
|
||||
<input type="checkbox" bind:checked={$serviceOpenSource} id="service-open-source">
|
||||
<p>Is this service fully open source? Aka, can it be compiled from source by the user, or self-hosted?</p>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={$serviceOpenSource}
|
||||
id="service-open-source"
|
||||
/>
|
||||
<p>
|
||||
Is this service fully open source? Aka, can it be compiled from source by
|
||||
the user, or self-hosted?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Security Audited Checkbox -->
|
||||
<div class="form-row">
|
||||
<label for="service-security-audited">Security Audited?</label>
|
||||
<input type="checkbox" bind:checked={$serviceSecurityAudited} id="service-security-audited">
|
||||
<p>Has this service been independently security audited by an accredited auditor?</p>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={$serviceSecurityAudited}
|
||||
id="service-security-audited"
|
||||
/>
|
||||
<p>
|
||||
Has this service been independently security audited by an accredited
|
||||
auditor?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Accepts Crypto Checkbox -->
|
||||
<div class="form-row">
|
||||
<label for="service-crypto">Accepts Anon Payment?</label>
|
||||
<input type="checkbox" bind:checked={$serviceCrypto} id="service-crypto">
|
||||
<p>If this is a hosted and paid for service, does it accept anonymous payment methods, including crypto (e.g., Monero)?</p>
|
||||
<input type="checkbox" bind:checked={$serviceCrypto} id="service-crypto" />
|
||||
<p>
|
||||
If this is a hosted and paid for service, does it accept anonymous payment
|
||||
methods, including crypto (e.g., Monero)?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="final-info">
|
||||
<p>
|
||||
Finally, please provide any supporting material, including:
|
||||
</p>
|
||||
<p>Finally, please provide any supporting material, including:</p>
|
||||
<ul>
|
||||
<li>
|
||||
A justification of why this app/service should be included in the list
|
||||
</li>
|
||||
<li>Links to any published security audit, if they exist</li>
|
||||
<li>
|
||||
Links to any published security audit, if they exist
|
||||
Links to the services privacy policy, terms of service and other
|
||||
relevant documents where applicable
|
||||
</li>
|
||||
<li>
|
||||
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
|
||||
</li>
|
||||
<li>
|
||||
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
|
||||
</li>
|
||||
<li>Links to relevant discussions, past issues/PRs related to this service</li>
|
||||
</ul>
|
||||
<textarea bind:value={$additionalInfo} id="additional-info" rows="5"></textarea>
|
||||
<textarea bind:value={$additionalInfo} id="additional-info" rows="5"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit">Submit</button>
|
||||
<a href={issueUrl} target="_blank" class="open-in-gh">Open in GitHub Issues</a>
|
||||
<a href={issueUrl} target="_blank" class="open-in-gh">Open in GitHub Issues</a
|
||||
>
|
||||
</form>
|
||||
|
||||
<div class="output-yaml">
|
||||
<p>Below is the YAML content, which will be appended to the appropriate section
|
||||
within <a href="github.com/lissy93/awesome-privacy/blob/main/awesome-privacy.yml">awesome-privacy.yml</a>
|
||||
<p>
|
||||
Below is the YAML content, which will be appended to the appropriate section
|
||||
within <a
|
||||
href="github.com/lissy93/awesome-privacy/blob/main/awesome-privacy.yml"
|
||||
>awesome-privacy.yml</a
|
||||
>
|
||||
upon approval.
|
||||
</p>
|
||||
{#if !interactiveActivated || !codeBlock}
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -- yamlText is generated from user form input via yaml.dump, not arbitrary HTML -->
|
||||
<pre><code class="language-yaml">{@html yamlText}</code></pre>
|
||||
{/if}
|
||||
<pre><code bind:this={codeBlock} class="language-yaml"></code></pre>
|
||||
<p>Your submission will need to be reviewed by a maintainer and the community before it can be merged.</p>
|
||||
<p>
|
||||
Your submission will need to be reviewed by a maintainer and the community
|
||||
before it can be merged.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
@ -332,7 +447,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
border: 1px solid var(--accent-3);
|
||||
border-radius: var(--curve-md);
|
||||
|
|
@ -347,15 +464,15 @@
|
|||
}
|
||||
input {
|
||||
height: fit-content;
|
||||
&[type="number"]::-webkit-outer-spin-button,
|
||||
&[type="number"]::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
&[type='number']::-webkit-outer-spin-button,
|
||||
&[type='number']::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
&[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
&[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
&[type="checkbox"] {
|
||||
&[type='checkbox'] {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background: var(--background-form);
|
||||
|
|
@ -381,7 +498,7 @@
|
|||
box-shadow: 3px 3px 0 var(--box-outline);
|
||||
border-radius: var(--curve-lg);
|
||||
font-size: 1.8rem;
|
||||
font-family: "Lekton", sans-serif;
|
||||
font-family: 'Lekton', sans-serif;
|
||||
margin: 1rem auto;
|
||||
display: flex;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
|
|
|||
|
|
@ -1,170 +1,178 @@
|
|||
---
|
||||
|
||||
import type { AndroidInfo } from '@utils/fetch-android-info';
|
||||
import { formatDate, timeAgo } from '@utils/dates-n-stuff';
|
||||
import FontAwesome from "@components/form/FontAwesome.svelte"
|
||||
|
||||
import { formatDate } from '@utils/dates-n-stuff';
|
||||
import FontAwesome from '@components/form/FontAwesome.svelte';
|
||||
|
||||
interface Props {
|
||||
androidData: AndroidInfo;
|
||||
};
|
||||
androidData: AndroidInfo;
|
||||
}
|
||||
|
||||
const { androidData } = Astro.props;
|
||||
|
||||
function permissionToReadable(permission: string): string {
|
||||
return (permission
|
||||
.split('.')
|
||||
.pop() || '')
|
||||
.replace(/_/g, ' ')
|
||||
.toLowerCase()
|
||||
.split(' ')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
return (permission.split('.').pop() || '')
|
||||
.replace(/_/g, ' ')
|
||||
.toLowerCase()
|
||||
.split(' ')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
|
||||
---
|
||||
|
||||
<div class="android-info-wrapper">
|
||||
<div class="left">
|
||||
<h4>Update Info</h4>
|
||||
<ul class="list-table">
|
||||
<li>
|
||||
<span class="lbl">App</span>
|
||||
<span class="val">
|
||||
<!-- <img width="20" src={`https://reports.exodus-privacy.eu.org/en/reports/${androidData.version_code}/icon`} alt="Android Icon" /> -->
|
||||
<a href={`https://play.google.com/store/apps/details?id=${androidData.handle}`}>{androidData.app_name}</a>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="lbl">Creation Date</span>
|
||||
<span class="val">{formatDate(androidData.created)}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="lbl">Last Updated</span>
|
||||
<span class="val">{formatDate(androidData.updated)}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="lbl">Current Version</span>
|
||||
<span class="val">{androidData.version_name}</span>
|
||||
</li>
|
||||
{androidData.creator && (
|
||||
<li>
|
||||
<span class="lbl">Creator</span>
|
||||
<span class="val">{androidData.creator}</span>
|
||||
</li>
|
||||
)}
|
||||
{androidData.downloads && (
|
||||
<li>
|
||||
<span class="lbl">Downloads</span>
|
||||
<span class="val">{androidData.downloads}</span>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
<div class="left">
|
||||
<h4>Update Info</h4>
|
||||
<ul class="list-table">
|
||||
<li>
|
||||
<span class="lbl">App</span>
|
||||
<span class="val">
|
||||
<!-- <img width="20" src={`https://reports.exodus-privacy.eu.org/en/reports/${androidData.version_code}/icon`} alt="Android Icon" /> -->
|
||||
<a
|
||||
href={`https://play.google.com/store/apps/details?id=${androidData.handle}`}
|
||||
>{androidData.app_name}</a
|
||||
>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="lbl">Creation Date</span>
|
||||
<span class="val">{formatDate(androidData.created)}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="lbl">Last Updated</span>
|
||||
<span class="val">{formatDate(androidData.updated)}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="lbl">Current Version</span>
|
||||
<span class="val">{androidData.version_name}</span>
|
||||
</li>
|
||||
{
|
||||
androidData.creator && (
|
||||
<li>
|
||||
<span class="lbl">Creator</span>
|
||||
<span class="val">{androidData.creator}</span>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
{
|
||||
androidData.downloads && (
|
||||
<li>
|
||||
<span class="lbl">Downloads</span>
|
||||
<span class="val">{androidData.downloads}</span>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
|
||||
<h4>Trackers</h4>
|
||||
{(androidData.trackers || []).length === 0 && (
|
||||
<p class="all-good">
|
||||
<FontAwesome iconName="noTrackers" />
|
||||
No trackers found
|
||||
</p>
|
||||
)}
|
||||
<ul class="list">
|
||||
{(androidData.trackers || []).map((track) => (
|
||||
<li title={track.code_signature}>{track.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<h4>Trackers</h4>
|
||||
{
|
||||
(androidData.trackers || []).length === 0 && (
|
||||
<p class="all-good">
|
||||
<FontAwesome iconName="noTrackers" />
|
||||
No trackers found
|
||||
</p>
|
||||
)
|
||||
}
|
||||
<ul class="list">
|
||||
{
|
||||
(androidData.trackers || []).map((track) => (
|
||||
<li title={track.code_signature}>{track.name}</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<h4>Permissions</h4>
|
||||
{(androidData.permissions || []).length === 0 && (
|
||||
<p class="all-good">
|
||||
<FontAwesome iconName="noTrackers" />
|
||||
No permissions required
|
||||
</p>
|
||||
)}
|
||||
<ul class="list">
|
||||
{(androidData.permissions || []).map((perm) => (
|
||||
<li title={perm}>{permissionToReadable(perm)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h4>Permissions</h4>
|
||||
{
|
||||
(androidData.permissions || []).length === 0 && (
|
||||
<p class="all-good">
|
||||
<FontAwesome iconName="noTrackers" />
|
||||
No permissions required
|
||||
</p>
|
||||
)
|
||||
}
|
||||
<ul class="list">
|
||||
{
|
||||
(androidData.permissions || []).map((perm) => (
|
||||
<li title={perm}>{permissionToReadable(perm)}</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.android-info-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
.left, .right {
|
||||
width: calc(50% - 1rem);
|
||||
@media screen and (max-width: 768px){
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.android-info-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
.left,
|
||||
.right {
|
||||
width: calc(50% - 1rem);
|
||||
@media screen and (max-width: 768px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 1rem 0 0 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
h4 {
|
||||
margin: 1rem 0 0 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
img {
|
||||
border-radius: var(--curve-sm);
|
||||
}
|
||||
.list-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
&.list-table {
|
||||
font-size: 0.9rem;
|
||||
padding-left: 0;
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.1rem 0;
|
||||
.lbl {
|
||||
font-weight: 400;
|
||||
}
|
||||
.val {
|
||||
img {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid #5f53f440;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.list {
|
||||
list-style: circle;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.all-good {
|
||||
color: var(--success);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
:global(svg) {
|
||||
width: 1.5rem;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
img {
|
||||
border-radius: var(--curve-sm);
|
||||
}
|
||||
.list-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
&.list-table {
|
||||
font-size: 0.9rem;
|
||||
padding-left: 0;
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.1rem 0;
|
||||
.lbl {
|
||||
font-weight: 400;
|
||||
}
|
||||
.val {
|
||||
img {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid #5f53f440;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.list {
|
||||
list-style: circle;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.all-good {
|
||||
color: var(--success);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
:global(svg) {
|
||||
width: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,25 @@
|
|||
<svelte:head>
|
||||
<script async lang="javascript">
|
||||
var remark_config = {
|
||||
host: 'https://comments.as93.net', site_id: 'awesome-privacy.xyz',
|
||||
components: ['embed'], show_rss_subsription: true, theme: 'dark',
|
||||
};
|
||||
!(function (e, n) {
|
||||
for (var o = 0; o < e.length; o++) {
|
||||
var r = n.createElement('script'), d = n.head || n.body;
|
||||
'noModule' in r ?
|
||||
(r.type = 'module', r.src = remark_config.host + '/web/' + e[o] + '.mjs')
|
||||
: ( r.async = !0, r.defer = !0, r.src = remark_config.host + '/web/' + e[o] + '.js'),
|
||||
d.appendChild(r);
|
||||
}
|
||||
})(remark_config.components || ['embed'], document);
|
||||
|
||||
var remark_config = {
|
||||
host: 'https://comments.as93.net',
|
||||
site_id: 'awesome-privacy.xyz',
|
||||
components: ['embed'],
|
||||
show_rss_subsription: true,
|
||||
theme: 'dark',
|
||||
};
|
||||
!(function (e, n) {
|
||||
for (var o = 0; o < e.length; o++) {
|
||||
var r = n.createElement('script'),
|
||||
d = n.head || n.body;
|
||||
('noModule' in r
|
||||
? ((r.type = 'module'),
|
||||
(r.src = remark_config.host + '/web/' + e[o] + '.mjs'))
|
||||
: ((r.async = !0),
|
||||
(r.defer = !0),
|
||||
(r.src = remark_config.host + '/web/' + e[o] + '.js')),
|
||||
d.appendChild(r));
|
||||
}
|
||||
})(remark_config.components || ['embed'], document);
|
||||
</script>
|
||||
</svelte:head>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { fetchSrcData, makeRemovalRequest, makeEditRequest } from '@utils/data-src-delete-n-edit';
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
fetchSrcData,
|
||||
makeRemovalRequest,
|
||||
makeEditRequest,
|
||||
} from '@utils/data-src-delete-n-edit';
|
||||
import FontAwesome from '@components/form/FontAwesome.svelte';
|
||||
|
||||
export let categoryName: string;
|
||||
export let sectionName: string;
|
||||
export let serviceName: string;
|
||||
|
||||
let lineNumbers: { start: number, end: number } | null = null;
|
||||
let lineNumbers: { start: number; end: number } | null = null;
|
||||
let yamlContent = '';
|
||||
|
||||
const getGitHubSrcFile = () => {
|
||||
if (lineNumbers) {
|
||||
const baseFile = 'https://github.com/lissy93/awesome-privacy/blob/main/awesome-privacy.yml';
|
||||
const baseFile =
|
||||
'https://github.com/lissy93/awesome-privacy/blob/main/awesome-privacy.yml';
|
||||
return `${baseFile}#L${lineNumbers.start}-L${lineNumbers.end}`;
|
||||
}
|
||||
return '';
|
||||
|
|
@ -21,56 +26,68 @@
|
|||
const getIframeSrc = () => {
|
||||
const host = 'https://github-embed.as93.net';
|
||||
const target = encodeURIComponent(getGitHubSrcFile());
|
||||
const opts = 'style=felipec&type=code&showBorder=on&showLineNumbers=on&showFileMeta=on&showFullPath=on&showCopy=on';
|
||||
const opts =
|
||||
'style=felipec&type=code&showBorder=on&showLineNumbers=on&showFileMeta=on&showFullPath=on&showCopy=on';
|
||||
return `${host}/iframe.html?target=${target}&${opts}`;
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
const results = await fetchSrcData(categoryName, sectionName, serviceName);
|
||||
lineNumbers = results.lineNumbers
|
||||
lineNumbers = results.lineNumbers;
|
||||
yamlContent = results.yamlContent;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{#if lineNumbers}
|
||||
<h4>Edit {serviceName} Data</h4>
|
||||
<p>
|
||||
You can view or edit this {serviceName}'s entry in
|
||||
<a href={getGitHubSrcFile()}> this section </a>
|
||||
of <code>awesome-privacy.yml</code> in our GitHub repo.
|
||||
</p>
|
||||
|
||||
<h4>Edit {serviceName} Data</h4>
|
||||
<p>
|
||||
You can view or edit this {serviceName}'s entry in
|
||||
<a href={getGitHubSrcFile()}>
|
||||
this section
|
||||
</a>
|
||||
of <code>awesome-privacy.yml</code> in our GitHub repo.
|
||||
</p>
|
||||
|
||||
<h4>Origin Data</h4>
|
||||
<iframe
|
||||
frameborder="0"
|
||||
scrolling="no"
|
||||
class="yaml-embed"
|
||||
allow="clipboard-write"
|
||||
title="awesome-privacy.yml"
|
||||
src={getIframeSrc()}></iframe>
|
||||
|
||||
<h4>Modify Data</h4>
|
||||
<div class="button-wrap">
|
||||
<a class="button-link" target="_blank"
|
||||
href={makeRemovalRequest(categoryName, sectionName, serviceName, yamlContent)}>
|
||||
<FontAwesome iconName="delete" /> Delete {serviceName}
|
||||
</a>
|
||||
<a class="button-link" target="_blank"
|
||||
href={makeEditRequest(categoryName, sectionName, serviceName, yamlContent)}>
|
||||
<FontAwesome iconName="edit" /> Submit Edit to {serviceName}
|
||||
</a>
|
||||
<a class="button-link" href="/submit">
|
||||
<FontAwesome iconName="add" /> Add alternative
|
||||
</a>
|
||||
</div>
|
||||
<h4>Origin Data</h4>
|
||||
<iframe
|
||||
frameborder="0"
|
||||
scrolling="no"
|
||||
class="yaml-embed"
|
||||
allow="clipboard-write"
|
||||
title="awesome-privacy.yml"
|
||||
src={getIframeSrc()}
|
||||
></iframe>
|
||||
|
||||
<h4>Modify Data</h4>
|
||||
<div class="button-wrap">
|
||||
<a
|
||||
class="button-link"
|
||||
target="_blank"
|
||||
href={makeRemovalRequest(
|
||||
categoryName,
|
||||
sectionName,
|
||||
serviceName,
|
||||
yamlContent,
|
||||
)}
|
||||
>
|
||||
<FontAwesome iconName="delete" /> Delete {serviceName}
|
||||
</a>
|
||||
<a
|
||||
class="button-link"
|
||||
target="_blank"
|
||||
href={makeEditRequest(
|
||||
categoryName,
|
||||
sectionName,
|
||||
serviceName,
|
||||
yamlContent,
|
||||
)}
|
||||
>
|
||||
<FontAwesome iconName="edit" /> Submit Edit to {serviceName}
|
||||
</a>
|
||||
<a class="button-link" href="/submit">
|
||||
<FontAwesome iconName="add" /> Add alternative
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
h4 {
|
||||
font-size: 1.4rem;
|
||||
|
|
@ -85,7 +102,7 @@
|
|||
gap: 1rem;
|
||||
justify-content: center;
|
||||
margin: 1rem auto;
|
||||
@media(max-width: 768px) {
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
|
@ -101,7 +118,7 @@
|
|||
min-width: 15rem;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
font-family: "Lekton",sans-serif;
|
||||
font-family: 'Lekton', sans-serif;
|
||||
font-size: 1.2rem;
|
||||
:global(svg) {
|
||||
width: 1rem;
|
||||
|
|
@ -118,5 +135,4 @@
|
|||
margin: 1rem auto;
|
||||
box-shadow: 3px 3px 0 var(--accent-3);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
|
||||
<script lang="ts">
|
||||
import FontAwesome from '@components/form/FontAwesome.svelte';
|
||||
import { fetchSrcData, makeRemovalRequest } from '@utils/data-src-delete-n-edit';
|
||||
import {
|
||||
fetchSrcData,
|
||||
makeRemovalRequest,
|
||||
} from '@utils/data-src-delete-n-edit';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
|
||||
export let categoryName: string;
|
||||
export let sectionName: string;
|
||||
export let serviceName: string;
|
||||
|
||||
const apYaml = 'https://github.com/lissy93/awesome-privacy/blob/main/awesome-privacy.yml';
|
||||
const apYaml =
|
||||
'https://github.com/lissy93/awesome-privacy/blob/main/awesome-privacy.yml';
|
||||
|
||||
let yamlContent = '';
|
||||
let editLink = apYaml;
|
||||
|
|
@ -19,43 +21,51 @@
|
|||
yamlContent = results.yamlContent;
|
||||
|
||||
const lineNumbers = results.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';
|
||||
editLink = `${yamlLink}${numberRange}`;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<div class="actions">
|
||||
<a title="Edit" target="_blank"
|
||||
href={editLink}>
|
||||
<a title="Edit" target="_blank" href={editLink}>
|
||||
<FontAwesome iconName="edit" />
|
||||
</a>
|
||||
<a title="Delete" target="_blank"
|
||||
href={makeRemovalRequest(categoryName, sectionName, serviceName, yamlContent)}>
|
||||
<a
|
||||
title="Delete"
|
||||
target="_blank"
|
||||
href={makeRemovalRequest(
|
||||
categoryName,
|
||||
sectionName,
|
||||
serviceName,
|
||||
yamlContent,
|
||||
)}
|
||||
>
|
||||
<FontAwesome iconName="delete" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.actions {
|
||||
position: absolute;
|
||||
right: 3.5rem;
|
||||
top: 1rem;
|
||||
width: 2.8rem;
|
||||
gap: 1rem;
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
transition: all 0.2s ease-in-out;
|
||||
a {
|
||||
color: var(--foreground);
|
||||
width: 1rem;
|
||||
.actions {
|
||||
position: absolute;
|
||||
right: 3.5rem;
|
||||
top: 1rem;
|
||||
width: 2.8rem;
|
||||
gap: 1rem;
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
color: var(--accent-3);
|
||||
opacity: 1;
|
||||
a {
|
||||
color: var(--foreground);
|
||||
width: 1rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
color: var(--accent-3);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
||||
---
|
||||
|
||||
<div class="discord-info-wrapper">
|
||||
<h3>Discord</h3>
|
||||
|
||||
<h3>Discord</h3>
|
||||
<ul class="list-table">
|
||||
<li>
|
||||
<span class="lbl">Server Name</span>
|
||||
<span class="val"
|
||||
><img src={discordData.icon} width="16" />{discordData.name}</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="lbl">Member Count</span>
|
||||
<span class="val"
|
||||
>{discordData.memberCount} ({discordData.memberOnlineCount} online)</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="lbl">Initial Channel</span>
|
||||
<span class="val">{discordData.channel}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="lbl">Inviter</span>
|
||||
<span class="val">{discordData.inviter || 'Anon'}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="lbl">Join Link</span>
|
||||
<span class="val"
|
||||
><a href={`https://discord.com/invite/${discordData.inviteCode}`}
|
||||
>discord.com/invite/{discordData.inviteCode}</a
|
||||
></span
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="list-table">
|
||||
<li>
|
||||
<span class="lbl">Server Name</span>
|
||||
<span class="val"><img src={discordData.icon} width="16" />{discordData.name}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="lbl">Member Count</span>
|
||||
<span class="val">{discordData.memberCount} ({discordData.memberOnlineCount} online)</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="lbl">Initial Channel</span>
|
||||
<span class="val">{discordData.channel}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="lbl">Inviter</span>
|
||||
<span class="val">{discordData.inviter || 'Anon'}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="lbl">Join Link</span>
|
||||
<span class="val"><a href={`https://discord.com/invite/${discordData.inviteCode}`}>discord.com/invite/{discordData.inviteCode}</a></span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{ discordData.banner && (<img class="banner" width="300" src={discordData.banner} />)}
|
||||
|
||||
|
||||
{
|
||||
discordData.banner && (
|
||||
<img class="banner" width="300" src={discordData.banner} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.discord-info-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 400px;
|
||||
}
|
||||
.discord-info-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 1rem 0 0 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
h4 {
|
||||
margin: 1rem 0 0 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
img {
|
||||
border-radius: var(--curve-sm);
|
||||
}
|
||||
.list-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
&.list-table {
|
||||
font-size: 0.9rem;
|
||||
padding-left: 0;
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.1rem 0;
|
||||
.lbl {
|
||||
font-weight: 400;
|
||||
}
|
||||
.val {
|
||||
img {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid #5f53f440;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.banner {
|
||||
width: 80%;
|
||||
margin: 1rem auto 0 auto;
|
||||
display: flex;
|
||||
border-radius: var(--curve-md);
|
||||
}
|
||||
ul {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
img {
|
||||
border-radius: var(--curve-sm);
|
||||
}
|
||||
.list-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
&.list-table {
|
||||
font-size: 0.9rem;
|
||||
padding-left: 0;
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.1rem 0;
|
||||
.lbl {
|
||||
font-weight: 400;
|
||||
}
|
||||
.val {
|
||||
img {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid #5f53f440;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.banner {
|
||||
width: 80%;
|
||||
margin: 1rem auto 0 auto;
|
||||
display: flex;
|
||||
border-radius: var(--curve-md);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,37 +1,39 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { slugify } from '@utils/fetch-data';
|
||||
|
||||
import { slugify } from "@utils/fetch-data";
|
||||
let linkId = '';
|
||||
let done = false;
|
||||
let error = false;
|
||||
|
||||
let linkId = '';
|
||||
let done = false;
|
||||
let error = false;
|
||||
|
||||
const save = async () => {
|
||||
const savedServices = JSON.parse(localStorage.getItem('savedServices') || '[]');
|
||||
const inventoryTitle = localStorage.getItem('userTitle') || 'Anon\'s Inventory';
|
||||
const uniqueId = Math.random().toString(36).substring(2);
|
||||
const saveKey = `${uniqueId}_${slugify(inventoryTitle)}`;
|
||||
const url = 'https://awesome-privacy-share-api.as93.net';
|
||||
const data = { key: saveKey, services: savedServices };
|
||||
fetch(url, {
|
||||
const save = async () => {
|
||||
const savedServices = JSON.parse(
|
||||
localStorage.getItem('savedServices') || '[]',
|
||||
);
|
||||
const inventoryTitle =
|
||||
localStorage.getItem('userTitle') || "Anon's Inventory";
|
||||
const uniqueId = Math.random().toString(36).substring(2);
|
||||
const saveKey = `${uniqueId}_${slugify(inventoryTitle)}`;
|
||||
const url = 'https://awesome-privacy-share-api.as93.net';
|
||||
const data = { key: saveKey, services: savedServices };
|
||||
fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
linkId = data.key;
|
||||
done = true;
|
||||
error = false;
|
||||
navigator.clipboard.writeText(`https://awesome-privacy.xyz/inventory/${linkId}`);
|
||||
})
|
||||
.catch(error => {
|
||||
error = true;
|
||||
console.error('Error:', error)
|
||||
});
|
||||
};
|
||||
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
linkId = data.key;
|
||||
done = true;
|
||||
error = false;
|
||||
navigator.clipboard.writeText(
|
||||
`https://awesome-privacy.xyz/inventory/${linkId}`,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
error = true;
|
||||
console.error('Error:', error);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="share-container">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
|
||||
import type { IoSApiResponse } from '@utils/fetch-ios-info';
|
||||
import { formatDate, timeAgo } from '@utils/dates-n-stuff';
|
||||
import { formatDate } from '@utils/dates-n-stuff';
|
||||
import FontAwesome from "@components/form/FontAwesome.svelte"
|
||||
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ const makeRatingPercentage = (rating: number) => (rating / 5) * 100;
|
|||
|
||||
const roundRatings = (rating: number) => Math.round(rating * 100) / 100;
|
||||
|
||||
const putCommaInNumber = (num: number | any) => {
|
||||
const putCommaInNumber = (num: number | string | undefined) => {
|
||||
if (!num) return 'Unknown';
|
||||
return typeof num === 'number' ? num.toLocaleString() : num;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,19 @@
|
|||
import FontAwesome from "@components/form/FontAwesome.svelte";
|
||||
const { github } = Astro.props;
|
||||
|
||||
// const [user, repo] = github.split("/");
|
||||
interface GitHubRepoData {
|
||||
stargazers_count?: number;
|
||||
forks_count?: number;
|
||||
open_issues_count?: number;
|
||||
language?: string;
|
||||
license?: { spdx_id?: string; name?: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given `user/repo` fetch repository stats from the GitHub API data
|
||||
* If API key is available through env var, use it to increase rate limit
|
||||
* Returns the response data and status code (200 == success)
|
||||
* @param repo
|
||||
* @param repo
|
||||
*/
|
||||
const fetchGitHubData = async (repo: string) => {
|
||||
const apiKey = import.meta.env.GITHUB_API_KEY;
|
||||
|
|
@ -17,8 +23,8 @@ const fetchGitHubData = async (repo: string) => {
|
|||
headers.append("Authorization", `token ${apiKey}`);
|
||||
}
|
||||
|
||||
let data = {};
|
||||
let statusCode = 0;
|
||||
let data: GitHubRepoData = {};
|
||||
let statusCode;
|
||||
|
||||
const response = await fetch(`https://api.github.com/repos/${repo}`, {
|
||||
headers: headers,
|
||||
|
|
@ -50,7 +56,7 @@ const fetchGitHubData = async (repo: string) => {
|
|||
* Given a license object, return SPDX ID, or a formatted name
|
||||
* @param license
|
||||
*/
|
||||
const formatLicense = (license: { spdx_id?: string, name?: string }) => {
|
||||
const formatLicense = (license?: { spdx_id?: string, name?: string }) => {
|
||||
if (!license) {
|
||||
return "Unknown";
|
||||
}
|
||||
|
|
@ -65,7 +71,8 @@ const formatLicense = (license: { spdx_id?: string, name?: string }) => {
|
|||
* E.g. If greater than thousand, then return in k format
|
||||
* @param num
|
||||
*/
|
||||
const formatBigNumber = (num: number) => {
|
||||
const formatBigNumber = (num: number | undefined) => {
|
||||
if (num == null) return 0;
|
||||
if (num > 1000) {
|
||||
return `${(num / 1000).toFixed(1)}k`;
|
||||
}
|
||||
|
|
@ -73,7 +80,7 @@ const formatBigNumber = (num: number) => {
|
|||
}
|
||||
|
||||
// Initiate GitHub fetch, and make available to the component
|
||||
const stats = (await fetchGitHubData(github)) as any;
|
||||
const stats = await fetchGitHubData(github);
|
||||
|
||||
const {
|
||||
stargazers_count, forks_count, open_issues_count, language, license,
|
||||
|
|
|
|||
|
|
@ -1,172 +1,191 @@
|
|||
---
|
||||
|
||||
import type { RedditData } from '@utils/fetch-reddit-info';
|
||||
import { timestampToDate, timeAgo } from '@utils/dates-n-stuff';
|
||||
import FontAwesome from "@components/form/FontAwesome.svelte"
|
||||
|
||||
import { timestampToDate } from '@utils/dates-n-stuff';
|
||||
|
||||
interface Props {
|
||||
redditData: RedditData;
|
||||
};
|
||||
redditData: RedditData;
|
||||
}
|
||||
|
||||
const { redditData } = Astro.props;
|
||||
|
||||
---
|
||||
|
||||
<div class="reddit-info-wrapper">
|
||||
<div class="left">
|
||||
<h3>Reddit</h3>
|
||||
<p class="website-title">
|
||||
<img src={redditData.info.icon} width="16" />
|
||||
{redditData.info.title || redditData.info.name}
|
||||
</p>
|
||||
<p class="website-description">{redditData.info.description}</p>
|
||||
{redditData.info.banner && (<img class="banner" width="300" src={redditData.info.banner} alt="Banner" />)}
|
||||
<ul class="list-table">
|
||||
{redditData.info.dateCreated && (
|
||||
<li>
|
||||
<span class="lbl">Created at</span>
|
||||
<span class="val">{timestampToDate(redditData.info.dateCreated * 1000)}</span>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<span class="lbl">Members</span>
|
||||
<span class="val">{redditData.info.subscribers}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="lbl">Join</span>
|
||||
<span class="val"><a href={`https://reddit.com/${redditData.info.name}`}>{redditData.info.name}</a></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h4>Posts</h4>
|
||||
<ul class="posts">
|
||||
{redditData.posts.map((post) => (
|
||||
<li title={post.body}>
|
||||
○ <a href={post.url} target="_blank">{post.title}</a>
|
||||
<span class="votes">(▲ {post.upVotes} ▼ {post.downVotes})</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="left">
|
||||
<h3>Reddit</h3>
|
||||
<p class="website-title">
|
||||
<img src={redditData.info.icon} width="16" />
|
||||
{redditData.info.title || redditData.info.name}
|
||||
</p>
|
||||
<p class="website-description">{redditData.info.description}</p>
|
||||
{
|
||||
redditData.info.banner && (
|
||||
<img
|
||||
class="banner"
|
||||
width="300"
|
||||
src={redditData.info.banner}
|
||||
alt="Banner"
|
||||
/>
|
||||
)
|
||||
}
|
||||
<ul class="list-table">
|
||||
{
|
||||
redditData.info.dateCreated && (
|
||||
<li>
|
||||
<span class="lbl">Created at</span>
|
||||
<span class="val">
|
||||
{timestampToDate(redditData.info.dateCreated * 1000)}
|
||||
</span>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
<li>
|
||||
<span class="lbl">Members</span>
|
||||
<span class="val">{redditData.info.subscribers}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="lbl">Join</span>
|
||||
<span class="val"
|
||||
><a href={`https://reddit.com/${redditData.info.name}`}
|
||||
>{redditData.info.name}</a
|
||||
></span
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h4>Posts</h4>
|
||||
<ul class="posts">
|
||||
{
|
||||
redditData.posts.map((post) => (
|
||||
<li title={post.body}>
|
||||
○{' '}
|
||||
<a href={post.url} target="_blank">
|
||||
{post.title}
|
||||
</a>
|
||||
<span class="votes">
|
||||
(▲ {post.upVotes} ▼ {post.downVotes})
|
||||
</span>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.reddit-info-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
.left, .right {
|
||||
width: calc(50% - 1rem);
|
||||
@media screen and (max-width: 768px){
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.reddit-info-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
.left,
|
||||
.right {
|
||||
width: calc(50% - 1rem);
|
||||
@media screen and (max-width: 768px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.banner {
|
||||
margin: 0.5rem auto;
|
||||
border-radius: var(--curve-md);
|
||||
width: 100%;
|
||||
}
|
||||
.banner {
|
||||
margin: 0.5rem auto;
|
||||
border-radius: var(--curve-md);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 1rem 0 0 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
:global(svg) {
|
||||
width: 1rem;
|
||||
}
|
||||
img {
|
||||
border-radius: var(--curve-sm);
|
||||
}
|
||||
}
|
||||
h4 {
|
||||
margin: 1rem 0 0 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
:global(svg) {
|
||||
width: 1rem;
|
||||
}
|
||||
img {
|
||||
border-radius: var(--curve-sm);
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
img {
|
||||
border-radius: var(--curve-sm);
|
||||
}
|
||||
.list-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
&.list-table {
|
||||
font-size: 0.9rem;
|
||||
padding-left: 0;
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.1rem 0;
|
||||
.lbl {
|
||||
font-weight: 400;
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid #5f53f440;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.posts {
|
||||
list-style: circle;
|
||||
padding-left: 1rem;
|
||||
font-size: 0.9rem;
|
||||
li {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
a {
|
||||
max-width: 80%;
|
||||
}
|
||||
.votes {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.website-title, .website-description {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
border-left: 2px solid var(--accent-3);
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
.website-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
.website-description {
|
||||
font-style: italic;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
.explainer {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.8;
|
||||
font-style: italic;
|
||||
}
|
||||
ul {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
img {
|
||||
border-radius: var(--curve-sm);
|
||||
}
|
||||
.list-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
&.list-table {
|
||||
font-size: 0.9rem;
|
||||
padding-left: 0;
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.1rem 0;
|
||||
.lbl {
|
||||
font-weight: 400;
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid #5f53f440;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.posts {
|
||||
list-style: circle;
|
||||
padding-left: 1rem;
|
||||
font-size: 0.9rem;
|
||||
li {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
a {
|
||||
max-width: 80%;
|
||||
}
|
||||
.votes {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.website-title,
|
||||
.website-description {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
border-left: 2px solid var(--accent-3);
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
.website-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
.website-description {
|
||||
font-style: italic;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
.explainer {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.8;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import FontAwesome from "@components/form/FontAwesome.svelte";
|
||||
import { slugify } from "@utils/fetch-data";
|
||||
import FontAwesome from '@components/form/FontAwesome.svelte';
|
||||
import { slugify } from '@utils/fetch-data';
|
||||
|
||||
export let categoryName: string;
|
||||
export let sectionName: string;
|
||||
|
|
@ -34,23 +34,24 @@
|
|||
</script>
|
||||
|
||||
<div class="wrapper-or-something">
|
||||
<button
|
||||
class={`save-container ${isSaved ? 'saved' : ''} ${showLabel ? 'label-button' : ''}`}
|
||||
title={`Save ${serviceName}`}
|
||||
on:click={toggleSave}>
|
||||
{#if showLabel }
|
||||
<span>
|
||||
{isSaved ? 'Saved' : 'Save'}
|
||||
</span>
|
||||
{/if}
|
||||
<FontAwesome iconName="saveListing"/>
|
||||
</button>
|
||||
<button
|
||||
class={`save-container ${isSaved ? 'saved' : ''} ${showLabel ? 'label-button' : ''}`}
|
||||
title={`Save ${serviceName}`}
|
||||
on:click={toggleSave}
|
||||
>
|
||||
{#if showLabel}
|
||||
<span>
|
||||
{isSaved ? 'Saved' : 'Save'}
|
||||
</span>
|
||||
{/if}
|
||||
<FontAwesome iconName="saveListing" />
|
||||
</button>
|
||||
|
||||
{#if showLabel && isSaved }
|
||||
<div class="done-msg">
|
||||
You can view all saved items in your <a href="/inventory">Inventory</a>
|
||||
</div>
|
||||
{/if}
|
||||
{#if showLabel && isSaved}
|
||||
<div class="done-msg">
|
||||
You can view all saved items in your <a href="/inventory">Inventory</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
@ -71,7 +72,7 @@
|
|||
font-size: 1.2rem;
|
||||
opacity: 0.8;
|
||||
color: var(--foreground);
|
||||
font-family: "Lekton";
|
||||
font-family: 'Lekton';
|
||||
}
|
||||
:global(svg) {
|
||||
color: var(--foreground);
|
||||
|
|
@ -98,9 +99,8 @@
|
|||
box-shadow: 3px 3px 0 var(--box-outline);
|
||||
border: 1px solid var(--box-outline);
|
||||
background: var(--background-form);
|
||||
|
||||
|
||||
&:hover {
|
||||
|
||||
box-shadow: 4px 4px 0 var(--box-outline);
|
||||
}
|
||||
}
|
||||
|
|
@ -110,7 +110,7 @@
|
|||
max-width: 165px;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.6;
|
||||
@media(max-width: 768px) {
|
||||
@media (max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,54 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
import type { Category, Service } from '../../types/Service';
|
||||
import { slugify } from "@utils/fetch-data";
|
||||
import ServiceCard from './ServiceCard.svelte';
|
||||
import type { Category, Service } from '../../types/Service';
|
||||
import { slugify } from '@utils/fetch-data';
|
||||
import ServiceCard from './ServiceCard.svelte';
|
||||
|
||||
export let allData: Category[];
|
||||
export let serviceList: string[] | null = null;
|
||||
export let allData: Category[];
|
||||
export let serviceList: string[] | null = null;
|
||||
|
||||
interface SavedServices {
|
||||
category: string;
|
||||
section: string;
|
||||
service: Service;
|
||||
}
|
||||
interface SavedServices {
|
||||
category: string;
|
||||
section: string;
|
||||
service: Service;
|
||||
}
|
||||
|
||||
const savedServices = writable<SavedServices[]>([]);
|
||||
const savedServices = writable<SavedServices[]>([]);
|
||||
|
||||
onMount(async () => {
|
||||
const results: SavedServices[] = [];
|
||||
const saved = serviceList || JSON.parse(localStorage.getItem('savedServices') || '[]');
|
||||
saved.forEach((serviceId: string) => {
|
||||
const parts = serviceId.split('/');
|
||||
const categoryName = parts[0];
|
||||
const sectionName = parts[1];
|
||||
const serviceName = parts[2];
|
||||
onMount(async () => {
|
||||
const results: SavedServices[] = [];
|
||||
const saved =
|
||||
serviceList || JSON.parse(localStorage.getItem('savedServices') || '[]');
|
||||
saved.forEach((serviceId: string) => {
|
||||
const parts = serviceId.split('/');
|
||||
const categoryName = parts[0];
|
||||
const sectionName = parts[1];
|
||||
const serviceName = parts[2];
|
||||
|
||||
const category = allData.find((category) => slugify(category.name) === categoryName);
|
||||
if (!category) return;
|
||||
const section = category.sections.find((section) => slugify(section.name) === sectionName);
|
||||
if (!section) return;
|
||||
const service = section.services.find((service) => slugify(service.name) === serviceName);
|
||||
if (!service) return;
|
||||
results.push({ category: category.name, section: section.name, service});
|
||||
const category = allData.find(
|
||||
(category) => slugify(category.name) === categoryName,
|
||||
);
|
||||
if (!category) return;
|
||||
const section = category.sections.find(
|
||||
(section) => slugify(section.name) === sectionName,
|
||||
);
|
||||
if (!section) return;
|
||||
const service = section.services.find(
|
||||
(service) => slugify(service.name) === serviceName,
|
||||
);
|
||||
if (!service) return;
|
||||
results.push({ category: category.name, section: section.name, service });
|
||||
});
|
||||
savedServices.set(results || []);
|
||||
});
|
||||
savedServices.set(results || []);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if $savedServices.length > 0}
|
||||
<div class="saved-services">
|
||||
{#each $savedServices as thingy}
|
||||
{#each $savedServices as thingy (thingy.service.name + thingy.section)}
|
||||
<ServiceCard
|
||||
categoryName={thingy.category}
|
||||
sectionName={thingy.section}
|
||||
|
|
@ -52,10 +58,13 @@ onMount(async () => {
|
|||
</div>
|
||||
{:else if !serviceList}
|
||||
<div class="nothing-yet">
|
||||
<p>Here you'll find a list of all the software and services you've bookmarked.</p>
|
||||
<p>
|
||||
Here you'll find a list of all the software and services you've
|
||||
bookmarked.
|
||||
</p>
|
||||
<small>
|
||||
All data is stored on-device, in your browser's local storage,
|
||||
and not sent anywhere unless you choose to share it
|
||||
All data is stored on-device, in your browser's local storage, and not
|
||||
sent anywhere unless you choose to share it
|
||||
</small>
|
||||
<p class="nope">Nothing saved yet!</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,16 +2,17 @@
|
|||
import { onMount } from 'svelte';
|
||||
import Fuse from 'fuse.js';
|
||||
import { slugify } from '@utils/fetch-data';
|
||||
import type { Category, Section, Service, ShortService } from '../../types/Service';
|
||||
import type { Category } from '../../types/Service';
|
||||
import { formatLink } from '@utils/parse-markdown';
|
||||
import { prepareSearchItems, searchOptions } from '@utils/do-searchy-searchy';
|
||||
import type { SearchItem } from '@utils/do-searchy-searchy';
|
||||
|
||||
export let data: Category[];
|
||||
export let previousSearch: string | undefined = undefined;
|
||||
|
||||
let fuse: Fuse<any>;
|
||||
let fuse: Fuse<SearchItem>;
|
||||
let searchQuery = '';
|
||||
let results: any[] = [];
|
||||
let results: SearchItem[];
|
||||
|
||||
// Initialize Fuse.js
|
||||
onMount(() => {
|
||||
|
|
@ -20,9 +21,9 @@
|
|||
});
|
||||
|
||||
const makeResultLink = (cat?: string, sec?: string, itm?: string) => {
|
||||
if (!cat) return '/'
|
||||
if (!sec) return `/${slugify(cat)}`
|
||||
if (!itm) return `/${slugify(cat)}/${slugify(sec)}`
|
||||
if (!cat) return '/';
|
||||
if (!sec) return `/${slugify(cat)}`;
|
||||
if (!itm) return `/${slugify(cat)}/${slugify(sec)}`;
|
||||
return `/${slugify(cat)}/${slugify(sec)}/${slugify(itm)}`;
|
||||
};
|
||||
|
||||
|
|
@ -40,7 +41,7 @@
|
|||
|
||||
const makeTitle = (typ: string, desc: string) => {
|
||||
if (desc && typ === 'Service') {
|
||||
return `${desc.slice(0, 60)}...`
|
||||
return `${desc.slice(0, 60)}...`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
|
@ -59,7 +60,10 @@
|
|||
|
||||
// Watch for changes in the search query and update results
|
||||
$: if (searchQuery) {
|
||||
results = fuse.search(searchQuery).map(result => result.item).splice(0, 25);
|
||||
results = fuse
|
||||
.search(searchQuery)
|
||||
.map((result) => result.item)
|
||||
.splice(0, 25);
|
||||
} else {
|
||||
results = [];
|
||||
}
|
||||
|
|
@ -79,35 +83,48 @@
|
|||
bind:value={searchQuery}
|
||||
on:keydown={handleKeyDown}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
{#if searchQuery.length > 0}
|
||||
<div class="suggestions">
|
||||
<ul>
|
||||
{#each results as result}
|
||||
<li class="result-row">
|
||||
<a
|
||||
href={makeResultLink(result.category, result.sectionName, result.name)}
|
||||
title={makeTitle(result.type, result.description)}
|
||||
{#each results as result (result.name + result.category + result.sectionName)}
|
||||
<li class="result-row">
|
||||
<a
|
||||
href={makeResultLink(
|
||||
result.category,
|
||||
result.sectionName,
|
||||
result.name,
|
||||
)}
|
||||
title={makeTitle(result.type, result.description)}
|
||||
>
|
||||
<span class="name">
|
||||
{#if result.type === 'Service'}
|
||||
<img src={makeLogoSrc(result.logo, result.url)} alt={result.name} width="20" height="20" loading="lazy" />
|
||||
{/if}
|
||||
|
||||
{makeResultText(result.category, result.sectionName, result.name)}
|
||||
|
||||
{#if result.itemCount}
|
||||
<i>({result.itemCount})</i>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="path">
|
||||
{result.category ? `${result.category}` : ''}
|
||||
{result.sectionName ? `➔ ${result.sectionName}` : ''}
|
||||
{result.name ? `➔ ${result.name}` : ''}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<span class="name">
|
||||
{#if result.type === 'Service'}
|
||||
<img
|
||||
src={makeLogoSrc(result.logo, result.url)}
|
||||
alt={result.name}
|
||||
width="20"
|
||||
height="20"
|
||||
loading="lazy"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{makeResultText(
|
||||
result.category,
|
||||
result.sectionName,
|
||||
result.name,
|
||||
)}
|
||||
|
||||
{#if result.itemCount}
|
||||
<i>({result.itemCount})</i>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="path">
|
||||
{result.category ? `${result.category}` : ''}
|
||||
{result.sectionName ? `➔ ${result.sectionName}` : ''}
|
||||
{result.name ? `➔ ${result.name}` : ''}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -115,103 +132,101 @@
|
|||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.search-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
margin: 1rem auto;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
width: 80vw;
|
||||
label {
|
||||
margin: 0.5rem 0;
|
||||
.search-wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
.enter-hint {
|
||||
font-size:0.8rem;
|
||||
opacity: 0.7;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
margin: 1rem auto;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
width: 80vw;
|
||||
label {
|
||||
margin: 0.5rem 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
.enter-hint {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1.8rem;
|
||||
border: 2px solid var(--box-outline);
|
||||
border-radius: var(--curve-lg);
|
||||
box-shadow: 3px 3px 0 var(--box-outline);
|
||||
z-index: 4;
|
||||
background: var(--accent-fg);
|
||||
color: var(--foreground);
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 3px 3px 0 var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
ul {
|
||||
position: absolute;
|
||||
background: var(--background-form);
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
input {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1.8rem;
|
||||
border: 2px solid var(--box-outline);
|
||||
border-radius: 0 0 var(--curve-lg) var(--curve-lg);
|
||||
border-radius: var(--curve-lg);
|
||||
box-shadow: 3px 3px 0 var(--box-outline);
|
||||
transform: translateY(-0.5rem);
|
||||
max-height: 500px;
|
||||
overflow-y: scroll;
|
||||
background: var(--background-form);
|
||||
li.result-row {
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0.5rem 0;
|
||||
a {
|
||||
color: var(--foreground);
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
.name {
|
||||
z-index: 4;
|
||||
background: var(--accent-fg);
|
||||
color: var(--foreground);
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 3px 3px 0 var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
ul {
|
||||
position: absolute;
|
||||
background: var(--background-form);
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 2px solid var(--box-outline);
|
||||
border-radius: 0 0 var(--curve-lg) var(--curve-lg);
|
||||
box-shadow: 3px 3px 0 var(--box-outline);
|
||||
transform: translateY(-0.5rem);
|
||||
max-height: 500px;
|
||||
overflow-y: scroll;
|
||||
background: var(--background-form);
|
||||
li.result-row {
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0.5rem 0;
|
||||
a {
|
||||
color: var(--foreground);
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
i {
|
||||
color: var(--accent);
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
.name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
i {
|
||||
color: var(--accent);
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
img {
|
||||
border-radius: var(--curve-md);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
font-size: 10px;
|
||||
color: var(--accent);
|
||||
overflow: hidden;
|
||||
background: #f453974d;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
img {
|
||||
border-radius: var(--curve-md);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
font-size: 10px;
|
||||
color: var(--accent);
|
||||
overflow: hidden;
|
||||
background: #f453974d;
|
||||
padding: 1px;
|
||||
.path {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
.path {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background: var(--accent);
|
||||
.name i {
|
||||
color: var(--accent-fg);
|
||||
&:hover {
|
||||
background: var(--accent);
|
||||
.name i {
|
||||
color: var(--accent-fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
---
|
||||
|
||||
|
||||
import FontAwesome from '@components/form/FontAwesome.svelte';
|
||||
import { slugify } from '../../utils/fetch-data';
|
||||
|
||||
|
|
@ -9,144 +7,161 @@ import type { Section } from '../../types/Service';
|
|||
interface Props {
|
||||
title: string;
|
||||
sections: Section[];
|
||||
bigTitle?: boolean;
|
||||
bigTitle?: boolean;
|
||||
}
|
||||
|
||||
const { title, sections, bigTitle } = Astro.props;
|
||||
|
||||
---
|
||||
|
||||
<div class="wrap">
|
||||
{
|
||||
bigTitle ? (
|
||||
<h2>
|
||||
<FontAwesome iconName={slugify(title)} />
|
||||
{title}{' '}
|
||||
</h2>
|
||||
) : (
|
||||
<a class="category-title" href={`/${slugify(title)}`}>
|
||||
<h3>{title}</h3>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
{ bigTitle ?
|
||||
<h2><FontAwesome iconName={slugify(title)} />{title} </h2> :
|
||||
<a class="category-title" href={`/${slugify(title)}`}><h3>{title}</h3></a>
|
||||
}
|
||||
{
|
||||
!bigTitle && (
|
||||
<span class="section-icon">
|
||||
<FontAwesome iconName={slugify(title)} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
{ !bigTitle && <span class="section-icon"><FontAwesome iconName={slugify(title)} /></span> }
|
||||
|
||||
<ul>
|
||||
{sections.map((section) => (
|
||||
<li class="section">
|
||||
<a href={`/${slugify(title)}/${slugify(section.name)}`}>
|
||||
<span>{section.name}</span>
|
||||
<span class="service-count">({section.services ? section.services.length : 0})</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<ul>
|
||||
{
|
||||
sections.map((section) => (
|
||||
<li class="section">
|
||||
<a href={`/${slugify(title)}/${slugify(section.name)}`}>
|
||||
<span>{section.name}</span>
|
||||
<span class="service-count">
|
||||
({section.services ? section.services.length : 0})
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.wrap {
|
||||
position: relative;
|
||||
&:hover {
|
||||
.section-icon :global(svg) {
|
||||
opacity: 1;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wrap {
|
||||
position: relative;
|
||||
&:hover {
|
||||
.section-icon :global(svg){
|
||||
opacity: 1;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
font-family: 'Lekton', sans-serif;
|
||||
font-size: 2rem;
|
||||
margin: -2rem 0 2rem -2rem;
|
||||
box-shadow: 6px 6px 0 var(--box-outline);
|
||||
border: 2px solid var(--box-outline);
|
||||
background: var(--accent);
|
||||
color: var(--accent-fg);
|
||||
width: fit-content;
|
||||
padding: 0.25rem 0.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
:global(svg) {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
color: var(--accent-fg);
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: "Lekton", sans-serif;
|
||||
font-size: 2rem;
|
||||
margin: -2rem 0 2rem -2rem;
|
||||
box-shadow: 6px 6px 0 var(--box-outline);
|
||||
border: 2px solid var(--box-outline);
|
||||
background: var(--accent);
|
||||
color: var(--accent-fg);
|
||||
width: fit-content;
|
||||
padding: 0.25rem 0.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
:global(svg) {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
color: var(--accent-fg);
|
||||
}
|
||||
}
|
||||
.category-title {
|
||||
text-decoration: none;
|
||||
color: var(--foreground);
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
h3 {
|
||||
font-family: 'Lekton', sans-serif;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
position: relative;
|
||||
&:after {
|
||||
background: none repeat scroll 0 0 transparent;
|
||||
bottom: 0;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 3px;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
background: var(--accent);
|
||||
transition:
|
||||
width 0.3s ease 0s,
|
||||
left 0.3s ease 0s;
|
||||
width: 0;
|
||||
}
|
||||
&:hover:after {
|
||||
width: 80%;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-title {
|
||||
text-decoration: none;
|
||||
color: var(--foreground);
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
h3 {
|
||||
font-family: "Lekton", sans-serif;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
position: relative;
|
||||
&:after {
|
||||
background: none repeat scroll 0 0 transparent;
|
||||
bottom: 0;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 3px;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
background: var(--accent);
|
||||
transition: width 0.3s ease 0s, left 0.3s ease 0s;
|
||||
width: 0;
|
||||
}
|
||||
&:hover:after {
|
||||
width: 80%;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
ul {
|
||||
list-style: circle;
|
||||
padding-left: 1rem;
|
||||
li {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.25rem;
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--foreground);
|
||||
position: relative;
|
||||
&:after {
|
||||
background: none repeat scroll 0 0 transparent;
|
||||
bottom: 0;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 2px;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
background: var(--accent-3);
|
||||
transition:
|
||||
width 0.15s ease 0s,
|
||||
left 0.15s ease 0s;
|
||||
width: 0;
|
||||
}
|
||||
&:hover:after {
|
||||
text-decoration: underline;
|
||||
width: 80%;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
.service-count {
|
||||
color: var(--accent-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: circle;
|
||||
padding-left: 1rem;
|
||||
li {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.25rem;
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--foreground);
|
||||
position: relative;
|
||||
&:after {
|
||||
background: none repeat scroll 0 0 transparent;
|
||||
bottom: 0;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 2px;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
background: var(--accent-3);
|
||||
transition: width 0.15s ease 0s, left 0.15s ease 0s;
|
||||
width: 0;
|
||||
}
|
||||
&:hover:after {
|
||||
text-decoration: underline;
|
||||
width: 80%;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
.service-count {
|
||||
color: var(--accent-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: fit-content;
|
||||
:global(svg) {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
opacity: 0.5;
|
||||
text-shadow: 3px 3px 0 black;
|
||||
color: var(--accent-3);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
.section-icon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: fit-content;
|
||||
:global(svg) {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
opacity: 0.5;
|
||||
text-shadow: 3px 3px 0 black;
|
||||
color: var(--accent-3);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
---
|
||||
|
||||
import { formatLink } from '@utils/parse-markdown';
|
||||
import type { Service } from 'src/types/Service';
|
||||
import FontAwesome from '@components/form/FontAwesome.svelte';
|
||||
|
|
@ -7,86 +6,91 @@ import SaveListing from '@components/things/SaveListing.svelte';
|
|||
import { slugify } from '@utils/fetch-data';
|
||||
|
||||
interface Props {
|
||||
service: Service;
|
||||
categoryName: string;
|
||||
sectionName: string;
|
||||
service: Service;
|
||||
categoryName: string;
|
||||
sectionName: string;
|
||||
}
|
||||
|
||||
const {
|
||||
service,
|
||||
sectionName,
|
||||
categoryName,
|
||||
} = Astro.props;
|
||||
|
||||
const { service, sectionName, categoryName } = Astro.props;
|
||||
---
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const serviceIcons = document.querySelectorAll<HTMLImageElement>('.service-icon');
|
||||
const broke = '/broken-image.png';
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const serviceIcons =
|
||||
document.querySelectorAll<HTMLImageElement>('.service-icon');
|
||||
const broke = '/broken-image.png';
|
||||
|
||||
serviceIcons.forEach(function(icon) {
|
||||
icon.onerror = function() {
|
||||
const imgElement = this as HTMLImageElement;
|
||||
const serviceUrl = imgElement.getAttribute('data-service-url');
|
||||
const newSrcAttribute = (imgElement.src.includes('on.ho') ? broke : `https://icon.horse/icon/${serviceUrl}`);
|
||||
imgElement.src = imgElement.src !== newSrcAttribute ? newSrcAttribute : broke;
|
||||
imgElement.onerror = null;
|
||||
};
|
||||
});
|
||||
});
|
||||
serviceIcons.forEach(function (icon) {
|
||||
icon.onerror = function () {
|
||||
const imgElement = this as HTMLImageElement;
|
||||
const serviceUrl = imgElement.getAttribute('data-service-url');
|
||||
const newSrcAttribute = imgElement.src.includes('on.ho')
|
||||
? broke
|
||||
: `https://icon.horse/icon/${serviceUrl}`;
|
||||
imgElement.src =
|
||||
imgElement.src !== newSrcAttribute ? newSrcAttribute : broke;
|
||||
imgElement.onerror = null;
|
||||
};
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<div class="service" id={slugify(service.name)}>
|
||||
<div class="service-head">
|
||||
<a class="service-title" href={`/${slugify(categoryName)}/${slugify(sectionName)}/${slugify(service.name)}`}>
|
||||
<h4>{service.name}</h4>
|
||||
</a>
|
||||
{service.followWith && <p class="follow-with">({service.followWith})</p> }
|
||||
</div>
|
||||
<div class="service-head">
|
||||
<a
|
||||
class="service-title"
|
||||
href={`/${slugify(categoryName)}/${slugify(sectionName)}/${slugify(service.name)}`}
|
||||
>
|
||||
<h4>{service.name}</h4>
|
||||
</a>
|
||||
{service.followWith && <p class="follow-with">({service.followWith})</p>}
|
||||
</div>
|
||||
|
||||
<div class="save-listing">
|
||||
<SaveListing client:visible
|
||||
categoryName={categoryName}
|
||||
sectionName={sectionName}
|
||||
serviceName={service.name}
|
||||
/>
|
||||
</div>
|
||||
<div class="save-listing">
|
||||
<SaveListing
|
||||
client:visible
|
||||
categoryName={categoryName}
|
||||
sectionName={sectionName}
|
||||
serviceName={service.name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="service-body">
|
||||
<img
|
||||
width="40"
|
||||
height="40"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="service-icon"
|
||||
alt={`${service.name} Icon`}
|
||||
data-service-url={formatLink(service.url)}
|
||||
src={service.icon || `https://icon.horse/icon/${formatLink(service.url)}`}
|
||||
/>
|
||||
<div class="service-body">
|
||||
<p set:html={service.description}></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="service-body">
|
||||
<img
|
||||
width="40"
|
||||
height="40"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="service-icon"
|
||||
alt={`${service.name} Icon`}
|
||||
data-service-url={formatLink(service.url)}
|
||||
src={service.icon || `https://icon.horse/icon/${formatLink(service.url)}`}
|
||||
/>
|
||||
<div class="service-body">
|
||||
<p set:html={service.description} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-links">
|
||||
<a class="link" href={service.url}>
|
||||
<FontAwesome iconName="website"/> <span>{formatLink(service.url)}</span>
|
||||
</a>
|
||||
{service.github &&
|
||||
<a class="link" href={`https://github.com/${service.github}`}>
|
||||
<FontAwesome iconName="sourceCode"/> GitHub
|
||||
</a>
|
||||
}
|
||||
<a href={`/${slugify(categoryName)}/${slugify(sectionName)}/${slugify(service.name)}`}>
|
||||
<FontAwesome iconName="viewReport" /> View Report ➔
|
||||
</a>
|
||||
<div class="service-links">
|
||||
<a class="link" href={service.url}>
|
||||
<FontAwesome iconName="website" />
|
||||
<span>{formatLink(service.url)}</span>
|
||||
</a>
|
||||
{
|
||||
service.github && (
|
||||
<a class="link" href={`https://github.com/${service.github}`}>
|
||||
<FontAwesome iconName="sourceCode" /> GitHub
|
||||
</a>
|
||||
)
|
||||
}
|
||||
<a
|
||||
href={`/${slugify(categoryName)}/${slugify(sectionName)}/${slugify(service.name)}`}
|
||||
>
|
||||
<FontAwesome iconName="viewReport" /> View Report ➔
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import './service-card.scss';
|
||||
</style>
|
||||
<style lang="scss">
|
||||
@use './service-card.scss';
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@
|
|||
|
||||
<div class="service" id={serviceRef}>
|
||||
<div class="service-head">
|
||||
<a class="service-title" href={`/${categorySlug}/${sectionSlug}/${serviceRef}`}>
|
||||
<a
|
||||
class="service-title"
|
||||
href={`/${categorySlug}/${sectionSlug}/${serviceRef}`}
|
||||
>
|
||||
<h4>{service.name}</h4>
|
||||
</a>
|
||||
{#if service.followWith}
|
||||
|
|
@ -26,11 +29,7 @@
|
|||
</div>
|
||||
|
||||
<div class="save-listing">
|
||||
<SaveListing
|
||||
categoryName={categoryName}
|
||||
sectionName={sectionName}
|
||||
serviceName={service.name}
|
||||
/>
|
||||
<SaveListing {categoryName} {sectionName} serviceName={service.name} />
|
||||
</div>
|
||||
|
||||
<div class="service-body">
|
||||
|
|
@ -45,6 +44,7 @@
|
|||
src={service.icon || `https://icon.horse/icon/${formatLink(service.url)}`}
|
||||
/>
|
||||
<div class="service-body">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -- description is from curated YAML data, not user input -->
|
||||
<p>{@html service.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -65,5 +65,5 @@
|
|||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import './service-card.scss';
|
||||
@use './service-card.scss';
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
---
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const serviceIcons = document.querySelectorAll<HTMLImageElement>('.service-icon');
|
||||
const broke = '/broken-image.png';
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const serviceIcons =
|
||||
document.querySelectorAll<HTMLImageElement>('.service-icon');
|
||||
const broke = '/broken-image.png';
|
||||
|
||||
serviceIcons.forEach(function(icon) {
|
||||
icon.onerror = function() {
|
||||
const imgElement = this as HTMLImageElement;
|
||||
const serviceUrl = imgElement.getAttribute('data-service-url');
|
||||
const newSrcAttribute = (imgElement.src.includes('on.ho') ? broke : `https://icon.horse/icon/${serviceUrl}`);
|
||||
imgElement.src = imgElement.src !== newSrcAttribute ? newSrcAttribute : broke;
|
||||
imgElement.onerror = null;
|
||||
};
|
||||
});
|
||||
});
|
||||
serviceIcons.forEach(function (icon) {
|
||||
icon.onerror = function () {
|
||||
const imgElement = this as HTMLImageElement;
|
||||
const serviceUrl = imgElement.getAttribute('data-service-url');
|
||||
const newSrcAttribute = imgElement.src.includes('on.ho')
|
||||
? broke
|
||||
: `https://icon.horse/icon/${serviceUrl}`;
|
||||
imgElement.src =
|
||||
imgElement.src !== newSrcAttribute ? newSrcAttribute : broke;
|
||||
imgElement.onerror = null;
|
||||
};
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section>
|
||||
{services && services.length > 0 ? (
|
||||
<ul>
|
||||
{services.map((service: Service) => (
|
||||
<li id={slugify(service.name)}>
|
||||
<DeleteListing client:load categoryName={categoryName} sectionName={sectionName} serviceName={service.name} />
|
||||
<div class="save-listing">
|
||||
<SaveListing client:visible
|
||||
categoryName={categoryName}
|
||||
sectionName={sectionName}
|
||||
serviceName={service.name}
|
||||
/>
|
||||
</div>
|
||||
<div class="service-head">
|
||||
<img
|
||||
width="40"
|
||||
height="40"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="service-icon"
|
||||
alt={`${service.name} Icon`}
|
||||
data-service-url={formatLink(service.url)}
|
||||
src={service.icon || `https://icon.horse/icon/${formatLink(service.url)}`}
|
||||
/>
|
||||
{
|
||||
services && services.length > 0 ? (
|
||||
<ul>
|
||||
{services.map((service: Service) => (
|
||||
<li id={slugify(service.name)}>
|
||||
<DeleteListing
|
||||
client:load
|
||||
categoryName={categoryName}
|
||||
sectionName={sectionName}
|
||||
serviceName={service.name}
|
||||
/>
|
||||
<div class="save-listing">
|
||||
<SaveListing
|
||||
client:visible
|
||||
categoryName={categoryName}
|
||||
sectionName={sectionName}
|
||||
serviceName={service.name}
|
||||
/>
|
||||
</div>
|
||||
<div class="service-head">
|
||||
<img
|
||||
width="40"
|
||||
height="40"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="service-icon"
|
||||
alt={`${service.name} Icon`}
|
||||
data-service-url={formatLink(service.url)}
|
||||
src={
|
||||
service.icon ||
|
||||
`https://icon.horse/icon/${formatLink(service.url)}`
|
||||
}
|
||||
/>
|
||||
|
||||
<a class="service-title" href={`/${slugify(categoryName)}/${slugify(sectionName)}/${slugify(service.name)}`}>
|
||||
{subHeading ? <h4>{service.name}</h4> : <h3>{service.name}</h3>}
|
||||
</a>
|
||||
{service.followWith && <p class="follow-with">({service.followWith})</p> }
|
||||
<a class="service-link" href={service.url}>{formatLink(service.url)}</a>
|
||||
</div>
|
||||
<div class="service-body">
|
||||
<p set:html={parseMarkdown(service.description)}></p>
|
||||
<div class="service-stats">
|
||||
<div class="left">
|
||||
{ service.securityAudited && (
|
||||
<span class="meta-item great" title={`${service.name} has been security audited by an accredited auditor, with results published publicly`}>
|
||||
<FontAwesome iconName="securityAudited" /> Security Audited
|
||||
</span>
|
||||
)}
|
||||
{ service.acceptsCrypto && (
|
||||
<span class="meta-item great" title={`${service.name} accepts anonymous payment methods`}>
|
||||
<FontAwesome iconName="cryptoAccepted" /> Crypto Payments Accepted
|
||||
</span>
|
||||
)}
|
||||
{ service.securityAudited === false && (
|
||||
<span class="meta-item warning" title={`${service.name} has not been audited`}>
|
||||
<FontAwesome iconName="notSecurityAudited" /> No Security Audit
|
||||
</span>
|
||||
)}
|
||||
{ (service.openSource === false) && (
|
||||
<span class="warning">
|
||||
<FontAwesome iconName="closedSource" />
|
||||
Not Open Source
|
||||
</span>
|
||||
)}
|
||||
{ service.openSource || (service.github && service.openSource !== false) ? (
|
||||
<span class="meta-item great" title={`${service.name} is open source`}>
|
||||
<FontAwesome iconName="openSource" /> Open Source
|
||||
</span>
|
||||
) : null }
|
||||
{ service.github && !noGitHubMetrics && <GitHubMetrics github={service.github} /> }
|
||||
{ service.github && noGitHubMetrics && (
|
||||
<span class="meta-item" title={`View ${service.name} on GitHub`}>
|
||||
<a href={`https://github.com/${service.github}`} target="_blank">
|
||||
<FontAwesome iconName="github" /> {service.github}
|
||||
</a>
|
||||
</span>
|
||||
) }
|
||||
</div>
|
||||
<div class="view-service">
|
||||
<a href={`/${slugify(categoryName)}/${slugify(sectionName)}/${slugify(service.name)}`}>View {service.name} Report</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p class="nothing-yet">
|
||||
<strong>⚠️ This section is still a work in progress ⚠️</strong><br />
|
||||
Check back soon, or help us complete it by submiting a pull request on GitHub.
|
||||
<br />
|
||||
<span class="quick-submit">Or submit an entry <a href="/submit">here</a></span>
|
||||
</p>
|
||||
)}
|
||||
<a
|
||||
class="service-title"
|
||||
href={`/${slugify(categoryName)}/${slugify(sectionName)}/${slugify(service.name)}`}
|
||||
>
|
||||
{subHeading ? <h4>{service.name}</h4> : <h3>{service.name}</h3>}
|
||||
</a>
|
||||
{service.followWith && (
|
||||
<p class="follow-with">({service.followWith})</p>
|
||||
)}
|
||||
<a class="service-link" href={service.url}>
|
||||
{formatLink(service.url)}
|
||||
</a>
|
||||
</div>
|
||||
<div class="service-body">
|
||||
<p set:html={parseMarkdown(service.description)} />
|
||||
<div class="service-stats">
|
||||
<div class="left">
|
||||
{service.securityAudited && (
|
||||
<span
|
||||
class="meta-item great"
|
||||
title={`${service.name} has been security audited by an accredited auditor, with results published publicly`}
|
||||
>
|
||||
<FontAwesome iconName="securityAudited" /> Security
|
||||
Audited
|
||||
</span>
|
||||
)}
|
||||
{service.acceptsCrypto && (
|
||||
<span
|
||||
class="meta-item great"
|
||||
title={`${service.name} accepts anonymous payment methods`}
|
||||
>
|
||||
<FontAwesome iconName="cryptoAccepted" /> Crypto Payments
|
||||
Accepted
|
||||
</span>
|
||||
)}
|
||||
{service.securityAudited === false && (
|
||||
<span
|
||||
class="meta-item warning"
|
||||
title={`${service.name} has not been audited`}
|
||||
>
|
||||
<FontAwesome iconName="notSecurityAudited" /> No Security
|
||||
Audit
|
||||
</span>
|
||||
)}
|
||||
{service.openSource === false && (
|
||||
<span class="warning">
|
||||
<FontAwesome iconName="closedSource" />
|
||||
Not Open Source
|
||||
</span>
|
||||
)}
|
||||
{service.openSource ||
|
||||
(service.github && service.openSource !== false) ? (
|
||||
<span
|
||||
class="meta-item great"
|
||||
title={`${service.name} is open source`}
|
||||
>
|
||||
<FontAwesome iconName="openSource" /> Open Source
|
||||
</span>
|
||||
) : null}
|
||||
{service.github && !noGitHubMetrics && (
|
||||
<GitHubMetrics github={service.github} />
|
||||
)}
|
||||
{service.github && noGitHubMetrics && (
|
||||
<span
|
||||
class="meta-item"
|
||||
title={`View ${service.name} on GitHub`}
|
||||
>
|
||||
<a
|
||||
href={`https://github.com/${service.github}`}
|
||||
target="_blank"
|
||||
>
|
||||
<FontAwesome iconName="github" /> {service.github}
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div class="view-service">
|
||||
<a
|
||||
href={`/${slugify(categoryName)}/${slugify(sectionName)}/${slugify(service.name)}`}
|
||||
>
|
||||
View {service.name} Report
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p class="nothing-yet">
|
||||
<>
|
||||
<strong>⚠️ This section is still a work in progress ⚠️</strong>
|
||||
<br />
|
||||
</>
|
||||
Check back soon, or help us complete it by submiting a pull request on
|
||||
GitHub.
|
||||
<br />
|
||||
<span class="quick-submit">
|
||||
Or submit an entry <a href="/submit">here</a>
|
||||
</span>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
{buttonLink && (
|
||||
<Button title={`View all ${categoryName}`} className="view-all" text="View More..." url={buttonLink} />
|
||||
)}
|
||||
{
|
||||
buttonLink && (
|
||||
<Button
|
||||
title={`View all ${categoryName}`}
|
||||
className="view-all"
|
||||
text="View More..."
|
||||
url={buttonLink}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
padding: 1rem 0;
|
||||
position: relative;
|
||||
&:not(:last-child) {
|
||||
border-bottom: 2px solid var(--accent-3);
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 1rem 0;
|
||||
position: relative;
|
||||
&:not(:last-child) {
|
||||
border-bottom: 2px solid var(--accent-3);
|
||||
}
|
||||
}
|
||||
|
||||
.nothing-yet {
|
||||
font-size: 1.4rem;
|
||||
opacity: 0.8;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
.quick-submit {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 3rem 0;
|
||||
li {
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
.save-listing {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
}
|
||||
.service-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
.service-title {
|
||||
text-decoration: none;
|
||||
color: var(--foreground);
|
||||
h3, h4 {
|
||||
margin: 0;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
.nothing-yet {
|
||||
font-size: 1.4rem;
|
||||
opacity: 0.8;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
.quick-submit {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
position: relative;
|
||||
&:after {
|
||||
background: none repeat scroll 0 0 transparent;
|
||||
bottom: 0;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 3px;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
background: var(--accent-3);
|
||||
transition: width 0.3s ease 0s, left 0.3s ease 0s;
|
||||
width: 0;
|
||||
}
|
||||
&:hover:after {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: var(--curve-sm);
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 3rem 0;
|
||||
li {
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
.save-listing {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
}
|
||||
.service-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
.service-title {
|
||||
text-decoration: none;
|
||||
color: var(--foreground);
|
||||
h3,
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
font-size: 10px;
|
||||
overflow: hidden;
|
||||
color: var(--accent);
|
||||
}
|
||||
.follow-with {
|
||||
opacity: 0.7;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
.service-link {
|
||||
max-width: 300px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.service-body {
|
||||
margin: 0.5rem 0 2rem;
|
||||
opacity: 0.8;
|
||||
:global(p) {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
:global(a) {
|
||||
color: var(--foregorund);
|
||||
}
|
||||
}
|
||||
.service-stats {
|
||||
.left {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.view-service {
|
||||
transition: all 0.2s ease-in-out;
|
||||
opacity: 0.95;
|
||||
a {
|
||||
padding: 0.25rem 0.6rem;
|
||||
width: fit-content;
|
||||
right: 1rem;
|
||||
font-size: 0.9rem;
|
||||
background: var(--accent-3);
|
||||
color: var(--accent-fg);
|
||||
text-decoration: none;
|
||||
border-radius: var(--curve-md);
|
||||
}
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
position: relative;
|
||||
&:after {
|
||||
background: none repeat scroll 0 0 transparent;
|
||||
bottom: 0;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 3px;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
background: var(--accent-3);
|
||||
transition:
|
||||
width 0.3s ease 0s,
|
||||
left 0.3s ease 0s;
|
||||
width: 0;
|
||||
}
|
||||
&:hover:after {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.meta-item, .warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// justify-content: center;
|
||||
gap: 0.25rem;
|
||||
// opacity: 0.6;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.5rem 0;
|
||||
:global(svg) {
|
||||
color: var(--foreground);
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--foreground);
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
&:hover {
|
||||
color: var(--accent-3);
|
||||
:global(svg) {
|
||||
color: var(--accent-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.warning {
|
||||
color: var(--danger);
|
||||
:global(svg) {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
.great {
|
||||
color: #0fb953; // var(--success);
|
||||
:global(svg) {
|
||||
color: #0fb953; // var(--success);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.service-icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: var(--curve-sm);
|
||||
|
||||
section :global(.view-all) {
|
||||
width: fit-content;
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
margin-top: -2.5rem;
|
||||
background: var(--accent-3);
|
||||
}
|
||||
font-size: 10px;
|
||||
overflow: hidden;
|
||||
color: var(--accent);
|
||||
}
|
||||
.follow-with {
|
||||
opacity: 0.7;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
.service-link {
|
||||
max-width: 300px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
li:hover :global(.actions) {
|
||||
opacity: 0.6;
|
||||
:global(a):hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
:global(.actions a):hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.service-body {
|
||||
margin: 0.5rem 0 2rem;
|
||||
opacity: 0.8;
|
||||
:global(p) {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
:global(a) {
|
||||
color: var(--foregorund);
|
||||
}
|
||||
}
|
||||
.service-stats {
|
||||
.left {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.view-service {
|
||||
transition: all 0.2s ease-in-out;
|
||||
opacity: 0.95;
|
||||
a {
|
||||
padding: 0.25rem 0.6rem;
|
||||
width: fit-content;
|
||||
right: 1rem;
|
||||
font-size: 0.9rem;
|
||||
background: var(--accent-3);
|
||||
color: var(--accent-fg);
|
||||
text-decoration: none;
|
||||
border-radius: var(--curve-md);
|
||||
}
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.meta-item,
|
||||
.warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// justify-content: center;
|
||||
gap: 0.25rem;
|
||||
// opacity: 0.6;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.5rem 0;
|
||||
:global(svg) {
|
||||
color: var(--foreground);
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--foreground);
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
&:hover {
|
||||
color: var(--accent-3);
|
||||
:global(svg) {
|
||||
color: var(--accent-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.warning {
|
||||
color: var(--danger);
|
||||
:global(svg) {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
.great {
|
||||
color: #0fb953; // var(--success);
|
||||
:global(svg) {
|
||||
color: #0fb953; // var(--success);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section :global(.view-all) {
|
||||
width: fit-content;
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
margin-top: -2.5rem;
|
||||
background: var(--accent-3);
|
||||
}
|
||||
|
||||
li:hover :global(.actions) {
|
||||
opacity: 0.6;
|
||||
:global(a):hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
:global(.actions a):hover {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
import type { Category, Service } from "../../types/Service";
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Category, Service } from '../../types/Service';
|
||||
import { formatLink } from '@utils/parse-markdown';
|
||||
import { slugify } from '@utils/fetch-data';
|
||||
|
||||
|
|
@ -14,21 +14,23 @@
|
|||
|
||||
let results = writable<ServiceResult[]>([]);
|
||||
|
||||
const normalize = (str: string) => str.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
const normalize = (str: string) =>
|
||||
str.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
|
||||
onMount(async () => {
|
||||
|
||||
const apiEndpoint = `https://awesome-privacy.as93.workers.dev/${searchTerm}`;
|
||||
const fetchedServices = await fetch(apiEndpoint)
|
||||
.then((response) => response.json())
|
||||
.then((data) => (JSON.parse(data) || []).map((servName: string) => normalize(servName)));
|
||||
.then((data) =>
|
||||
(JSON.parse(data) || []).map((servName: string) => normalize(servName)),
|
||||
);
|
||||
|
||||
const tmpResults: ServiceResult[] = [];
|
||||
categories.forEach((category) => {
|
||||
(category.sections || []).forEach((section) => {
|
||||
(section.services || []).forEach((service) => {
|
||||
if (fetchedServices.includes(normalize(service.name))) {
|
||||
const path = `/${slugify(category.name)}/${slugify(section.name)}/${slugify(service.name)}`
|
||||
const path = `/${slugify(category.name)}/${slugify(section.name)}/${slugify(service.name)}`;
|
||||
tmpResults.push({ ...service, path });
|
||||
return;
|
||||
}
|
||||
|
|
@ -45,35 +47,36 @@
|
|||
{#if $results.length > 1}
|
||||
<h3>Top Results</h3>
|
||||
{/if}
|
||||
<section>
|
||||
{#each $results as service (service)}
|
||||
<a class="service-result" href={service.path}>
|
||||
<div class="service-head">
|
||||
<img
|
||||
width="40"
|
||||
height="40"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="service-icon"
|
||||
alt={`${service.name} Icon`}
|
||||
data-service-url={formatLink(service.url)}
|
||||
src={service.icon || `https://icon.horse/icon/${formatLink(service.url)}`}
|
||||
/>
|
||||
<div>
|
||||
<h4>
|
||||
{service.name}
|
||||
{#if service.followWith}
|
||||
<section>
|
||||
{#each $results as service (service)}
|
||||
<a class="service-result" href={service.path}>
|
||||
<div class="service-head">
|
||||
<img
|
||||
width="40"
|
||||
height="40"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="service-icon"
|
||||
alt={`${service.name} Icon`}
|
||||
data-service-url={formatLink(service.url)}
|
||||
src={service.icon ||
|
||||
`https://icon.horse/icon/${formatLink(service.url)}`}
|
||||
/>
|
||||
<div>
|
||||
<h4>
|
||||
{service.name}
|
||||
{#if service.followWith}
|
||||
<p class="follow-with">({service.followWith})</p>
|
||||
{/if}
|
||||
</h4>
|
||||
<a class="service-link" href={service.url}>{formatLink(service.url)}</a>
|
||||
</div>
|
||||
{/if}
|
||||
</h4>
|
||||
<a class="service-link" href={service.url}
|
||||
>{formatLink(service.url)}</a
|
||||
>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
h3 {
|
||||
|
|
|
|||
|
|
@ -29,21 +29,23 @@
|
|||
h4 {
|
||||
text-decoration: none;
|
||||
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-3);
|
||||
transition: width 0.2s ease 0s, left 0.2s ease 0s;
|
||||
transition:
|
||||
width 0.2s ease 0s,
|
||||
left 0.2s ease 0s;
|
||||
width: 0;
|
||||
}
|
||||
&:hover:after {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
&:hover:after {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -66,7 +68,7 @@
|
|||
:global(p) {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 0;
|
||||
width: calc(100% - 2rem);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
import { ViewTransitions } from 'astro:transitions'
|
||||
import { ViewTransitions } from 'astro:transitions';
|
||||
import NavBar from '@components/scafold/NavBar.astro';
|
||||
import Footer from '@components/scafold/Footer.astro';
|
||||
import config from '../site-config';
|
||||
|
|
@ -7,14 +7,14 @@ import config from '../site-config';
|
|||
interface Props {
|
||||
title?: string; // Page title
|
||||
description?: string; // Overide description tag
|
||||
keywords?: string; // Overide keywords tag
|
||||
keywords?: string; // Overide keywords tag
|
||||
hideNav?: boolean; // Don't show the navbar (just homepage)
|
||||
author?: string; // Author of the content
|
||||
customSchemaJson?: any; // Custom schema item
|
||||
customSchemaJson?: Record<string, unknown>; // Custom schema item
|
||||
breadcrumbs?: Array<{
|
||||
name: string;
|
||||
item: string;
|
||||
}>
|
||||
}>;
|
||||
}
|
||||
|
||||
const {
|
||||
|
|
@ -30,92 +30,120 @@ const {
|
|||
const makeBreadcrumbs = () => {
|
||||
if (!breadcrumbs) return null;
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": breadcrumbs.map((breadcrumb, index) => ({
|
||||
"@type": "ListItem",
|
||||
"position": index + 1,
|
||||
"name": breadcrumb.name,
|
||||
"item": `https://awesome-privacy.xyz/${breadcrumb.item}`
|
||||
}))
|
||||
}
|
||||
}
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: breadcrumbs.map((breadcrumb, index) => ({
|
||||
'@type': 'ListItem',
|
||||
position: index + 1,
|
||||
name: breadcrumb.name,
|
||||
item: `https://awesome-privacy.xyz/${breadcrumb.item}`,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const makeSearchLd = () => {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"url": "https://awesome-privacy.xyz/",
|
||||
"potentialAction": [{
|
||||
"@type": "SearchAction",
|
||||
"target": {
|
||||
"@type": "EntryPoint",
|
||||
"urlTemplate": "https://awesome-privacy.xyz/search?q={search_term_string}"
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
url: 'https://awesome-privacy.xyz/',
|
||||
potentialAction: [
|
||||
{
|
||||
'@type': 'SearchAction',
|
||||
target: {
|
||||
'@type': 'EntryPoint',
|
||||
urlTemplate:
|
||||
'https://awesome-privacy.xyz/search?q={search_term_string}',
|
||||
},
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
"query-input": "required name=search_term_string"
|
||||
}]
|
||||
}
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
|
||||
<ViewTransitions />
|
||||
|
||||
|
||||
<!-- Core info -->
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description}>
|
||||
<meta name="keywords" content={keywords}>
|
||||
<meta name="author" content={author}>
|
||||
<meta name="description" content={description} />
|
||||
<meta name="keywords" content={keywords} />
|
||||
<meta name="author" content={author} />
|
||||
|
||||
<!-- Page info, viewport, Astro credit -->
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<meta name="robots" content="index, follow">
|
||||
<meta name="robots" content="index, follow" />
|
||||
|
||||
<!-- Icons and colors -->
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" sizes="64x64" href="/favicon.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="64x64" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
|
||||
<!-- Social media meta tags (Open Graphh + Twitter) -->
|
||||
<meta property="og:site_name" content="Awesome Privacy">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://awesome-privacy.xyz">
|
||||
<meta property="og:title" content={title}>
|
||||
<meta property="og:description" content={description}>
|
||||
<meta property="og:image" content="https://awesome-privacy.xyz/banner.png">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:url" content="https://awesome-privacy.xyz">
|
||||
<meta name="twitter:title" content={title}>
|
||||
<meta name="twitter:description" content={description}>
|
||||
<meta name="twitter:image" content="https://awesome-privacy.xyz/banner.png">
|
||||
<link rel="twitter:image" sizes="180x180" href="https://awesome-privacy.xyz/apple-touch-icon.png">
|
||||
<meta name="twitter:site" content="@Lissy_Sykes">
|
||||
<meta name="twitter:creator" content="@Lissy_Sykes">
|
||||
<meta property="og:site_name" content="Awesome Privacy" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://awesome-privacy.xyz" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://awesome-privacy.xyz/banner.png"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:url" content="https://awesome-privacy.xyz" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://awesome-privacy.xyz/banner.png"
|
||||
/>
|
||||
<link
|
||||
rel="twitter:image"
|
||||
sizes="180x180"
|
||||
href="https://awesome-privacy.xyz/apple-touch-icon.png"
|
||||
/>
|
||||
<meta name="twitter:site" content="@Lissy_Sykes" />
|
||||
<meta name="twitter:creator" content="@Lissy_Sykes" />
|
||||
|
||||
<!-- Non-tracking hit counter -->
|
||||
<script defer is:inline
|
||||
<script
|
||||
defer
|
||||
is:inline
|
||||
type="text/partytown"
|
||||
data-domain="awesome-privacy.xyz"
|
||||
src="https://no-track.as93.net/js/script.js">
|
||||
</script>
|
||||
src="https://no-track.as93.net/js/script.js"></script>
|
||||
|
||||
<!-- Schema.org markup for Google -->
|
||||
{breadcrumbs && (
|
||||
<script type="application/ld+json" set:html={JSON.stringify(makeBreadcrumbs())} />
|
||||
)}
|
||||
{customSchemaJson && (
|
||||
<script type="application/ld+json" set:html={JSON.stringify(customSchemaJson)} />
|
||||
)}
|
||||
<script type="application/ld+json" set:html={JSON.stringify(makeSearchLd)} />
|
||||
{
|
||||
breadcrumbs && (
|
||||
<script
|
||||
is:inline
|
||||
type="application/ld+json"
|
||||
set:html={JSON.stringify(makeBreadcrumbs())}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
customSchemaJson && (
|
||||
<script
|
||||
is:inline
|
||||
type="application/ld+json"
|
||||
set:html={JSON.stringify(customSchemaJson)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<script
|
||||
is:inline
|
||||
type="application/ld+json"
|
||||
set:html={JSON.stringify(makeSearchLd)}
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
{!hideNav && <NavBar /> }
|
||||
{!hideNav && <NavBar />}
|
||||
<slot />
|
||||
<Footer />
|
||||
</body>
|
||||
|
|
@ -124,11 +152,9 @@ const makeSearchLd = () => {
|
|||
<style is:global>
|
||||
@import '../styles/values.css';
|
||||
@import '../styles/typography.css';
|
||||
</style>
|
||||
|
||||
</style>
|
||||
|
||||
<style is:global>
|
||||
|
||||
html {
|
||||
::selection {
|
||||
background: var(--accent);
|
||||
|
|
@ -142,5 +168,4 @@ const makeSearchLd = () => {
|
|||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,98 +1,91 @@
|
|||
---
|
||||
|
||||
import Layout from '@layouts/Layout.astro';
|
||||
import Buton from '@components/form/Button.astro';
|
||||
|
||||
---
|
||||
|
||||
<Layout title="404 | Awesome Privacy">
|
||||
<article class="oh-crap">
|
||||
<h2>404</h2>
|
||||
<p class="what-happened">Page not found 😢</p>
|
||||
<p class="why-happened">
|
||||
It seems this page doesn't exist (yet). We're sorry about that.
|
||||
</p>
|
||||
<span class="back-you-go-then"><Buton url="/">Go back home</Buton></span>
|
||||
|
||||
<article class="oh-crap">
|
||||
<h2>404</h2>
|
||||
<p class="what-happened">Page not found 😢</p>
|
||||
<p class="why-happened">
|
||||
It seems this page doesn't exist (yet). We're sorry about that.
|
||||
</p>
|
||||
<span class="back-you-go-then"><Buton url="/">Go back home</Buton></span>
|
||||
|
||||
<nav class="other-places">
|
||||
Looking for somewhere else?
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/search">Search</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/browse">Browse</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/about">About</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/lissy93/awesome-privacy">Source</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://as93.net">More Apps</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</article>
|
||||
|
||||
<nav class="other-places">
|
||||
Looking for somewhere else?
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/search">Search</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/browse">Browse</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/about">About</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/lissy93/awesome-privacy">Source</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://as93.net">More Apps</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</article>
|
||||
</Layout>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.oh-crap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: calc(100vh - 6rem);
|
||||
h2 {
|
||||
font-size: 12rem;
|
||||
margin: 0;
|
||||
}
|
||||
.what-happened {
|
||||
font-size: 4rem;
|
||||
margin: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.why-happened {
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.back-you-go-then :global(a) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
.oh-crap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: calc(100vh - 6rem);
|
||||
h2 {
|
||||
font-size: 12rem;
|
||||
margin: 0;
|
||||
}
|
||||
.what-happened {
|
||||
font-size: 4rem;
|
||||
margin: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.why-happened {
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.back-you-go-then :global(a) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
|
||||
.other-places {
|
||||
opacity: 0.8;
|
||||
margin: 2rem auto 1rem auto;
|
||||
text-align: center;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
gap: 0.5rem;
|
||||
li {
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid var(--accent);
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
li a {
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.other-places {
|
||||
opacity: 0.8;
|
||||
margin: 2rem auto 1rem auto;
|
||||
text-align: center;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
gap: 0.5rem;
|
||||
li {
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid var(--accent);
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
li a {
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,4 @@
|
|||
---
|
||||
|
||||
import Layout from '@layouts/Layout.astro';
|
||||
import SectionList from '@components/things/SectionList.astro';
|
||||
import Main from '@components/scafold/MainCard.astro';
|
||||
|
|
@ -29,129 +28,137 @@ export async function getStaticPaths() {
|
|||
});
|
||||
});
|
||||
|
||||
return pages.map(({ category, title, sections }) => {
|
||||
return {
|
||||
params: { category },
|
||||
props: { title, sections },
|
||||
};
|
||||
});
|
||||
return pages.map(({ category, title, sections }) => {
|
||||
return {
|
||||
params: { category },
|
||||
props: { title, sections },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const makeCarasolData = () => {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ItemList",
|
||||
"itemListElement": [
|
||||
sections.map((section, index) => ({
|
||||
"@type": "ListItem",
|
||||
"position": index + 1,
|
||||
"url": `https://awesomeprivacy.com/${slugify(title)}/${slugify(section.name)}`,
|
||||
"item": {
|
||||
"@type": "Service",
|
||||
"name": section.name,
|
||||
"url": `https://awesomeprivacy.com/${slugify(title)}/${slugify(section.name)}`,
|
||||
}
|
||||
})),
|
||||
]
|
||||
}
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ItemList',
|
||||
itemListElement: [
|
||||
sections.map((section, index) => ({
|
||||
'@type': 'ListItem',
|
||||
position: index + 1,
|
||||
url: `https://awesomeprivacy.com/${slugify(title)}/${slugify(section.name)}`,
|
||||
item: {
|
||||
'@type': 'Service',
|
||||
name: section.name,
|
||||
url: `https://awesomeprivacy.com/${slugify(title)}/${slugify(section.name)}`,
|
||||
},
|
||||
})),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const makeBreadcrumbs = () => {
|
||||
return [
|
||||
{ name: 'Home', item: '/' },
|
||||
{ name: title, item: slugify(title) },
|
||||
];
|
||||
return [
|
||||
{ name: 'Home', item: '/' },
|
||||
{ name: title, item: slugify(title) },
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
const makePaginationLinks = () => {
|
||||
const index = categories.findIndex(category => category.name === title);
|
||||
const previousCategory = index > 0 ? categories[index - 1].name : null;
|
||||
const nextCategory = index < categories.length - 1 ? categories[index + 1].name : null;
|
||||
return { previous: previousCategory, next: nextCategory };
|
||||
const index = categories.findIndex((category) => category.name === title);
|
||||
const previousCategory = index > 0 ? categories[index - 1].name : null;
|
||||
const nextCategory =
|
||||
index < categories.length - 1 ? categories[index + 1].name : null;
|
||||
return { previous: previousCategory, next: nextCategory };
|
||||
};
|
||||
|
||||
const { previous, next } = makePaginationLinks();
|
||||
|
||||
---
|
||||
|
||||
<Layout title={`${title} | Awesome Privacy`} breadcrumbs={makeBreadcrumbs()} customSchemaJson={makeCarasolData()}>
|
||||
<Layout
|
||||
title={`${title} | Awesome Privacy`}
|
||||
breadcrumbs={makeBreadcrumbs()}
|
||||
customSchemaJson={makeCarasolData()}
|
||||
>
|
||||
<Main>
|
||||
<div class="breadcrumbs">
|
||||
<span>
|
||||
<a href="/">Awesome Privacy</a>
|
||||
➔ <a href={`/${slugify(title)}`}>{title}</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="breadcrumbs">
|
||||
<span>
|
||||
<a href="/">Awesome Privacy</a>
|
||||
➔ <a href={`/${slugify(title)}`}>{title}</a>
|
||||
</span>
|
||||
</div>
|
||||
<SectionList title={title} sections={sections} bigTitle={true} />
|
||||
Or, <a href={`/all#${slugify(title)}`}>Browse All {title}</a>
|
||||
Or, <a href={`/all#${slugify(title)}`}>Browse All {title}</a>
|
||||
<div class="pagination-navigation">
|
||||
{ previous ? (
|
||||
<Button url={`/${slugify(previous)}`}>
|
||||
<span>← Previous</span>
|
||||
<p>{previous}</p>
|
||||
</Button>
|
||||
) : <p class="nothing"></p>}
|
||||
{ next && (
|
||||
<Button url={`/${slugify(next)}`}>
|
||||
<span>Next →</span>
|
||||
<p>{next}</p>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{
|
||||
previous ? (
|
||||
<Button url={`/${slugify(previous)}`}>
|
||||
<span>← Previous</span>
|
||||
<p>{previous}</p>
|
||||
</Button>
|
||||
) : (
|
||||
<p class="nothing" />
|
||||
)
|
||||
}
|
||||
{
|
||||
next && (
|
||||
<Button url={`/${slugify(next)}`}>
|
||||
<span>Next →</span>
|
||||
<p>{next}</p>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Main>
|
||||
</Layout>
|
||||
|
||||
<style lang="scss">
|
||||
.pagination-navigation {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 0;
|
||||
:global(.button) {
|
||||
min-width: 120px;
|
||||
width: fit-content;
|
||||
padding: 0.25rem 1rem;
|
||||
text-align: right;
|
||||
&:first-child { text-align: left; }
|
||||
p {
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
font-size: 1rem;
|
||||
}
|
||||
span {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
}
|
||||
.nothing {
|
||||
width: 120px;
|
||||
}
|
||||
.go-to-category {
|
||||
color: var(--foreground);
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
.pagination-navigation {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 0;
|
||||
:global(.button) {
|
||||
min-width: 120px;
|
||||
width: fit-content;
|
||||
padding: 0.25rem 1rem;
|
||||
text-align: right;
|
||||
&:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
font-size: 1rem;
|
||||
}
|
||||
span {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
.nothing {
|
||||
width: 120px;
|
||||
}
|
||||
.go-to-category {
|
||||
color: var(--foreground);
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
opacity: 0.5;
|
||||
font-size: 0.8rem;
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
a {
|
||||
color: var(--foreground);
|
||||
transition: all 0.15s ease-in-out;
|
||||
|
||||
.breadcrumbs {
|
||||
opacity: 0.5;
|
||||
font-size: 0.8rem;
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
a {
|
||||
color: var(--foreground);
|
||||
transition: all 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
@media(max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,359 +1,478 @@
|
|||
---
|
||||
|
||||
import Layout from '@layouts/Layout.astro';
|
||||
import Main from '@components/scafold/MainCard.astro';
|
||||
import Icon from '@components/form/Icon.astro';
|
||||
import SocialShare from '@components/form/Social.astro';
|
||||
import { parseMarkdown } from '@utils/parse-markdown';
|
||||
import { authorProjects, authorSocials, aboutOurData, projectRequirements, appDescription } from '../site-config';
|
||||
import {
|
||||
authorProjects,
|
||||
authorSocials,
|
||||
aboutOurData,
|
||||
projectRequirements,
|
||||
appDescription,
|
||||
} from '../site-config';
|
||||
|
||||
const contributorsResource = async () => {
|
||||
const url = 'https://api.github.com/repos/lissy93/personal-security-checklist/contributors?per_page=100';
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch contributors');
|
||||
}
|
||||
interface GitHubContributor {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
html_url: string;
|
||||
}
|
||||
|
||||
interface Sponsor {
|
||||
login: string;
|
||||
avatarUrl: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const githubHeaders: Record<string, string> = {
|
||||
'User-Agent': 'awesome-privacy',
|
||||
};
|
||||
const apiKey = import.meta.env.GITHUB_API_KEY;
|
||||
if (apiKey) {
|
||||
githubHeaders['Authorization'] = `Bearer ${apiKey}`;
|
||||
}
|
||||
|
||||
const contributorsResource = async (): Promise<GitHubContributor[] | null> => {
|
||||
const url =
|
||||
'https://api.github.com/repos/lissy93/personal-security-checklist/contributors?per_page=100';
|
||||
const response = await fetch(url, { headers: githubHeaders });
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
const sponsorsResource = async () => {
|
||||
const sponsorsResource = async (): Promise<Sponsor[] | null> => {
|
||||
const url = 'https://github-sponsors.as93.workers.dev/lissy93';
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch sponsors');
|
||||
}
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
const contributors = await contributorsResource();
|
||||
const sponsors = await sponsorsResource();
|
||||
|
||||
const licenseContent = async () => {
|
||||
const url = 'https://raw.githubusercontent.com/Lissy93/awesome-privacy/HEAD/LICENSE';
|
||||
const url =
|
||||
'https://raw.githubusercontent.com/Lissy93/awesome-privacy/HEAD/LICENSE';
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch license');
|
||||
}
|
||||
return await response.text();
|
||||
};
|
||||
|
||||
---
|
||||
|
||||
<Layout title="About | Awesome Privacy" description={appDescription}>
|
||||
<div class="about-page">
|
||||
<Main>
|
||||
<h2 id="objective">Objective</h2>
|
||||
<p>
|
||||
Large data-hungry corporations dominate the digital world but with little,
|
||||
or no respect for your privacy.
|
||||
We believe that privacy is a fundamental human right and that it should be protected.
|
||||
<br /><br />
|
||||
Migrating to open-source applications and those with a strong emphasis on
|
||||
privacy & security will help safegaurd you from corporations,
|
||||
governments, and hackers who log, store or sell your sensetive personal data.
|
||||
<br /><br />
|
||||
Awesome Privacy is a directory of alternative software which respects your privacy.
|
||||
<br /><br />
|
||||
|
||||
</p>
|
||||
<hr />
|
||||
<Main>
|
||||
<h2 id="objective">Objective</h2>
|
||||
<p>
|
||||
Large data-hungry corporations dominate the digital world but with
|
||||
little, or no respect for your privacy. We believe that privacy is a
|
||||
fundamental human right and that it should be protected.
|
||||
<br /><br />
|
||||
Migrating to open-source applications and those with a strong emphasis on
|
||||
privacy & security will help safegaurd you from corporations, governments,
|
||||
and hackers who log, store or sell your sensetive personal data.
|
||||
<br /><br />
|
||||
Awesome Privacy is a directory of alternative software which respects your
|
||||
privacy.
|
||||
<br /><br />
|
||||
</p>
|
||||
<hr />
|
||||
|
||||
<h2 id="creteria">Software Requirements</h2>
|
||||
<div class="software-requirements"><p set:html={parseMarkdown(projectRequirements)}></p></div>
|
||||
<hr />
|
||||
<h2 id="creteria">Software Requirements</h2>
|
||||
<div class="software-requirements">
|
||||
<p set:html={parseMarkdown(projectRequirements)} />
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<h2 id="our-data">Our Data</h2>
|
||||
<div class="about-the-data"><p set:html={parseMarkdown(aboutOurData)}></p></div>
|
||||
<hr />
|
||||
<h2 id="our-data">Our Data</h2>
|
||||
<div class="about-the-data">
|
||||
<p set:html={parseMarkdown(aboutOurData)} />
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<h2 id="contributing">Contributing</h2>
|
||||
<p>
|
||||
Awesome Privacy (including all data and code) is fully open source,
|
||||
maintained by a core group of volenteers, with a lot of help from the community.
|
||||
<br /><br />
|
||||
We welcome suggestions, additions, edits and removals to the list.<br />
|
||||
It's thanks to contributors like you that this project is possible 💜
|
||||
<br /><br />
|
||||
To get started, head over to our <a href="https://github.com/lissy93/awesome-privacy">GitHub repository</a>.
|
||||
From here, you'll be able to edit <a href="#"><code>awesome-privacy.yml</code></a>
|
||||
where all data is stored, and then submit your changes as a pull request.
|
||||
<br /><br />
|
||||
If you're new to open source, you can find some resources to get you started
|
||||
at <a href="https://git-in.to">git-in.to</a>, but feel free to reach out if you need any help 😊
|
||||
</p>
|
||||
<hr />
|
||||
<h2 id="contributing">Contributing</h2>
|
||||
<p>
|
||||
Awesome Privacy (including all data and code) is fully open source,
|
||||
maintained by a core group of volenteers, with a lot of help from the
|
||||
community.
|
||||
<br /><br />
|
||||
We welcome suggestions, additions, edits and removals to the list.<br />
|
||||
It's thanks to contributors like you that this project is possible 💜
|
||||
<br /><br />
|
||||
To get started, head over to our <a
|
||||
href="https://github.com/lissy93/awesome-privacy">GitHub repository</a
|
||||
>. From here, you'll be able to edit <a href="#"
|
||||
><code>awesome-privacy.yml</code></a
|
||||
>
|
||||
where all data is stored, and then submit your changes as a pull request.
|
||||
<br /><br />
|
||||
If you're new to open source, you can find some resources to get you started
|
||||
at <a href="https://git-in.to">git-in.to</a>, but feel free to reach out
|
||||
if you need any help 😊
|
||||
</p>
|
||||
<hr />
|
||||
|
||||
<h2 id="acknowledgements">Acknowledgements</h2>
|
||||
<h3 id="sponsors">Sponsors</h3>
|
||||
<p>Huge thanks to the following sponsors, for their ongoing support 💖</p>
|
||||
<div class="user-list">
|
||||
{sponsorsResource().then((sponsors) => {
|
||||
return sponsors.map((sponsor: any) => (
|
||||
<a class="user" href={`https://github.com/${sponsor.login}`} target="_blank" rel="noreferrer">
|
||||
<img src={sponsor.avatarUrl} alt={sponsor.login} />
|
||||
<p>{sponsor.name || sponsor.login}</p>
|
||||
</a>
|
||||
));
|
||||
})}
|
||||
</div>
|
||||
<h2 id="acknowledgements">Acknowledgements</h2>
|
||||
<h3 id="sponsors">Sponsors</h3>
|
||||
<p>Huge thanks to the following sponsors, for their ongoing support 💖</p>
|
||||
{
|
||||
sponsors ? (
|
||||
<div class="user-list">
|
||||
{sponsors.map((sponsor) => (
|
||||
<a
|
||||
class="user"
|
||||
href={`https://github.com/${sponsor.login}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img src={sponsor.avatarUrl} alt={sponsor.login} />
|
||||
<p>{sponsor.name || sponsor.login}</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
class="fallback-img"
|
||||
src="https://readme-contribs.as93.net/sponsors/lissy93?perRow=12&shape=squircle&textColor=ffffff&limit=96"
|
||||
alt="Sponsors"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<h3 id="contributors">Contributors</h3>
|
||||
<p>
|
||||
This project exists thanks to all the people who've helped build and maintain it.<br />
|
||||
Special thanks to the below, top-100 contributors 🌟
|
||||
</p>
|
||||
<div class="user-list">
|
||||
{contributorsResource().then((sponsors) => {
|
||||
return sponsors.map((sponsor: any) => (
|
||||
<a class="user" href={sponsor.html_url} target="_blank" rel="noreferrer">
|
||||
<img src={sponsor.avatar_url} alt={sponsor.login} />
|
||||
<p>{sponsor.login}</p>
|
||||
</a>
|
||||
));
|
||||
})}
|
||||
</div>
|
||||
<h3 id="contributors">Contributors</h3>
|
||||
<p>
|
||||
This project exists thanks to all the people who've helped build and
|
||||
maintain it.<br />
|
||||
Special thanks to the below, top-100 contributors 🌟
|
||||
</p>
|
||||
{
|
||||
contributors ? (
|
||||
<div class="user-list">
|
||||
{contributors.map((contributor) => (
|
||||
<a
|
||||
class="user"
|
||||
href={contributor.html_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img src={contributor.avatar_url} alt={contributor.login} />
|
||||
<p>{contributor.login}</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
class="fallback-img"
|
||||
src="https://readme-contribs.as93.net/contributors/lissy93/awesome-privacy?perRow=12&shape=squircle&textColor=ffffff&limit=96"
|
||||
alt="Contributors"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<hr />
|
||||
<hr />
|
||||
|
||||
<h2 id="support-us">Help us out</h2>
|
||||
<p>
|
||||
Awesome Privacy is a free, open source and community-maintained resource.<br />
|
||||
There's a few ways you can support us:
|
||||
<ul class="help-list">
|
||||
<li>Visiting, forking, or starring or our <a href="https://github.com/lissy93/awesome-privacy">GitHub repository</a></li>
|
||||
<li>Help us keep our info up-to-date, but submitting an addition, removal or ammendment</li>
|
||||
<li>Leave feedback on services that you've used, to help others make a more informed decission</li>
|
||||
<li>Consider <a href="https://github.com/sponsors/Lissy93">sponsoring us on GitHub</a>, if you're able 💖</li>
|
||||
<li>Share Awesome Privacy with your network</li>
|
||||
</ul>
|
||||
<SocialShare />
|
||||
</p>
|
||||
<h2 id="support-us">Help us out</h2>
|
||||
<p>
|
||||
Awesome Privacy is a free, open source and community-maintained
|
||||
resource.<br />
|
||||
There's a few ways you can support us:
|
||||
<ul class="help-list">
|
||||
<li>
|
||||
Visiting, forking, or starring or our <a
|
||||
href="https://github.com/lissy93/awesome-privacy"
|
||||
>GitHub repository</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
Help us keep our info up-to-date, but submitting an addition,
|
||||
removal or ammendment
|
||||
</li>
|
||||
<li>
|
||||
Leave feedback on services that you've used, to help others make a
|
||||
more informed decission
|
||||
</li>
|
||||
<li>
|
||||
Consider <a href="https://github.com/sponsors/Lissy93"
|
||||
>sponsoring us on GitHub</a
|
||||
>, if you're able 💖
|
||||
</li>
|
||||
<li>Share Awesome Privacy with your network</li>
|
||||
</ul>
|
||||
<SocialShare />
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
<hr />
|
||||
|
||||
<h2 id="author">Author</h2>
|
||||
<article class="author">
|
||||
<h2 id="author">Author</h2>
|
||||
<article class="author">
|
||||
<p>
|
||||
This project was originally started by
|
||||
me, <a href="https://aliciasykes.com">Alicia Sykes</a>
|
||||
This project was originally started by me, <a
|
||||
href="https://aliciasykes.com">Alicia Sykes</a
|
||||
>
|
||||
(with a lot of help from the community)
|
||||
</p>
|
||||
<br />
|
||||
<p class="about">
|
||||
I build apps which aim to help people escape big tech, secure their data,
|
||||
and protect their privacy.
|
||||
I build apps which aim to help people escape big tech, secure their
|
||||
data, and protect their privacy.
|
||||
</p>
|
||||
<br />
|
||||
<div class="pic-and-socials">
|
||||
<a href="https://aliciasykes.com">
|
||||
<img class="pic" width="180" height="240" alt="Alicia Sykes" src="https://i.ibb.co/fq10qhL/DSC-0597.jpg" />
|
||||
<img
|
||||
class="pic"
|
||||
width="180"
|
||||
height="240"
|
||||
alt="Alicia Sykes"
|
||||
src="https://cdn.as93.net/profile-pictures/dsc_0597"
|
||||
/>
|
||||
</a>
|
||||
<div class="socials">
|
||||
{
|
||||
authorSocials.map((social: { link: string; color: string; icon: string; }) => (
|
||||
<a href={social.link} style={`--color: ${social.color}`} target="_blank">
|
||||
<Icon icon={social.icon} width={24} height={24} />
|
||||
</a>
|
||||
))
|
||||
authorSocials.map(
|
||||
(social: { link: string; color: string; icon: string }) => (
|
||||
<a
|
||||
href={social.link}
|
||||
style={`--color: ${social.color}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Icon icon={social.icon} width={24} height={24} />
|
||||
</a>
|
||||
),
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<p class="more-about">
|
||||
I have a particular interest in self-hosting, Linux, security and OSINT.<br />
|
||||
I have a particular interest in self-hosting, Linux, security and
|
||||
OSINT.<br />
|
||||
So if this type of stuff interests you, check out these other projects:
|
||||
</p>
|
||||
<ul>
|
||||
{
|
||||
authorProjects.map((project: { title: string; icon: string; link: string; description: string; }) => (
|
||||
<li>
|
||||
<img width="20" height="20" alt={project.title} src={project.icon} />
|
||||
<a href={project.link} target="_blank" rel="noreferrer">
|
||||
{project.title}
|
||||
</a> - {project.description}
|
||||
</li>
|
||||
))
|
||||
authorProjects.map(
|
||||
(project: {
|
||||
title: string;
|
||||
icon: string;
|
||||
link: string;
|
||||
description: string;
|
||||
}) => (
|
||||
<li>
|
||||
<img
|
||||
width="20"
|
||||
height="20"
|
||||
alt={project.title}
|
||||
src={project.icon}
|
||||
/>
|
||||
<a href={project.link} target="_blank" rel="noreferrer">
|
||||
{project.title}
|
||||
</a>{' '}
|
||||
- {project.description}
|
||||
</li>
|
||||
),
|
||||
)
|
||||
}
|
||||
<li>
|
||||
...and loads more, over at <a href="https://apps.aliciasykes.com/">apps.aliciasykes.com</a>
|
||||
...and loads more, over at <a href="https://apps.aliciasykes.com/"
|
||||
>apps.aliciasykes.com</a
|
||||
>
|
||||
and on <a href="https://github.com/lissy93">github.com/lissy93</a>
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
<p>
|
||||
If you'd like to support the ongoing work on Awesome Privacy,
|
||||
and other similar projects,
|
||||
consider <a href="https://github.com/sponsors/lissy93">sponsoring me</a> on GitHub 💖
|
||||
If you'd like to support the ongoing work on Awesome Privacy, and
|
||||
other similar projects, consider <a
|
||||
href="https://github.com/sponsors/lissy93">sponsoring me</a
|
||||
> on GitHub 💖
|
||||
</p>
|
||||
</article>
|
||||
|
||||
</article>
|
||||
|
||||
<hr />
|
||||
<hr />
|
||||
|
||||
<h2 id="license">License</h2>
|
||||
<p>
|
||||
All content on Awesome Privacy is freely available, within the public domain,
|
||||
licensed under Creative Commons Zero v1.0 Universal.
|
||||
The code for the website is licensed under MIT.
|
||||
</p>
|
||||
<p>
|
||||
{licenseContent().then((license) => {
|
||||
return (<pre class="license-content">{license}</pre>)
|
||||
})}
|
||||
</p>
|
||||
|
||||
</Main>
|
||||
<h2 id="license">License</h2>
|
||||
<p>
|
||||
All content on Awesome Privacy is freely available, within the public
|
||||
domain, licensed under Creative Commons Zero v1.0 Universal. The code
|
||||
for the website is licensed under MIT.
|
||||
</p>
|
||||
<p>
|
||||
{
|
||||
licenseContent().then((license) => {
|
||||
return <pre class="license-content">{license}</pre>;
|
||||
})
|
||||
}
|
||||
</p>
|
||||
</Main>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style lang="scss">
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
margin: 2rem 0 1rem 0;
|
||||
}
|
||||
p {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
hr {
|
||||
margin: 2rem 0;
|
||||
border: 0;
|
||||
border-top: 2px solid var(--accent);
|
||||
&:nth-child(6) {
|
||||
border-color: var(--accent-3);
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
margin: 1rem 0 0.5rem 0;
|
||||
}
|
||||
&:nth-child(9) {
|
||||
border-color: var(--accent-2);
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.software-requirements, .about-the-data {
|
||||
:global(p) {
|
||||
font-size: 1.2rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
:global(ul) {
|
||||
padding-left: 0.5rem;
|
||||
list-style: none;
|
||||
font-size: 1.2rem;
|
||||
:global(li ul) {
|
||||
padding: 0 0 0.5rem 1rem;
|
||||
list-style: circle;
|
||||
}
|
||||
|
||||
}
|
||||
:global(h3) {
|
||||
font-size: 1.6rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
:global(strong) {
|
||||
font-weight: 500;
|
||||
}
|
||||
:global(small) {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
:global(a) {
|
||||
color: var(--accent-3);
|
||||
}
|
||||
}
|
||||
:global(code) {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
background: #acabb782;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: var(--curve-sm);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.user-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin: 2rem 0;
|
||||
.user {
|
||||
width: 6rem;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
img {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
border-radius: var(--curve-md);
|
||||
}
|
||||
p {
|
||||
font-size: 1rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.license-content {
|
||||
max-height: 500px;
|
||||
overflow: scroll;
|
||||
background: var(--background-form);
|
||||
width: fit-content;
|
||||
border-radius: var(--curve-sm);
|
||||
padding: 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
font-family: mono;
|
||||
max-width: 100vw;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.help-list {
|
||||
padding-left: 1rem;
|
||||
list-style: circle;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.author {
|
||||
p {
|
||||
margin: 1rem 0 0 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
ul {
|
||||
padding-left: 1.5rem;
|
||||
hr {
|
||||
margin: 2rem 0;
|
||||
border: 0;
|
||||
border-top: 2px solid var(--accent);
|
||||
&:nth-child(6) {
|
||||
border-color: var(--accent-3);
|
||||
}
|
||||
&:nth-child(9) {
|
||||
border-color: var(--accent-2);
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.software-requirements,
|
||||
.about-the-data {
|
||||
:global(p) {
|
||||
font-size: 1.2rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
:global(ul) {
|
||||
padding-left: 0.5rem;
|
||||
list-style: none;
|
||||
font-size: 1.2rem;
|
||||
:global(li ul) {
|
||||
padding: 0 0 0.5rem 1rem;
|
||||
list-style: circle;
|
||||
}
|
||||
}
|
||||
:global(h3) {
|
||||
font-size: 1.6rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
:global(strong) {
|
||||
font-weight: 500;
|
||||
}
|
||||
:global(small) {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
:global(a) {
|
||||
color: var(--accent-3);
|
||||
}
|
||||
}
|
||||
:global(code) {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
background: #acabb782;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: var(--curve-sm);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.user-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
margin: 2rem 0;
|
||||
.user {
|
||||
width: 6rem;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--curve-sm);
|
||||
transition: background 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background: var(--background-form);
|
||||
}
|
||||
img {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
border-radius: var(--curve-md);
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem auto 0 auto;
|
||||
font-size: 1rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fallback-img {
|
||||
max-width: 100%;
|
||||
margin: 2rem 0;
|
||||
border-radius: var(--curve-sm);
|
||||
}
|
||||
|
||||
.license-content {
|
||||
max-height: 500px;
|
||||
overflow: scroll;
|
||||
background: var(--background-form);
|
||||
width: fit-content;
|
||||
border-radius: var(--curve-sm);
|
||||
padding: 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
font-family: 'Lekton', monospace;
|
||||
max-width: 100vw;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.help-list {
|
||||
padding-left: 1rem;
|
||||
list-style: circle;
|
||||
margin-bottom: 0;
|
||||
li {
|
||||
font-size: 1.1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.author {
|
||||
p {
|
||||
margin: 1rem 0 0 0;
|
||||
}
|
||||
}
|
||||
.about {
|
||||
font-size: 1.4rem;
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: var(--accent-3);
|
||||
text-align: center;
|
||||
max-width: 550px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.pic-and-socials {
|
||||
float:right;
|
||||
img {
|
||||
margin: 0.5rem 1rem;
|
||||
border-radius: var(--curve-md);
|
||||
border: 2px solid var(--box-outline);
|
||||
box-shadow: 4px 4px 0 var(--box-outline);
|
||||
ul {
|
||||
padding-left: 1.5rem;
|
||||
list-style: circle;
|
||||
margin-bottom: 0;
|
||||
li {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
.socials {
|
||||
display: flex;
|
||||
margin: 0.5rem 1rem;
|
||||
justify-content: space-between;
|
||||
:global(a svg) {
|
||||
color: var(--foreground);
|
||||
transition: color 0.2s ease-in-out;
|
||||
&:hover {
|
||||
color: var(--color);
|
||||
.about {
|
||||
font-size: 1.4rem;
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
color: var(--accent-3);
|
||||
text-align: center;
|
||||
max-width: 550px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.pic-and-socials {
|
||||
float: right;
|
||||
img {
|
||||
margin: 0.5rem 1rem;
|
||||
border-radius: var(--curve-md);
|
||||
border: 2px solid var(--box-outline);
|
||||
box-shadow: 4px 4px 0 var(--box-outline);
|
||||
}
|
||||
.socials {
|
||||
display: flex;
|
||||
margin: 0.5rem 1rem;
|
||||
justify-content: space-between;
|
||||
:global(a svg) {
|
||||
color: var(--foreground);
|
||||
transition: color 0.2s ease-in-out;
|
||||
&:hover {
|
||||
color: var(--color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -3,19 +3,22 @@ import yaml from 'js-yaml';
|
|||
|
||||
import type { AwesomePrivacy } from '../../types/Service';
|
||||
|
||||
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';
|
||||
|
||||
export const GET: APIRoute = async () => {
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
const yamlObject = yaml.load(yamlContent) as AwesomePrivacy;
|
||||
|
||||
return new Response(
|
||||
JSON.stringify(yamlObject), { headers: { 'content-type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
return new Response(JSON.stringify(yamlObject), {
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,43 +3,45 @@ import Layout from '@layouts/Layout.astro';
|
|||
import Button from '@components/form/Button.astro';
|
||||
|
||||
import MainCard from '@components/scafold/MainCard.astro';
|
||||
|
||||
---
|
||||
|
||||
<Layout title="API Docs | Awesome Privacy">
|
||||
|
||||
<MainCard>
|
||||
<div id="swagger-ui"></div>
|
||||
<div class="go">
|
||||
<Button url="https://api.awesome-privacy.xyz" text="Try it Out" />
|
||||
</div>
|
||||
</MainCard>
|
||||
|
||||
<MainCard>
|
||||
<div id="swagger-ui"></div>
|
||||
<div class="go">
|
||||
<Button url="https://api.awesome-privacy.xyz" text="Try it Out" />
|
||||
</div>
|
||||
</MainCard>
|
||||
</Layout>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.1.3/swagger-ui.css">
|
||||
<script is:inline src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.1.3/swagger-ui-bundle.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.1.3/swagger-ui.css"
|
||||
/>
|
||||
<script
|
||||
is:inline
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.1.3/swagger-ui-bundle.js"
|
||||
></script>
|
||||
<script type="module" is:inline>
|
||||
window.onload = () => {
|
||||
SwaggerUIBundle({
|
||||
url: 'https://raw.githubusercontent.com/Lissy93/awesome-privacy/main/api/open-api-spec.yml',
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
]
|
||||
});
|
||||
}
|
||||
window.onload = () => {
|
||||
SwaggerUIBundle({
|
||||
url: 'https://raw.githubusercontent.com/Lissy93/awesome-privacy/main/api/open-api-spec.yml',
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset,
|
||||
],
|
||||
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.go {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.go {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
---
|
||||
|
||||
<Layout title="Browse">
|
||||
<div class="head-wrap">
|
||||
<h2>Browse</h2>
|
||||
<span>
|
||||
<input
|
||||
id="searchInput"
|
||||
type="search"
|
||||
placeholder="Start typing to filter..."
|
||||
/>
|
||||
<p id="press-enter-msg" class="press-enter">
|
||||
Press enter for deep search
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<p class="sitemap-link">
|
||||
Not sure what you're looking for? Take a look through <a href="/all"
|
||||
>all listings</a
|
||||
>
|
||||
</p>
|
||||
|
||||
<div class="head-wrap">
|
||||
<h2>Browse</h2>
|
||||
<span>
|
||||
<input id="searchInput" type="search" placeholder="Start typing to filter..." />
|
||||
<p id="press-enter-msg" class="press-enter">Press enter for deep search</p>
|
||||
</span>
|
||||
</div>
|
||||
<p class="sitemap-link">Not sure what you're looking for? Take a look through <a href="/all">all listings</a></p>
|
||||
<ul class="categories">
|
||||
{
|
||||
categories.map((category) => (
|
||||
<li class="category">
|
||||
<SectionList title={category.name} sections={category.sections} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
|
||||
<ul class="categories">
|
||||
{categories.map((category) => (
|
||||
<li class="category">
|
||||
<SectionList title={category.name} sections={category.sections} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div class="no-results">
|
||||
<p class="zilch">Nothing found 😢</p>
|
||||
<p>Try a <a href="/search">deep search</a> instead</p>
|
||||
</div>
|
||||
<div class="no-results">
|
||||
<p class="zilch">Nothing found 😢</p>
|
||||
<p>Try a <a href="/search">deep search</a> instead</p>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
// Time for some good ol' fashioned vanilla JS...
|
||||
// I reccomend you not to look at this code for too long, it's not pretty.
|
||||
const filterInput = document.querySelector('input');
|
||||
const categories = document.querySelectorAll<HTMLElement>('.category');
|
||||
const pressEnterMsg = document.getElementById('press-enter-msg') as HTMLElement | null;
|
||||
const noResults = document.querySelector('.no-results') as HTMLElement | null;
|
||||
// Time for some good ol' fashioned vanilla JS...
|
||||
// I reccomend you not to look at this code for too long, it's not pretty.
|
||||
const filterInput = document.querySelector('input');
|
||||
const categories = document.querySelectorAll<HTMLElement>('.category');
|
||||
const pressEnterMsg = document.getElementById(
|
||||
'press-enter-msg',
|
||||
) as HTMLElement | null;
|
||||
const noResults = document.querySelector('.no-results') as HTMLElement | null;
|
||||
|
||||
if (!pressEnterMsg || !noResults) {
|
||||
throw new Error('No pressEnterMsg or noResults');
|
||||
};
|
||||
if (!pressEnterMsg || !noResults) {
|
||||
throw new Error('No pressEnterMsg or noResults');
|
||||
}
|
||||
|
||||
filterInput?.addEventListener('input', (e) => {
|
||||
let resultsCount = 0;
|
||||
const filter = (e.target as HTMLInputElement).value.toLowerCase();
|
||||
if (filter.length > 0) {
|
||||
pressEnterMsg.style.visibility = 'visible';
|
||||
} else {
|
||||
pressEnterMsg.style.visibility = 'hidden';
|
||||
}
|
||||
categories.forEach((category) => {
|
||||
const titleElement = category.querySelector('.category-title');
|
||||
const title = titleElement ? titleElement.textContent?.toLowerCase() : '';
|
||||
const sections = category.querySelectorAll<HTMLElement>('.section');
|
||||
let count = 0;
|
||||
sections.forEach((section) => {
|
||||
const sectionTitle = section.textContent?.toLowerCase();
|
||||
if (sectionTitle?.includes(filter)) {
|
||||
section.style.display = 'block';
|
||||
count++;
|
||||
resultsCount++;
|
||||
} else {
|
||||
section.style.display = 'none';
|
||||
}
|
||||
});
|
||||
if (title && title.includes(filter)) {
|
||||
category.style.display = 'inline-flex';
|
||||
resultsCount++;
|
||||
} else if (count === 0) {
|
||||
category.style.display = 'none';
|
||||
}
|
||||
});
|
||||
noResults.style.display = resultsCount === 0 ? 'block' : 'none';
|
||||
});
|
||||
filterInput?.addEventListener('input', (e) => {
|
||||
let resultsCount = 0;
|
||||
const filter = (e.target as HTMLInputElement).value.toLowerCase();
|
||||
if (filter.length > 0) {
|
||||
pressEnterMsg.style.visibility = 'visible';
|
||||
} else {
|
||||
pressEnterMsg.style.visibility = 'hidden';
|
||||
}
|
||||
categories.forEach((category) => {
|
||||
const titleElement = category.querySelector('.category-title');
|
||||
const title = titleElement ? titleElement.textContent?.toLowerCase() : '';
|
||||
const sections = category.querySelectorAll<HTMLElement>('.section');
|
||||
let count = 0;
|
||||
sections.forEach((section) => {
|
||||
const sectionTitle = section.textContent?.toLowerCase();
|
||||
if (sectionTitle?.includes(filter)) {
|
||||
section.style.display = 'block';
|
||||
count++;
|
||||
resultsCount++;
|
||||
} else {
|
||||
section.style.display = 'none';
|
||||
}
|
||||
});
|
||||
if (title && title.includes(filter)) {
|
||||
category.style.display = 'inline-flex';
|
||||
resultsCount++;
|
||||
} else if (count === 0) {
|
||||
category.style.display = 'none';
|
||||
}
|
||||
});
|
||||
noResults.style.display = resultsCount === 0 ? 'block' : 'none';
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchInput = document.getElementById('searchInput') as HTMLInputElement | null;
|
||||
if (searchInput === null) return;
|
||||
searchInput.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
window.location.href = `/search/${encodeURIComponent(searchInput.value.trim())}`;
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
searchInput.value = '';
|
||||
searchInput.blur();
|
||||
pressEnterMsg.style.visibility = 'hidden';
|
||||
categories.forEach((category) => {
|
||||
category.style.display = 'inline-flex';
|
||||
const sections = category.querySelectorAll<HTMLElement>('.section');
|
||||
sections.forEach((section) => {
|
||||
section.style.display = 'block';
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const searchInput = document.getElementById(
|
||||
'searchInput',
|
||||
) as HTMLInputElement | null;
|
||||
if (searchInput === null) return;
|
||||
searchInput.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
window.location.href = `/search/${encodeURIComponent(searchInput.value.trim())}`;
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
searchInput.value = '';
|
||||
searchInput.blur();
|
||||
pressEnterMsg.style.visibility = 'hidden';
|
||||
categories.forEach((category) => {
|
||||
category.style.display = 'inline-flex';
|
||||
const sections = category.querySelectorAll<HTMLElement>('.section');
|
||||
sections.forEach((section) => {
|
||||
section.style.display = 'block';
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.head-wrap {
|
||||
margin: 1rem auto 0 auto;
|
||||
width: 85vw;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
.press-enter {
|
||||
margin: 0.5rem 0 0 0;
|
||||
opacity: 0.5;
|
||||
visibility: hidden;
|
||||
}
|
||||
input {
|
||||
padding: 0.5rem;
|
||||
border: 2px solid var(--box-outline);
|
||||
border-radius: var(--curve-sm);
|
||||
font-size: 1.2rem;
|
||||
box-shadow: 3px 3px 0 var(--box-outline);
|
||||
color: var(--accent-3);
|
||||
background: var(--background-form);
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 4px 4px 0 var(--box-outline);
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
font-family: 'Lekton', sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 3rem;
|
||||
margin: 0;
|
||||
color: var(--accent-3);
|
||||
}
|
||||
}
|
||||
|
||||
.head-wrap {
|
||||
margin: 1rem auto 0 auto;
|
||||
width: 85vw;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
.press-enter {
|
||||
margin: 0.5rem 0 0 0;
|
||||
opacity: 0.5;
|
||||
visibility: hidden;
|
||||
}
|
||||
input {
|
||||
padding: 0.5rem;
|
||||
border: 2px solid var(--box-outline);
|
||||
border-radius: var(--curve-sm);
|
||||
font-size: 1.2rem;
|
||||
box-shadow: 3px 3px 0 var(--box-outline);
|
||||
color: var(--accent-3);
|
||||
background: var(--background-form);
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 4px 4px 0 var(--box-outline);
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
font-family: "Lekton", sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 3rem;
|
||||
margin: 0;
|
||||
color: var(--accent-3);
|
||||
}
|
||||
}
|
||||
.sitemap-link {
|
||||
margin: 0 auto;
|
||||
width: 85vw;
|
||||
a {
|
||||
color: var(--accent-3);
|
||||
}
|
||||
}
|
||||
|
||||
.sitemap-link {
|
||||
margin: 0 auto;
|
||||
width: 85vw;
|
||||
a {
|
||||
color: var(--accent-3);
|
||||
}
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
width: 85vw;
|
||||
margin: 2rem auto 4rem auto;
|
||||
display: none;
|
||||
.zilch {
|
||||
font-size: 2rem;
|
||||
color: var(--accent-3);
|
||||
opacity: 0.5;
|
||||
font-weight: bold;
|
||||
}
|
||||
p {
|
||||
margin: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.categories {
|
||||
columns: 6 300px;
|
||||
column-gap: 1rem;
|
||||
margin: 0 auto;
|
||||
width: 85vw;
|
||||
max-width: 1800px;
|
||||
padding: 1rem 0 2rem 0;
|
||||
@media(max-width: 768px) {
|
||||
padding: 0;
|
||||
width: 95%;
|
||||
}
|
||||
.category {
|
||||
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%;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
margin: 1rem;
|
||||
.category-title {
|
||||
text-decoration: none;
|
||||
color: var(--foreground);
|
||||
}
|
||||
h3 {
|
||||
font-family: "Lekton", sans-serif;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
.sections {
|
||||
padding-left: 1rem;
|
||||
.section {
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--foreground);
|
||||
}
|
||||
.service-count {
|
||||
color: var(--accent-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.no-results {
|
||||
text-align: center;
|
||||
width: 85vw;
|
||||
margin: 2rem auto 4rem auto;
|
||||
display: none;
|
||||
.zilch {
|
||||
font-size: 2rem;
|
||||
color: var(--accent-3);
|
||||
opacity: 0.5;
|
||||
font-weight: bold;
|
||||
}
|
||||
p {
|
||||
margin: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.categories {
|
||||
columns: 6 300px;
|
||||
column-gap: 1rem;
|
||||
margin: 0 auto;
|
||||
width: 85vw;
|
||||
max-width: 1800px;
|
||||
padding: 1rem 0 2rem 0;
|
||||
@media (max-width: 768px) {
|
||||
padding: 0;
|
||||
width: 95%;
|
||||
}
|
||||
.category {
|
||||
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%;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
margin: 1rem;
|
||||
.category-title {
|
||||
text-decoration: none;
|
||||
color: var(--foreground);
|
||||
}
|
||||
h3 {
|
||||
font-family: 'Lekton', sans-serif;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
.sections {
|
||||
padding-left: 1rem;
|
||||
.section {
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--foreground);
|
||||
}
|
||||
.service-count {
|
||||
color: var(--accent-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
---
|
||||
|
||||
<Layout title="Home | Awesome Privacy" hideNav={true} description={description}>
|
||||
|
|
@ -29,11 +28,13 @@ const description = 'Privacy is a fundamental human right; '
|
|||
<Search client:visible data={categories} />
|
||||
<h2 class="browse-title"><a href="/browse">Browse</a></h2>
|
||||
<ul class="categories">
|
||||
{categories.map((category) => (
|
||||
<li class="category">
|
||||
<SectionList title={category.name} sections={category.sections} />
|
||||
</li>
|
||||
))}
|
||||
{
|
||||
categories.map((category) => (
|
||||
<li class="category">
|
||||
<SectionList title={category.name} sections={category.sections} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<div class="view-all">
|
||||
<span>Or, just</span>
|
||||
|
|
@ -48,28 +49,33 @@ const description = 'Privacy is a fundamental human right; '
|
|||
</h2>
|
||||
<div class="about-summary">
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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 <a href="https://github.com/lissy93/awesome-privacy">open source</a>.
|
||||
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 <a href="https://github.com/lissy93/awesome-privacy"
|
||||
>open source</a
|
||||
>.
|
||||
</p>
|
||||
<p>
|
||||
If you've found Awesome Privacy useful, help us out by sharing it with others,
|
||||
contributing, or consider <a href="https://github.com/sponsors/lissy93">sponsoring me</a> on GitHub.
|
||||
If you've found Awesome Privacy useful, help us out by sharing it with
|
||||
others, contributing, or consider <a
|
||||
href="https://github.com/sponsors/lissy93">sponsoring me</a
|
||||
> on GitHub.
|
||||
</p>
|
||||
</div>
|
||||
<div class="view-all">
|
||||
<span>Want to learn more?</span>
|
||||
<Button url="/about" text="Keep Reading..." />
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
|
|
@ -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; '
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
<Layout title="Saved Services">
|
||||
<main>
|
||||
<h2>{makeTitle(inventoryId)}</h2>
|
||||
{cheekyLilError && (
|
||||
<div class="error">
|
||||
<p class="oh-deary-me">An error occoured</p>
|
||||
<p class="what-the-fuck-happened">{cheekyLilError}</p>
|
||||
<p class="what-next">
|
||||
We're sorry about that.<br />
|
||||
Try going <a href="/">back home</a>,
|
||||
or <a href="https://github.com/Lissy93/awesome-privacy/issues/new/choose">raising a ticket</a> on
|
||||
GitHub.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<SavedServices allData={categories} serviceList={serviceList} client:load />
|
||||
<div class="buttons">
|
||||
<p>Not found what you're looking for?</p>
|
||||
<Button url="/all">Browse Services</Button>
|
||||
</div>
|
||||
</main>
|
||||
<main>
|
||||
<h2>{makeTitle(inventoryId)}</h2>
|
||||
{
|
||||
cheekyLilError && (
|
||||
<div class="error">
|
||||
<p class="oh-deary-me">An error occoured</p>
|
||||
<p class="what-the-fuck-happened">{cheekyLilError}</p>
|
||||
<p class="what-next">
|
||||
We're sorry about that.
|
||||
<br />
|
||||
Try going <a href="/">back home</a>, or{' '}
|
||||
<a href="https://github.com/Lissy93/awesome-privacy/issues/new/choose">
|
||||
raising a ticket
|
||||
</a>{' '}
|
||||
on GitHub.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<SavedServices allData={categories} serviceList={serviceList} client:load />
|
||||
<div class="buttons">
|
||||
<p>Not found what you're looking for?</p>
|
||||
<Button url="/all">Browse Services</Button>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
<style lang="scss">
|
||||
main {
|
||||
margin: 0 auto 2rem auto;
|
||||
padding: 1rem;
|
||||
width: 1200px;
|
||||
max-width: calc(100% - 5rem);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - 12rem);
|
||||
font-size: 1.25rem;
|
||||
h2 {
|
||||
font-family: "Lekton", sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 3rem;
|
||||
margin: 0;
|
||||
color: var(--accent-3);
|
||||
}
|
||||
.buttons {
|
||||
margin: 1rem auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.error {
|
||||
text-align: center;
|
||||
font-size: 1.4rem;
|
||||
.oh-deary-me {
|
||||
font-size: 1.8rem;
|
||||
margin: 0.2rem auto;
|
||||
}
|
||||
.what-the-fuck-happened {
|
||||
color: var(--danger);
|
||||
margin: 0.2rem auto;
|
||||
}
|
||||
.what-next {
|
||||
font-size: 1rem;
|
||||
margin-top: 3rem;
|
||||
opacity: 0.6;
|
||||
a {
|
||||
color: var(--foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
main {
|
||||
margin: 0 auto 2rem auto;
|
||||
padding: 1rem;
|
||||
width: 1200px;
|
||||
max-width: calc(100% - 5rem);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - 12rem);
|
||||
font-size: 1.25rem;
|
||||
h2 {
|
||||
font-family: 'Lekton', sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 3rem;
|
||||
margin: 0;
|
||||
color: var(--accent-3);
|
||||
}
|
||||
.buttons {
|
||||
margin: 1rem auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.error {
|
||||
text-align: center;
|
||||
font-size: 1.4rem;
|
||||
.oh-deary-me {
|
||||
font-size: 1.8rem;
|
||||
margin: 0.2rem auto;
|
||||
}
|
||||
.what-the-fuck-happened {
|
||||
color: var(--danger);
|
||||
margin: 0.2rem auto;
|
||||
}
|
||||
.what-next {
|
||||
font-size: 1rem;
|
||||
margin-top: 3rem;
|
||||
opacity: 0.6;
|
||||
a {
|
||||
color: var(--foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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[]);
|
||||
---
|
||||
|
||||
<Layout title="Saved Services">
|
||||
<main>
|
||||
<div class="top-row">
|
||||
<!-- <h2>Inventory</h2> -->
|
||||
<EditableTitle client:load />
|
||||
<GetSharableLink client:load />
|
||||
</div>
|
||||
<SavedServices allData={categories} client:load />
|
||||
<div class="buttons">
|
||||
<p>Not found what you're looking for?</p>
|
||||
<Button url="/all">Browse Services</Button>
|
||||
</div>
|
||||
</main>
|
||||
<main>
|
||||
<div class="top-row">
|
||||
<!-- <h2>Inventory</h2> -->
|
||||
<EditableTitle client:load />
|
||||
<GetSharableLink client:load />
|
||||
</div>
|
||||
<SavedServices allData={categories} client:load />
|
||||
<div class="buttons">
|
||||
<p>Not found what you're looking for?</p>
|
||||
<Button url="/all">Browse Services</Button>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
<style lang="scss">
|
||||
main {
|
||||
margin: 0 auto 2rem auto;
|
||||
padding: 1rem;
|
||||
width: 1200px;
|
||||
max-width: calc(100% - 5rem);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - 12rem);
|
||||
font-size: 1.25rem;
|
||||
.top-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
h2 {
|
||||
font-family: "Lekton", sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 3rem;
|
||||
margin: 0;
|
||||
color: var(--accent-3);
|
||||
}
|
||||
.buttons {
|
||||
margin: 1rem auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
main {
|
||||
margin: 0 auto 2rem auto;
|
||||
padding: 1rem;
|
||||
width: 1200px;
|
||||
max-width: calc(100% - 5rem);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - 12rem);
|
||||
font-size: 1.25rem;
|
||||
.top-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
h2 {
|
||||
font-family: 'Lekton', sans-serif;
|
||||
font-weight: bold;
|
||||
font-size: 3rem;
|
||||
margin: 0;
|
||||
color: var(--accent-3);
|
||||
}
|
||||
.buttons {
|
||||
margin: 1rem auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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<any>;
|
||||
let fuse: Fuse<SearchItem>;
|
||||
|
||||
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<string, GroupedSection>;
|
||||
}
|
||||
|
||||
const putResultsIntoGroups = () => {
|
||||
const grouped = services.reduce((acc, item) => {
|
||||
const { category: categoryName, sectionName, ...service } = item;
|
||||
const grouped: Record<string, GroupedCategory> = {};
|
||||
|
||||
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();
|
||||
|
||||
---
|
||||
|
||||
<Layout title="Search | Awesome Privacy">
|
||||
<section>
|
||||
<h1>Search</h1>
|
||||
<Search client:visible data={categories} previousSearch={searchTerm} />
|
||||
</section>
|
||||
<SmartSuggestions client:visible searchTerm={searchTerm || ''} categories={categories} />
|
||||
<section class="result-count">
|
||||
<h3>Deep Search</h3>
|
||||
<p>Showing {services.length} results for "{searchTerm}" sorted by relevence</p>
|
||||
</section>
|
||||
<div class="results">
|
||||
{
|
||||
beer.map((category: any) => (
|
||||
<div class="category">
|
||||
<a class="category-title" href={`/${slugify(category.categoryName)}`}>
|
||||
<h3>{category.categoryName}</h3>
|
||||
</a>
|
||||
<span class="section-icon"><FontAwesome iconName={slugify(category.categoryName)} /></span>
|
||||
<ul class="section-list">
|
||||
{category.sections.map((section: any) => (
|
||||
<li class="section-item">
|
||||
<h4>{section.sectionName}</h4>
|
||||
<ul class="service-list">
|
||||
{section.items.map((item: Service) => (
|
||||
<li>
|
||||
<a href={item.url}>{item.name}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<section>
|
||||
<h1>Search</h1>
|
||||
<Search client:visible data={categories} previousSearch={searchTerm} />
|
||||
</section>
|
||||
<SmartSuggestions
|
||||
client:visible
|
||||
searchTerm={searchTerm || ''}
|
||||
categories={categories}
|
||||
/>
|
||||
<section class="result-count">
|
||||
<h3>Deep Search</h3>
|
||||
<p>
|
||||
Showing {services.length} results for "{searchTerm}" sorted by relevence
|
||||
</p>
|
||||
</section>
|
||||
<div class="results">
|
||||
{
|
||||
beer.map((category) => (
|
||||
<div class="category">
|
||||
<a class="category-title" href={`/${slugify(category.categoryName)}`}>
|
||||
<h3>{category.categoryName}</h3>
|
||||
</a>
|
||||
<span class="section-icon">
|
||||
<FontAwesome iconName={slugify(category.categoryName)} />
|
||||
</span>
|
||||
<ul class="section-list">
|
||||
{category.sections.map((section) => (
|
||||
<li class="section-item">
|
||||
<h4>{section.sectionName}</h4>
|
||||
<ul class="service-list">
|
||||
{section.items.map((item) => (
|
||||
<li>
|
||||
<a href={item.url}>{item.name}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
margin: 2rem auto;
|
||||
padding: 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
max-width: 1100px;
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
margin: 2rem auto;
|
||||
padding: 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
max-width: 1100px;
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
color: var(--accent-3);
|
||||
font-family: 'Lekton', sans-serif;
|
||||
text-align: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
color: var(--accent-3);
|
||||
font-family: "Lekton", sans-serif;
|
||||
text-align: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
}
|
||||
.result-count {
|
||||
padding: 0;
|
||||
width: 80vw;
|
||||
max-width: 900px;
|
||||
margin: 1rem auto 0 auto;
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: var(--accent-3);
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.result-count {
|
||||
padding: 0;
|
||||
width: 80vw;
|
||||
max-width: 900px;
|
||||
margin: 1rem auto 0 auto;
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: var(--accent-3);
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.results {
|
||||
margin: 0 auto 2rem auto;
|
||||
padding: 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
max-width: 1100px;
|
||||
columns: 6 300px;
|
||||
column-gap: 1rem;
|
||||
.category {
|
||||
padding: 1rem;
|
||||
box-shadow: 4px 4px 0 var(--box-outline);
|
||||
background: var(--accent-fg);
|
||||
border: 2px solid var(--box-outline);
|
||||
border-radius: var(--curve-sm);
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
width: 85%;
|
||||
margin: 1rem;
|
||||
position: relative;
|
||||
.category-title {
|
||||
text-decoration: none;
|
||||
color: var(--foreground);
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
h3 {
|
||||
font-family: "Lekton", sans-serif;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
}
|
||||
.section-icon {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
width: fit-content;
|
||||
:global(svg) {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
opacity: 0.5;
|
||||
text-shadow: 3px 3px 0 black;
|
||||
color: var(--accent-3);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
.section-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
h4 {
|
||||
margin: 1rem 0 0 0;
|
||||
}
|
||||
.service-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.results {
|
||||
margin: 0 auto 2rem auto;
|
||||
padding: 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
max-width: 1100px;
|
||||
columns: 6 300px;
|
||||
column-gap: 1rem;
|
||||
.category {
|
||||
padding: 1rem;
|
||||
box-shadow: 4px 4px 0 var(--box-outline);
|
||||
background: var(--accent-fg);
|
||||
border: 2px solid var(--box-outline);
|
||||
border-radius: var(--curve-sm);
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
width: 85%;
|
||||
margin: 1rem;
|
||||
position: relative;
|
||||
.category-title {
|
||||
text-decoration: none;
|
||||
color: var(--foreground);
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
h3 {
|
||||
font-family: 'Lekton', sans-serif;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
}
|
||||
.section-icon {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
width: fit-content;
|
||||
:global(svg) {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
opacity: 0.5;
|
||||
text-shadow: 3px 3px 0 black;
|
||||
color: var(--accent-3);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
.section-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
h4 {
|
||||
margin: 1rem 0 0 0;
|
||||
}
|
||||
.service-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
---
|
||||
|
||||
<Layout title="Search | Awesome Privacy">
|
||||
<section>
|
||||
<h1>Search</h1>
|
||||
<Search client:visible data={categories} />
|
||||
<p class="sitemap-link">
|
||||
Browse <a href="/all">all listings</a>, or see all pages in the <a href="/sitemap">Sitemap</a>
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h1>Search</h1>
|
||||
<Search client:visible data={categories} />
|
||||
<p class="sitemap-link">
|
||||
Browse <a href="/all">all listings</a>, or see all pages in the <a
|
||||
href="/sitemap">Sitemap</a
|
||||
>
|
||||
</p>
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
box-shadow: 4px 4px 0 var(--box-outline);
|
||||
background: var(--accent-fg);
|
||||
border: 2px solid var(--box-outline);
|
||||
border-radius: var(--curve-sm);
|
||||
margin: 2rem auto;
|
||||
padding: 1rem;
|
||||
width: calc(100% - 5rem);
|
||||
max-width: 1100px;
|
||||
min-height: 80vh;
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
box-shadow: 4px 4px 0 var(--box-outline);
|
||||
background: var(--accent-fg);
|
||||
border: 2px solid var(--box-outline);
|
||||
border-radius: var(--curve-sm);
|
||||
margin: 2rem auto;
|
||||
padding: 1rem;
|
||||
width: calc(100% - 5rem);
|
||||
max-width: 1100px;
|
||||
min-height: 80vh;
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
color: var(--accent-3);
|
||||
font-family: "Lekton", sans-serif;
|
||||
text-align: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.sitemap-link {
|
||||
opacity: 0.6;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
margin: 2rem auto;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
color: var(--accent-3);
|
||||
font-family: 'Lekton', sans-serif;
|
||||
text-align: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.sitemap-link {
|
||||
opacity: 0.6;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
margin: 2rem auto;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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 || [];
|
||||
---
|
||||
|
||||
<Layout title="All Links | Awesome Privacy">
|
||||
<main>
|
||||
<h2>Sitemap</h2>
|
||||
<p>
|
||||
Below is a full listing of all pages on this site.<br>
|
||||
<small>As reflected in our <a href="/sitemap-index.xml">sitemap.xml</a></small>
|
||||
</p>
|
||||
<span class="search-controlls">
|
||||
<input id="searchInput" type="search" placeholder="Start typing to filter..." />
|
||||
<p id="press-enter-msg" class="press-enter">Press enter for deep search</p>
|
||||
</span>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/search">Search</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/all">View All</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/inventory">My Inventory</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/submit">Submit Listing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/api">API</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/browse">Categories</a>
|
||||
<ul>
|
||||
{categories.map((category) => (
|
||||
<li>
|
||||
<a href={`/${slugify(category.name)}`}>{category.name}</a>
|
||||
<ul>
|
||||
{category.sections.map((section) => (
|
||||
<li>
|
||||
<a href={`/${slugify(category.name)}/${slugify(section.name)}`}>{section.name}</a>
|
||||
<ul>
|
||||
{(section.services || []).map((service) => (
|
||||
<li title={service.description || ''}>
|
||||
<a href={`/${slugify(category.name)}/${slugify(section.name)}/${slugify(service.name)}`}>
|
||||
{service.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/about">About</a>
|
||||
<ul>
|
||||
<li><a href="/about#objective">Objective</a></li>
|
||||
<li><a href="/about#criteria">Listing Criteria</a></li>
|
||||
<li><a href="/about#contributing">Contributing</a></li>
|
||||
<li>
|
||||
<a href="/about#acknowledgements">Acknowledgements</a>
|
||||
<ul>
|
||||
<li><a href="/about#contributors">Contributors</a></li>
|
||||
<li><a href="/about#sponsors">Sponsors</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="/about#author">Author</a></li>
|
||||
<li><a href="/about#license">License</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/sitemap">Sitemap</a>
|
||||
</li>
|
||||
</ul>
|
||||
</main>
|
||||
|
||||
<main>
|
||||
<h2>Sitemap</h2>
|
||||
<p>
|
||||
Below is a full listing of all pages on this site.<br />
|
||||
<small
|
||||
>As reflected in our <a href="/sitemap-index.xml">sitemap.xml</a></small
|
||||
>
|
||||
</p>
|
||||
<span class="search-controlls">
|
||||
<input
|
||||
id="searchInput"
|
||||
type="search"
|
||||
placeholder="Start typing to filter..."
|
||||
/>
|
||||
<p id="press-enter-msg" class="press-enter">
|
||||
Press enter for deep search
|
||||
</p>
|
||||
</span>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/search">Search</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/all">View All</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/inventory">My Inventory</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/submit">Submit Listing</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/api">API</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/browse">Categories</a>
|
||||
<ul>
|
||||
{
|
||||
categories.map((category) => (
|
||||
<li>
|
||||
<a href={`/${slugify(category.name)}`}>{category.name}</a>
|
||||
<ul>
|
||||
{category.sections.map((section) => (
|
||||
<li>
|
||||
<a
|
||||
href={`/${slugify(category.name)}/${slugify(section.name)}`}
|
||||
>
|
||||
{section.name}
|
||||
</a>
|
||||
<ul>
|
||||
{(section.services || []).map((service) => (
|
||||
<li title={service.description || ''}>
|
||||
<a
|
||||
href={`/${slugify(category.name)}/${slugify(section.name)}/${slugify(service.name)}`}
|
||||
>
|
||||
{service.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/about">About</a>
|
||||
<ul>
|
||||
<li><a href="/about#objective">Objective</a></li>
|
||||
<li><a href="/about#criteria">Listing Criteria</a></li>
|
||||
<li><a href="/about#contributing">Contributing</a></li>
|
||||
<li>
|
||||
<a href="/about#acknowledgements">Acknowledgements</a>
|
||||
<ul>
|
||||
<li><a href="/about#contributors">Contributors</a></li>
|
||||
<li><a href="/about#sponsors">Sponsors</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="/about#author">Author</a></li>
|
||||
<li><a href="/about#license">License</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/sitemap">Sitemap</a>
|
||||
</li>
|
||||
</ul>
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Select elements for events (search field, each result, and message str)
|
||||
const filterInput = document.querySelector<HTMLInputElement>('input');
|
||||
const pages = document.querySelectorAll<HTMLElement>('li');
|
||||
const pressEnterMsg = document.getElementById('press-enter-msg');
|
||||
if (!pressEnterMsg) throw new Error('pressEnterMsg element not found');
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Select elements for events (search field, each result, and message str)
|
||||
const filterInput = document.querySelector<HTMLInputElement>('input');
|
||||
const pages = document.querySelectorAll<HTMLElement>('li');
|
||||
const pressEnterMsg = document.getElementById('press-enter-msg');
|
||||
if (!pressEnterMsg) throw new Error('pressEnterMsg element not found');
|
||||
|
||||
// Instant search/filter functionality
|
||||
filterInput?.addEventListener('input', (e: Event) => {
|
||||
const filter = (e.target as HTMLInputElement).value.toLowerCase();
|
||||
pressEnterMsg.style.visibility = filter.length > 0 ? 'visible' : 'hidden';
|
||||
Array.from(pages).reduce((count, page) => {
|
||||
const match = page.textContent?.toLowerCase().includes(filter) || false;
|
||||
page.style.display = match ? 'block' : 'none';
|
||||
return count + (match ? 1 : 0);
|
||||
}, 0);
|
||||
});
|
||||
// Handle keypress events, so Esc clears search, and Entr runs deep search
|
||||
document.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (!filterInput) return;
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
const searchTerm = encodeURIComponent(filterInput.value);
|
||||
window.location.href = `/search/${searchTerm}`;
|
||||
break;
|
||||
case 'Escape':
|
||||
filterInput.value = '';
|
||||
filterInput.blur();
|
||||
pressEnterMsg.style.visibility = 'hidden';
|
||||
pages.forEach(page => page.style.display = 'block');
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
// Instant search/filter functionality
|
||||
filterInput?.addEventListener('input', (e: Event) => {
|
||||
const filter = (e.target as HTMLInputElement).value.toLowerCase();
|
||||
pressEnterMsg.style.visibility = filter.length > 0 ? 'visible' : 'hidden';
|
||||
Array.from(pages).reduce((count, page) => {
|
||||
const match = page.textContent?.toLowerCase().includes(filter) || false;
|
||||
page.style.display = match ? 'block' : 'none';
|
||||
return count + (match ? 1 : 0);
|
||||
}, 0);
|
||||
});
|
||||
// Handle keypress events, so Esc clears search, and Entr runs deep search
|
||||
document.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (!filterInput) return;
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
const searchTerm = encodeURIComponent(filterInput.value);
|
||||
window.location.href = `/search/${searchTerm}`;
|
||||
break;
|
||||
case 'Escape':
|
||||
filterInput.value = '';
|
||||
filterInput.blur();
|
||||
pressEnterMsg.style.visibility = 'hidden';
|
||||
pages.forEach((page) => (page.style.display = 'block'));
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
main {
|
||||
padding: 1rem;
|
||||
width: 1000px;
|
||||
max-width: calc(100% - 5rem);
|
||||
margin: 4rem auto 5rem auto;
|
||||
padding: 0 2rem;
|
||||
border: 2px solid var(--box-outline);
|
||||
box-shadow: 6px 6px 0 var(--box-outline);
|
||||
background: var(--accent-fg);
|
||||
position: relative;
|
||||
p {
|
||||
font-size: 1.3rem;
|
||||
margin: 0;
|
||||
small {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.5;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
ul {
|
||||
padding-left: 1.2rem;
|
||||
list-style: none;
|
||||
border-left: 3px solid #5f53f482;
|
||||
border-radius: 4px;
|
||||
li {
|
||||
font-weight: 600;
|
||||
ul li {
|
||||
font-weight: 500;
|
||||
ul li {
|
||||
font-weight: 400;
|
||||
ul li {
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
}
|
||||
&::before {
|
||||
content: '━ ';
|
||||
color: #5f53f482;
|
||||
margin-left: -1.2rem;
|
||||
}
|
||||
a {
|
||||
transition: all ease-in-out 0.2s;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1rem;
|
||||
width: 1000px;
|
||||
max-width: calc(100% - 5rem);
|
||||
margin: 4rem auto 5rem auto;
|
||||
padding: 0 2rem;
|
||||
border: 2px solid var(--box-outline);
|
||||
box-shadow: 6px 6px 0 var(--box-outline);
|
||||
background: var(--accent-fg);
|
||||
position: relative;
|
||||
p {
|
||||
font-size: 1.3rem;
|
||||
margin: 0;
|
||||
small {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.5;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover { opacity: 0.8; }
|
||||
}
|
||||
}
|
||||
ul {
|
||||
padding-left: 1.2rem;
|
||||
list-style: none;
|
||||
border-left: 3px solid #5f53f482;
|
||||
border-radius: 4px;;
|
||||
li {
|
||||
font-weight: 600;
|
||||
ul li {
|
||||
font-weight: 500;
|
||||
ul li {
|
||||
font-weight: 400;
|
||||
ul li {
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
}
|
||||
&::before {
|
||||
content: "━ ";
|
||||
color: #5f53f482;
|
||||
margin-left: -1.2rem;
|
||||
}
|
||||
a {
|
||||
transition: all ease-in-out 0.2s;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
gap: 1rem;
|
||||
font-size: 2rem;
|
||||
margin: -2rem 0 2rem -4rem;
|
||||
box-shadow: 6px 6px 0 var(--box-outline);
|
||||
background: var(--accent);
|
||||
color: var(--accent-fg);
|
||||
width: fit-content;
|
||||
padding: 0.25rem 0.5rem;
|
||||
@media (max-width: 768px) {
|
||||
margin: -2rem auto 1rem auto;
|
||||
}
|
||||
}
|
||||
|
||||
.search-controlls {
|
||||
float: right;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
position: absolute;
|
||||
@media (max-width: 768px) {
|
||||
position: relative;
|
||||
float: none;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
.press-enter {
|
||||
margin: 0.5rem 0 0 0;
|
||||
opacity: 0.5;
|
||||
visibility: hidden;
|
||||
font-size: 1rem;
|
||||
}
|
||||
input {
|
||||
padding: 0.25rem;
|
||||
border: 2px solid var(--box-outline);
|
||||
border-radius: var(--curve-sm);
|
||||
font-size: 1rem;
|
||||
box-shadow: 3px 3px 0 var(--box-outline);
|
||||
color: var(--accent-3);
|
||||
background: var(--background-form);
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 4px 4px 0 var(--box-outline);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
gap: 1rem;
|
||||
font-size: 2rem;
|
||||
margin: -2rem 0 2rem -4rem;
|
||||
box-shadow: 6px 6px 0 var(--box-outline);
|
||||
background: var(--accent);
|
||||
color: var(--accent-fg);
|
||||
width: fit-content;
|
||||
padding: 0.25rem 0.5rem;
|
||||
@media (max-width: 768px) {
|
||||
margin: -2rem auto 1rem auto;
|
||||
}
|
||||
}
|
||||
|
||||
.search-controlls {
|
||||
float: right;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
position: absolute;
|
||||
@media (max-width: 768px) {
|
||||
position: relative;
|
||||
float: none;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
.press-enter {
|
||||
margin: 0.5rem 0 0 0;
|
||||
opacity: 0.5;
|
||||
visibility: hidden;
|
||||
font-size: 1rem;
|
||||
}
|
||||
input {
|
||||
padding: 0.25rem;
|
||||
border: 2px solid var(--box-outline);
|
||||
border-radius: var(--curve-sm);
|
||||
font-size: 1rem;
|
||||
box-shadow: 3px 3px 0 var(--box-outline);
|
||||
color: var(--accent-3);
|
||||
background: var(--background-form);
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 4px 4px 0 var(--box-outline);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
---
|
||||
|
||||
<Layout title="Awesome Privacy">
|
||||
<section>
|
||||
<h2>About our Data</h2>
|
||||
<p class="about-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 <a href="/about#our-data">about page</a>.
|
||||
<br /><br />
|
||||
You can make ammendments/additions/removals simply by editing the
|
||||
<a href="github.com/lissy93/awesome-privacy/blob/main/awesome-privacy.yml">awesome-privacy.yml</a> file.
|
||||
<br />
|
||||
Before you proceed, please first read our <a href="https://github.com/Lissy93/awesome-privacy/blob/main/.github/CONTRIBUTING.md">Contributing Docs</a>
|
||||
<br /><br />
|
||||
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 💜
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Submit an Addition</h2>
|
||||
<AddNewService client:load />
|
||||
</section>
|
||||
<section>
|
||||
<h2>About our Data</h2>
|
||||
<p class="about-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 <a href="/about#our-data"
|
||||
>about page</a
|
||||
>.
|
||||
<br /><br />
|
||||
You can make ammendments/additions/removals simply by editing the
|
||||
<a href="github.com/lissy93/awesome-privacy/blob/main/awesome-privacy.yml"
|
||||
>awesome-privacy.yml</a
|
||||
> file.
|
||||
<br />
|
||||
Before you proceed, please first read our <a
|
||||
href="https://github.com/Lissy93/awesome-privacy/blob/main/.github/CONTRIBUTING.md"
|
||||
>Contributing Docs</a
|
||||
>
|
||||
<br /><br />
|
||||
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 💜
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Submit an Addition</h2>
|
||||
<AddNewService client:load />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Submit a Removal Request</h2>
|
||||
<p>
|
||||
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 <a href="github.com/lissy93/awesome-privacy/blob/main/awesome-privacy.yml">awesome-privacy.yml</a> file.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Submit a Removal Request</h2>
|
||||
<p>
|
||||
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 <a
|
||||
href="github.com/lissy93/awesome-privacy/blob/main/awesome-privacy.yml"
|
||||
>awesome-privacy.yml</a
|
||||
> file.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Edit a Listing</h2>
|
||||
<p>
|
||||
Edits are welcome! All data is located in
|
||||
<a href="github.com/lissy93/awesome-privacy/blob/main/awesome-privacy.yml">awesome-privacy.yml</a>.
|
||||
<br>
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Edit a Listing</h2>
|
||||
<p>
|
||||
Edits are welcome! All data is located in
|
||||
<a href="github.com/lissy93/awesome-privacy/blob/main/awesome-privacy.yml"
|
||||
>awesome-privacy.yml</a
|
||||
>.
|
||||
<br />
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Checklist</h2>
|
||||
<ul>
|
||||
<li>You must read the <a href="https://github.com/Lissy93/awesome-privacy/blob/main/.github/CONTRIBUTING.md">Contributing</a> guidelines before proceeding</li>
|
||||
<li>All listing must meed our <a href="/about#creteria">Criteria</a> to be considered privacy-respecting</li>
|
||||
<li>Double check that your changes haven't already been proposed</li>
|
||||
<li>If you're associated with a service included, you must declare your affiliation</li>
|
||||
<li>Before commiting changes, ensure the YAML syntax is valid and it complies with our schema</li>
|
||||
<li>Please complete the issue or PR description template in full, do not remove any fields</li>
|
||||
<li>All submissions must be made via <a href="https://github.com/Lissy93/awesome-privacy">our GitHub</a>, do not email/PM maintainers</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Checklist</h2>
|
||||
<ul>
|
||||
<li>
|
||||
You must read the <a
|
||||
href="https://github.com/Lissy93/awesome-privacy/blob/main/.github/CONTRIBUTING.md"
|
||||
>Contributing</a
|
||||
> guidelines before proceeding
|
||||
</li>
|
||||
<li>
|
||||
All listing must meed our <a href="/about#creteria">Criteria</a> to be considered
|
||||
privacy-respecting
|
||||
</li>
|
||||
<li>Double check that your changes haven't already been proposed</li>
|
||||
<li>
|
||||
If you're associated with a service included, you must declare your
|
||||
affiliation
|
||||
</li>
|
||||
<li>
|
||||
Before commiting changes, ensure the YAML syntax is valid and it
|
||||
complies with our schema
|
||||
</li>
|
||||
<li>
|
||||
Please complete the issue or PR description template in full, do not
|
||||
remove any fields
|
||||
</li>
|
||||
<li>
|
||||
All submissions must be made via <a
|
||||
href="https://github.com/Lissy93/awesome-privacy">our GitHub</a
|
||||
>, do not email/PM maintainers
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{commits && commits.length > 0 && (
|
||||
<section>
|
||||
<h2>Recent Changes</h2>
|
||||
<p class="about-data">
|
||||
You can view a full ledger of all updates made
|
||||
at <a href="https://github.com/lissy93/awesome-privacy">github.com/lissy93/awesome-privacy</a>
|
||||
</p>
|
||||
<ul class="commit-log">
|
||||
{commits.map((commit) => (
|
||||
<li title={commit.sha}>
|
||||
{commit.message}<br />
|
||||
<img width="14" src={commit.authorAvatar} />
|
||||
<small>
|
||||
By <a href={`https://github.com/${commit.authorUsername}`}>{commit.authorName || commit.authorUsername}</a>
|
||||
on <a href={`https://github.com/Lissy93/awesome-privacy/commit/${commit.sha}`}>{formatDate(commit.authorDate)}</a>
|
||||
</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{
|
||||
commits && commits.length > 0 && (
|
||||
<section>
|
||||
<h2>Recent Changes</h2>
|
||||
<p class="about-data">
|
||||
You can view a full ledger of all updates made at{' '}
|
||||
<a href="https://github.com/lissy93/awesome-privacy">
|
||||
github.com/lissy93/awesome-privacy
|
||||
</a>
|
||||
</p>
|
||||
<ul class="commit-log">
|
||||
{commits.map((commit) => (
|
||||
<li title={commit.sha}>
|
||||
{commit.message}
|
||||
<br />
|
||||
<img width="14" src={commit.authorAvatar} />
|
||||
<small>
|
||||
By{' '}
|
||||
<a href={`https://github.com/${commit.authorUsername}`}>
|
||||
{commit.authorName || commit.authorUsername}
|
||||
</a>
|
||||
on{' '}
|
||||
<a
|
||||
href={`https://github.com/Lissy93/awesome-privacy/commit/${commit.sha}`}
|
||||
>
|
||||
{formatDate(commit.authorDate)}
|
||||
</a>
|
||||
</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
</Layout>
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
margin: 2rem auto;
|
||||
padding: 1rem;
|
||||
width: 1000px;
|
||||
max-width: calc(100% - 5rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 0 2rem;
|
||||
border: 2px solid var(--box-outline);
|
||||
box-shadow: 6px 6px 0 var(--box-outline);
|
||||
background: var(--accent-fg);
|
||||
border-radius: var(--curve-sm);
|
||||
@media (max-width: 768px) {
|
||||
max-width: 95%;
|
||||
padding: 0.5rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
ul {
|
||||
margin-top: 0;
|
||||
padding-left: 1rem;
|
||||
list-style: circle;
|
||||
}
|
||||
.about-data {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: 2rem auto;
|
||||
padding: 1rem;
|
||||
width: 1000px;
|
||||
max-width: calc(100% - 5rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 0 2rem;
|
||||
border: 2px solid var(--box-outline);
|
||||
box-shadow: 6px 6px 0 var(--box-outline);
|
||||
background: var(--accent-fg);
|
||||
border-radius: var(--curve-sm);
|
||||
@media(max-width: 768px) {
|
||||
max-width: 95%;
|
||||
padding: 0.5rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
ul {
|
||||
margin-top: 0;
|
||||
padding-left: 1rem;
|
||||
list-style: circle;
|
||||
}
|
||||
.about-data {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.commit-log {
|
||||
column-width: 350px;
|
||||
li {
|
||||
small {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
a { text-transform: capitalize;}
|
||||
}
|
||||
img {
|
||||
border-radius: var(--curve-md);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.commit-log {
|
||||
column-width: 350px;
|
||||
li {
|
||||
small {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
a {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
img {
|
||||
border-radius: var(--curve-md);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
|
|
|
|||
|
|
@ -1,60 +1,82 @@
|
|||
import { slugify } from '@utils/fetch-data';
|
||||
|
||||
export const makeRemovalRequest = (categoryName: string, sectionName: string, serviceName: string, yaml?: string) => {
|
||||
export const makeRemovalRequest = (
|
||||
categoryName: string,
|
||||
sectionName: string,
|
||||
serviceName: string,
|
||||
yaml?: string,
|
||||
) => {
|
||||
const title = `[REMOVAL] ${serviceName}`;
|
||||
const under = `**${serviceName}** (source: [${categoryName} ➜ ${sectionName} ➜ ${serviceName}`
|
||||
+ `](https://github.com/Lissy93/awesome-privacy/tree/main#${slugify(sectionName)}))`;
|
||||
const removalData = `&title=${encodeURIComponent(title)}&removal-data=`
|
||||
+ `${encodeURIComponent(yaml || '')}&service-title=${encodeURIComponent(under)}`;
|
||||
const issueCreate = 'https://github.com/Lissy93/awesome-privacy/issues/new'
|
||||
const baseOptions = '?assignees=lissy93&labels=Suggested+Removal%2CAwaiting+'
|
||||
+ 'Review&projects=&template=removal.yml'
|
||||
const under =
|
||||
`**${serviceName}** (source: [${categoryName} ➜ ${sectionName} ➜ ${serviceName}` +
|
||||
`](https://github.com/Lissy93/awesome-privacy/tree/main#${slugify(sectionName)}))`;
|
||||
const removalData =
|
||||
`&title=${encodeURIComponent(title)}&removal-data=` +
|
||||
`${encodeURIComponent(yaml || '')}&service-title=${encodeURIComponent(under)}`;
|
||||
const issueCreate = 'https://github.com/Lissy93/awesome-privacy/issues/new';
|
||||
const baseOptions =
|
||||
'?assignees=lissy93&labels=Suggested+Removal%2CAwaiting+' +
|
||||
'Review&projects=&template=removal.yml';
|
||||
return `${issueCreate}${baseOptions}${removalData}`;
|
||||
};
|
||||
|
||||
export const makeEditRequest = (categoryName: string, sectionName: string, serviceName: string, yaml?: string) => {
|
||||
export const makeEditRequest = (
|
||||
categoryName: string,
|
||||
sectionName: string,
|
||||
serviceName: string,
|
||||
yaml?: string,
|
||||
) => {
|
||||
const title = `[AMENDMENT] ${serviceName}`;
|
||||
const under = `**${serviceName}** (source: [${categoryName} ➜ ${sectionName} ➜ ${serviceName}`
|
||||
+ `](https://github.com/Lissy93/awesome-privacy/tree/main#${slugify(sectionName)}))`;
|
||||
const removalData = `&title=${encodeURIComponent(title)}&amendment-data=`
|
||||
+ `${encodeURIComponent(yaml || '')}&service-title=${encodeURIComponent(under)}`;
|
||||
const issueCreate = 'https://github.com/Lissy93/awesome-privacy/issues/new'
|
||||
const baseOptions = '?assignees=lissy93&labels=Suggested+Removal%2CAwaiting+'
|
||||
+ 'Review&projects=&template=amendment.yml'
|
||||
const under =
|
||||
`**${serviceName}** (source: [${categoryName} ➜ ${sectionName} ➜ ${serviceName}` +
|
||||
`](https://github.com/Lissy93/awesome-privacy/tree/main#${slugify(sectionName)}))`;
|
||||
const removalData =
|
||||
`&title=${encodeURIComponent(title)}&amendment-data=` +
|
||||
`${encodeURIComponent(yaml || '')}&service-title=${encodeURIComponent(under)}`;
|
||||
const issueCreate = 'https://github.com/Lissy93/awesome-privacy/issues/new';
|
||||
const baseOptions =
|
||||
'?assignees=lissy93&labels=Suggested+Removal%2CAwaiting+' +
|
||||
'Review&projects=&template=amendment.yml';
|
||||
return `${issueCreate}${baseOptions}${removalData}`;
|
||||
};
|
||||
|
||||
export const makeAdditionRequest = (formData: {
|
||||
listingCategory: string;
|
||||
serviceName: string;
|
||||
serviceUrl: string;
|
||||
serviceIcon: string;
|
||||
serviceDescription: string;
|
||||
serviceGithub: string;
|
||||
serviceTosdrId: string;
|
||||
serviceIosApp: string,
|
||||
serviceAndroidApp: string,
|
||||
serviceDiscordInvite: string,
|
||||
serviceSubreddit: string,
|
||||
serviceOpenSource: boolean;
|
||||
serviceSecurityAudited: boolean;
|
||||
serviceCrypto: boolean;
|
||||
additionalInfo: string;
|
||||
}, yamlText?: string) => {
|
||||
|
||||
const userInfo = formData.additionalInfo.split('\n').map(line => `> ${line}`).join('\n');
|
||||
const additionalInfoText: string = `\n${userInfo}`
|
||||
+ `\n\n**YAML Content for Addition**\n\n\`\`\`yaml\n${yamlText || '# nothing yet'}\n\`\`\`\n`
|
||||
+ `\n\n<sup>This ticket was submitted via `
|
||||
+ `<a href="https://awesome-privacy.xyz/submit">awesome-privacy.xyz/submit</a></sup>`;
|
||||
export const makeAdditionRequest = (
|
||||
formData: {
|
||||
listingCategory: string;
|
||||
serviceName: string;
|
||||
serviceUrl: string;
|
||||
serviceIcon: string;
|
||||
serviceDescription: string;
|
||||
serviceGithub: string;
|
||||
serviceTosdrId: string;
|
||||
serviceIosApp: string;
|
||||
serviceAndroidApp: string;
|
||||
serviceDiscordInvite: string;
|
||||
serviceSubreddit: string;
|
||||
serviceOpenSource: boolean;
|
||||
serviceSecurityAudited: boolean;
|
||||
serviceCrypto: boolean;
|
||||
additionalInfo: string;
|
||||
},
|
||||
yamlText?: string,
|
||||
) => {
|
||||
const userInfo = formData.additionalInfo
|
||||
.split('\n')
|
||||
.map((line) => `> ${line}`)
|
||||
.join('\n');
|
||||
const additionalInfoText: string =
|
||||
`\n${userInfo}` +
|
||||
`\n\n**YAML Content for Addition**\n\n\`\`\`yaml\n${yamlText || '# nothing yet'}\n\`\`\`\n` +
|
||||
`\n\n<sup>This ticket was submitted via ` +
|
||||
`<a href="https://awesome-privacy.xyz/submit">awesome-privacy.xyz/submit</a></sup>`;
|
||||
|
||||
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: '' };
|
||||
}
|
||||
};
|
||||
|
|
|
|||
32
web/src/utils/dates-n-stuff.test.ts
Normal file
32
web/src/utils/dates-n-stuff.test.ts
Normal file
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
106
web/src/utils/do-searchy-searchy.test.ts
Normal file
106
web/src/utils/do-searchy-searchy.test.ts
Normal file
|
|
@ -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> = {}): 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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 },
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
|
||||
const doubleCheckPackageName = (packageStr: string) => {
|
||||
return packageStr.includes('id=') ? packageStr.split('id=')[1] : packageStr;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchAndroidInfo = async (androidPackage: string): Promise<AndroidInfo | null> => {
|
||||
export const fetchAndroidInfo = async (
|
||||
androidPackage: string,
|
||||
): Promise<AndroidInfo | null> => {
|
||||
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[];
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
37
web/src/utils/fetch-data.test.ts
Normal file
37
web/src/utils/fetch-data.test.ts
Normal file
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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<AwesomePrivacy> => {
|
||||
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('?', '');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
export const fetchDiscordInfo = async (discordInvite: string): Promise<DiscordInfo | null> => {
|
||||
export const fetchDiscordInfo = async (
|
||||
discordInvite: string,
|
||||
): Promise<DiscordInfo | null> => {
|
||||
const endpoint = `https://discord-invite-info.as93.net/${discordInvite}`;
|
||||
try {
|
||||
return await fetch(endpoint).then((res) => res.json());
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
|
||||
export const fetchDockerData = async (serviceName: string): Promise<TemplateResponse | null> => {
|
||||
export const fetchDockerData = async (
|
||||
serviceName: string,
|
||||
): Promise<TemplateResponse | null> => {
|
||||
const endpoint = `https://docker-info.as93.workers.dev/${serviceName}`;
|
||||
try {
|
||||
return await fetch(endpoint).then((res) => res.json());
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
export const fetchIosInfo = async (iosUrl: string): Promise<IoSApiResponse | null> => {
|
||||
export const fetchIosInfo = async (
|
||||
iosUrl: string,
|
||||
): Promise<IoSApiResponse | null> => {
|
||||
const endpoint = `https://ios-app-info.as93.net?appStoreUrl=${iosUrl}`;
|
||||
try {
|
||||
return await fetch(endpoint).then((res) => res.json());
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
export const fetchTosdrPrivacy = async (serviceId: string): Promise<PrivacyPolicyResponse | null> => {
|
||||
export const fetchTosdrPrivacy = async (
|
||||
serviceId: string,
|
||||
): Promise<PrivacyPolicyResponse | null> => {
|
||||
const endpoint = `https://privacy-policies.as93.workers.dev/${serviceId}`;
|
||||
try {
|
||||
return await fetch(endpoint).then((res) => res.json());
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
export const fetchRedditInfo = async (subreddit: string): Promise<RedditData | null> => {
|
||||
export const fetchRedditInfo = async (
|
||||
subreddit: string,
|
||||
): Promise<RedditData | null> => {
|
||||
const endpoint = `https://subreddit-info.as93.net/${subreddit}`;
|
||||
try {
|
||||
return await fetch(endpoint).then((res) => res.json());
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
|
||||
|
||||
export const fetchGitHubStats = async (github: string): Promise<GitHubStatsResponse | null> => {
|
||||
export const fetchGitHubStats = async (
|
||||
github: string,
|
||||
): Promise<GitHubStatsResponse | null> => {
|
||||
const endpoint = `https://repo-info.as93.workers.dev/${github}`;
|
||||
try {
|
||||
return await fetch(endpoint).then((res) => res.json());
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
export const fetchWebsiteInfo = async (url: string): Promise<WebsiteData | null> => {
|
||||
export const fetchWebsiteInfo = async (
|
||||
url: string,
|
||||
): Promise<WebsiteData | null> => {
|
||||
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 {
|
||||
|
|
|
|||
43
web/src/utils/parse-markdown.test.ts
Normal file
43
web/src/utils/parse-markdown.test.ts
Normal file
|
|
@ -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('');
|
||||
});
|
||||
});
|
||||
|
|
@ -23,19 +23,24 @@ export const parseMarkdown = (text: string | undefined): string => {
|
|||
|
||||
// Sanitize the input to remove <script> tags
|
||||
const sanitizeHtml = (html: string): string => {
|
||||
return html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
|
||||
return html.replace(
|
||||
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
|
||||
'',
|
||||
);
|
||||
};
|
||||
|
||||
// Configure marked with the custom renderer
|
||||
marked.use({ renderer });
|
||||
|
||||
// Parse the markdown, then sanitize the HTML to remove <script> tags
|
||||
const rawHtml = marked.parse(text, { async: false}) as string;
|
||||
const rawHtml = marked.parse(text, { async: false }) as string;
|
||||
const sanitizedHtml = sanitizeHtml(rawHtml);
|
||||
|
||||
return sanitizedHtml;
|
||||
};
|
||||
|
||||
export const formatLink = (link: string) => {
|
||||
return (link || '').replace(/^(https?:\/\/)?(www\.)?/, '').replace(/\/+$/, '');
|
||||
return (link || '')
|
||||
.replace(/^(https?:\/\/)?(www\.)?/, '')
|
||||
.replace(/\/+$/, '');
|
||||
};
|
||||
|
|
|
|||
68
web/src/utils/security-check-mappings.test.ts
Normal file
68
web/src/utils/security-check-mappings.test.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { analyzeSecurityChecks } from './security-check-mappings';
|
||||
|
||||
describe('analyzeSecurityChecks', () => {
|
||||
it('classifies a passing HTTPS check as passed', () => {
|
||||
const { passedChecks, failedChecks } = analyzeSecurityChecks({
|
||||
is_valid_https: true,
|
||||
});
|
||||
expect(passedChecks).toContain('Valid HTTPS Connection');
|
||||
expect(failedChecks).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('classifies is_valid_https: "no" as failed', () => {
|
||||
// The pass logic uses (value === shouldPass) || (shouldPass === true && value !== "no")
|
||||
// For boolean false, the second condition still passes (false !== "no" is true).
|
||||
// Only the string "no" actually triggers a failure when shouldPass is true.
|
||||
const { failedChecks } = analyzeSecurityChecks({
|
||||
is_valid_https: 'no',
|
||||
});
|
||||
expect(failedChecks).toContain('Valid HTTPS Connection');
|
||||
});
|
||||
|
||||
it('classifies a false-means-pass check correctly', () => {
|
||||
// is_host_an_ipv4 should be false to pass
|
||||
const { passedChecks } = analyzeSecurityChecks({
|
||||
is_host_an_ipv4: false,
|
||||
});
|
||||
expect(passedChecks).toContain('Host is an IPv4 Address');
|
||||
});
|
||||
|
||||
it('classifies a false-means-pass check as failed when true', () => {
|
||||
const { failedChecks } = analyzeSecurityChecks({
|
||||
is_host_an_ipv4: true,
|
||||
});
|
||||
expect(failedChecks).toContain('Host is an IPv4 Address');
|
||||
});
|
||||
|
||||
it('handles multiple checks at once', () => {
|
||||
const { passedChecks, failedChecks } = analyzeSecurityChecks({
|
||||
is_valid_https: true,
|
||||
is_host_an_ipv4: false,
|
||||
is_suspended_page: true, // should fail (expected false)
|
||||
});
|
||||
expect(passedChecks).toHaveLength(2);
|
||||
expect(failedChecks).toHaveLength(1);
|
||||
expect(failedChecks).toContain('Suspended Page');
|
||||
});
|
||||
|
||||
it('handles string "no" values for domain_recent checks', () => {
|
||||
const { passedChecks } = analyzeSecurityChecks({
|
||||
is_domain_recent: 'no',
|
||||
});
|
||||
expect(passedChecks).toContain('Domain Recently Created');
|
||||
});
|
||||
|
||||
it('fails domain_recent when value is not "no"', () => {
|
||||
const { failedChecks } = analyzeSecurityChecks({
|
||||
is_domain_recent: 'yes',
|
||||
});
|
||||
expect(failedChecks).toContain('Domain Recently Created');
|
||||
});
|
||||
|
||||
it('returns empty arrays for empty input', () => {
|
||||
const { passedChecks, failedChecks } = analyzeSecurityChecks({});
|
||||
expect(passedChecks).toHaveLength(0);
|
||||
expect(failedChecks).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
|
||||
const securityCheckMapping: {[key: string]: string} = {
|
||||
const securityCheckMapping: { [key: string]: string } = {
|
||||
is_host_an_ipv4: 'Host is an IPv4 Address',
|
||||
is_uncommon_host_length: 'Uncommon Host Length',
|
||||
is_uncommon_dash_char_count: 'Uncommon Number of Dashes in Host',
|
||||
|
|
@ -51,7 +50,8 @@ const securityCheckMapping: {[key: string]: string} = {
|
|||
is_masked_linux_elf_file: 'Masked Linux ELF File Detected',
|
||||
is_masked_windows_exe_file: 'Masked Windows Executable Detected',
|
||||
is_ms_office_file: 'Microsoft Office File Detected',
|
||||
is_windows_exe_file_on_free_dynamic_dns: 'Windows Executable on Free Dynamic DNS',
|
||||
is_windows_exe_file_on_free_dynamic_dns:
|
||||
'Windows Executable on Free Dynamic DNS',
|
||||
is_windows_exe_file_on_free_hosting: 'Windows Executable on Free Hosting',
|
||||
is_windows_exe_file_on_ipv4: 'Windows Executable on IPv4 Address',
|
||||
is_windows_exe_file: 'Windows Executable Detected',
|
||||
|
|
@ -65,91 +65,98 @@ const securityCheckMapping: {[key: string]: string} = {
|
|||
is_domain_very_recent: 'Domain Very Recently Created',
|
||||
is_credit_card_field: 'Credit Card Field Present',
|
||||
is_password_field: 'Password Field Present',
|
||||
is_valid_https: 'Valid HTTPS Connection'
|
||||
is_valid_https: 'Valid HTTPS Connection',
|
||||
};
|
||||
const passFailLogic: {[key: string]: boolean | string} = {
|
||||
// True means the check needs to be true to pass
|
||||
is_valid_https: true,
|
||||
is_host_an_ipv4: false,
|
||||
is_uncommon_host_length: false,
|
||||
is_uncommon_dash_char_count: false,
|
||||
is_uncommon_dot_char_count: false,
|
||||
is_uncommon_host_name: false,
|
||||
is_suspicious_url_pattern: false,
|
||||
is_suspicious_file_extension: false,
|
||||
is_robots_noindex: false,
|
||||
is_suspended_page: false,
|
||||
is_most_abused_tld: false,
|
||||
is_uncommon_clickable_url: false,
|
||||
is_phishing_heuristic: false,
|
||||
is_possible_emotet: false,
|
||||
is_redirect_to_search_engine: false,
|
||||
is_redirect_to_wikipedia: false,
|
||||
is_http_status_error: false,
|
||||
is_http_server_error: false,
|
||||
is_http_client_error: false,
|
||||
is_suspicious_content: false,
|
||||
is_url_accessible: true,
|
||||
is_empty_page_title: false,
|
||||
is_empty_page_content: false,
|
||||
is_domain_ipv4_assigned: true,
|
||||
is_domain_ipv4_private: false,
|
||||
is_domain_ipv4_loopback: false,
|
||||
is_domain_ipv4_reserved: false,
|
||||
is_domain_ipv4_valid: true,
|
||||
is_domain_blacklisted: false,
|
||||
is_suspicious_domain: false,
|
||||
is_sinkholed_domain: false,
|
||||
is_defaced_heuristic: false,
|
||||
is_masked_file: false,
|
||||
is_risky_geo_location: false,
|
||||
is_china_country: false,
|
||||
is_nigeria_country: false,
|
||||
is_non_standard_port: false,
|
||||
is_email_address_on_url_query: false,
|
||||
is_directory_listing: false,
|
||||
is_exe_on_directory_listing: false,
|
||||
is_zip_on_directory_listing: false,
|
||||
is_php_on_directory_listing: false,
|
||||
is_doc_on_directory_listing: false,
|
||||
is_pdf_on_directory_listing: false,
|
||||
is_apk_on_directory_listing: false,
|
||||
is_linux_elf_file: false,
|
||||
is_linux_elf_file_on_free_dynamic_dns: false,
|
||||
is_linux_elf_file_on_free_hosting: false,
|
||||
is_linux_elf_file_on_ipv4: false,
|
||||
is_masked_linux_elf_file: false,
|
||||
is_masked_windows_exe_file: false,
|
||||
is_ms_office_file: false,
|
||||
is_windows_exe_file_on_free_dynamic_dns: false,
|
||||
is_windows_exe_file_on_free_hosting: false,
|
||||
is_windows_exe_file_on_ipv4: false,
|
||||
is_windows_exe_file: false,
|
||||
is_android_apk_file_on_free_dynamic_dns: false,
|
||||
is_android_apk_file_on_free_hosting: false,
|
||||
is_android_apk_file_on_ipv4: false,
|
||||
is_android_apk_file: false,
|
||||
is_external_redirect: false,
|
||||
is_risky_category: false,
|
||||
is_domain_recent: "no",
|
||||
is_domain_very_recent: "no",
|
||||
is_credit_card_field: false,
|
||||
is_password_field: false,
|
||||
const passFailLogic: { [key: string]: boolean | string } = {
|
||||
// True means the check needs to be true to pass
|
||||
is_valid_https: true,
|
||||
is_host_an_ipv4: false,
|
||||
is_uncommon_host_length: false,
|
||||
is_uncommon_dash_char_count: false,
|
||||
is_uncommon_dot_char_count: false,
|
||||
is_uncommon_host_name: false,
|
||||
is_suspicious_url_pattern: false,
|
||||
is_suspicious_file_extension: false,
|
||||
is_robots_noindex: false,
|
||||
is_suspended_page: false,
|
||||
is_most_abused_tld: false,
|
||||
is_uncommon_clickable_url: false,
|
||||
is_phishing_heuristic: false,
|
||||
is_possible_emotet: false,
|
||||
is_redirect_to_search_engine: false,
|
||||
is_redirect_to_wikipedia: false,
|
||||
is_http_status_error: false,
|
||||
is_http_server_error: false,
|
||||
is_http_client_error: false,
|
||||
is_suspicious_content: false,
|
||||
is_url_accessible: true,
|
||||
is_empty_page_title: false,
|
||||
is_empty_page_content: false,
|
||||
is_domain_ipv4_assigned: true,
|
||||
is_domain_ipv4_private: false,
|
||||
is_domain_ipv4_loopback: false,
|
||||
is_domain_ipv4_reserved: false,
|
||||
is_domain_ipv4_valid: true,
|
||||
is_domain_blacklisted: false,
|
||||
is_suspicious_domain: false,
|
||||
is_sinkholed_domain: false,
|
||||
is_defaced_heuristic: false,
|
||||
is_masked_file: false,
|
||||
is_risky_geo_location: false,
|
||||
is_china_country: false,
|
||||
is_nigeria_country: false,
|
||||
is_non_standard_port: false,
|
||||
is_email_address_on_url_query: false,
|
||||
is_directory_listing: false,
|
||||
is_exe_on_directory_listing: false,
|
||||
is_zip_on_directory_listing: false,
|
||||
is_php_on_directory_listing: false,
|
||||
is_doc_on_directory_listing: false,
|
||||
is_pdf_on_directory_listing: false,
|
||||
is_apk_on_directory_listing: false,
|
||||
is_linux_elf_file: false,
|
||||
is_linux_elf_file_on_free_dynamic_dns: false,
|
||||
is_linux_elf_file_on_free_hosting: false,
|
||||
is_linux_elf_file_on_ipv4: false,
|
||||
is_masked_linux_elf_file: false,
|
||||
is_masked_windows_exe_file: false,
|
||||
is_ms_office_file: false,
|
||||
is_windows_exe_file_on_free_dynamic_dns: false,
|
||||
is_windows_exe_file_on_free_hosting: false,
|
||||
is_windows_exe_file_on_ipv4: false,
|
||||
is_windows_exe_file: false,
|
||||
is_android_apk_file_on_free_dynamic_dns: false,
|
||||
is_android_apk_file_on_free_hosting: false,
|
||||
is_android_apk_file_on_ipv4: false,
|
||||
is_android_apk_file: false,
|
||||
is_external_redirect: false,
|
||||
is_risky_category: false,
|
||||
is_domain_recent: 'no',
|
||||
is_domain_very_recent: 'no',
|
||||
is_credit_card_field: false,
|
||||
is_password_field: false,
|
||||
};
|
||||
|
||||
export const analyzeSecurityChecks = (checks: { [key: string]: boolean | string }) => {
|
||||
|
||||
let passedChecks = [];
|
||||
let failedChecks = [];
|
||||
export const analyzeSecurityChecks = (checks: {
|
||||
[key: string]: boolean | string;
|
||||
}) => {
|
||||
const passedChecks = [];
|
||||
const failedChecks = [];
|
||||
for (const [check, value] of Object.entries(checks)) {
|
||||
let shouldPass = passFailLogic.hasOwnProperty(check) ? passFailLogic[check] : false;
|
||||
let actualPass = (value === shouldPass) || (shouldPass === true && value !== "no");
|
||||
const shouldPass = Object.prototype.hasOwnProperty.call(
|
||||
passFailLogic,
|
||||
check,
|
||||
)
|
||||
? passFailLogic[check]
|
||||
: false;
|
||||
const actualPass =
|
||||
value === shouldPass || (shouldPass === true && value !== 'no');
|
||||
|
||||
if (actualPass) {
|
||||
passedChecks.push(securityCheckMapping[check]);
|
||||
} else {
|
||||
failedChecks.push(securityCheckMapping[check]);
|
||||
}
|
||||
if (actualPass) {
|
||||
passedChecks.push(securityCheckMapping[check]);
|
||||
} else {
|
||||
failedChecks.push(securityCheckMapping[check]);
|
||||
}
|
||||
}
|
||||
return { passedChecks, failedChecks };
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { vitePreprocess } from '@astrojs/svelte';
|
||||
|
||||
export default {
|
||||
preprocess: vitePreprocess(),
|
||||
preprocess: vitePreprocess(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@components/*": ["src/components/*"],
|
||||
"@layouts/*": ["src/layouts/*"],
|
||||
"@utils/*": ["src/utils/*"]
|
||||
}
|
||||
}
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@components/*": ["src/components/*"],
|
||||
"@layouts/*": ["src/layouts/*"],
|
||||
"@utils/*": ["src/utils/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
web/vitest.config.ts
Normal file
16
web/vitest.config.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
import { resolve } from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@components': resolve(__dirname, 'src/components'),
|
||||
'@layouts': resolve(__dirname, 'src/layouts'),
|
||||
'@utils': resolve(__dirname, 'src/utils'),
|
||||
},
|
||||
},
|
||||
});
|
||||
5092
web/yarn.lock
5092
web/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue