Fixes remaining lint issues, updates footer date

This commit is contained in:
Alicia Sykes 2026-02-26 14:36:21 +00:00
parent 19ae398e7f
commit e1c0318d9b
18 changed files with 173 additions and 93 deletions

View file

@ -1,8 +1,13 @@
---
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
href="https://github.com/Lissy93/awesome-privacy/blob/main/LICENSE"
>CC0 1.0 Universal</a
>
© <a href="https://aliciasykes.com">Alicia Sykes</a> 2024 | Source code available
© <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>

View file

@ -20,24 +20,31 @@
const serviceCrypto = writable(false);
const additionalInfo = writable('');
let codeBlock: any;
let codeBlock: HTMLElement | undefined;
let interactiveActivated = false;
$: (yamlText, updateHighlighting());
/* eslint-disable svelte/no-dom-manipulating -- hljs requires direct DOM access for syntax highlighting */
function updateHighlighting() {
if (codeBlock) {
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> = {};
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];
@ -397,6 +404,7 @@
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>

View file

@ -1,6 +1,6 @@
---
import type { AndroidInfo } from '@utils/fetch-android-info';
import { formatDate, timeAgo } from '@utils/dates-n-stuff';
import { formatDate } from '@utils/dates-n-stuff';
import FontAwesome from '@components/form/FontAwesome.svelte';
interface Props {

View file

@ -1,7 +1,5 @@
---
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;

View file

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

View file

@ -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,

View file

@ -1,7 +1,6 @@
---
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;

View file

@ -48,7 +48,7 @@
<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}

View file

@ -2,21 +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(() => {
@ -91,7 +87,7 @@
{#if searchQuery.length > 0}
<div class="suggestions">
<ul>
{#each results as result}
{#each results as result (result.name + result.category + result.sectionName)}
<li class="result-row">
<a
href={makeResultLink(

View file

@ -92,5 +92,5 @@ const { service, sectionName, categoryName } = Astro.props;
</div>
<style lang="scss">
@import './service-card.scss';
@use './service-card.scss';
</style>

View file

@ -44,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>
@ -64,5 +65,5 @@
</div>
<style lang="scss">
@import './service-card.scss';
@use './service-card.scss';
</style>

View file

@ -10,7 +10,7 @@ interface Props {
keywords?: string; // Overide keywords tag
hideNav?: boolean; // Don't show the navbar (just homepage)
author?: string; // Author of the content
customSchemaJson?: any; // Custom schema item
customSchemaJson?: Record<string, unknown>; // Custom schema item
breadcrumbs?: Array<{
name: string;
item: string;
@ -121,6 +121,7 @@ const makeSearchLd = () => {
{
breadcrumbs && (
<script
is:inline
type="application/ld+json"
set:html={JSON.stringify(makeBreadcrumbs())}
/>
@ -129,12 +130,14 @@ const makeSearchLd = () => {
{
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)}
/>

View file

@ -12,25 +12,44 @@ import {
appDescription,
} from '../site-config';
const contributorsResource = async () => {
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);
if (!response.ok) {
throw new Error('Failed to fetch contributors');
}
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';
@ -98,10 +117,10 @@ const licenseContent = async () => {
<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) => (
{
sponsors ? (
<div class="user-list">
{sponsors.map((sponsor) => (
<a
class="user"
href={`https://github.com/${sponsor.login}`}
@ -111,10 +130,16 @@ const licenseContent = async () => {
<img src={sponsor.avatarUrl} alt={sponsor.login} />
<p>{sponsor.name || sponsor.login}</p>
</a>
));
})
}
</div>
))}
</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>
@ -122,23 +147,29 @@ const licenseContent = async () => {
maintain it.<br />
Special thanks to the below, top-100 contributors 🌟
</p>
<div class="user-list">
{
contributorsResource().then((sponsors) => {
return sponsors.map((sponsor: any) => (
{
contributors ? (
<div class="user-list">
{contributors.map((contributor) => (
<a
class="user"
href={sponsor.html_url}
href={contributor.html_url}
target="_blank"
rel="noreferrer"
>
<img src={sponsor.avatar_url} alt={sponsor.login} />
<p>{sponsor.login}</p>
<img src={contributor.avatar_url} alt={contributor.login} />
<p>{contributor.login}</p>
</a>
));
})
}
</div>
))}
</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 />
@ -281,7 +312,7 @@ const licenseContent = async () => {
<style lang="scss">
h2 {
font-size: 2rem;
margin: 2rem 0 1rem 0;
margin: 1rem 0 0.5rem 0;
}
p {
font-size: 1.2rem;
@ -343,18 +374,25 @@ const licenseContent = async () => {
.user-list {
display: flex;
flex-wrap: wrap;
gap: 1rem;
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;
@ -364,6 +402,12 @@ const licenseContent = async () => {
}
}
.fallback-img {
max-width: 100%;
margin: 2rem 0;
border-radius: var(--curve-sm);
}
.license-content {
max-height: 500px;
overflow: scroll;
@ -372,7 +416,7 @@ const licenseContent = async () => {
border-radius: var(--curve-sm);
padding: 0.5rem;
font-size: 0.7rem;
font-family: mono;
font-family: 'Lekton', monospace;
max-width: 100vw;
white-space: pre-line;
}

View file

@ -1,11 +1,9 @@
---
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[]);

View file

@ -4,15 +4,14 @@ 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;
@ -28,26 +27,35 @@ const searchResults = fuse
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 (!grouped[categoryName]) {
grouped[categoryName] = { categoryName, sections: {} };
}
if (!acc[categoryName].sections[sectionName]) {
acc[categoryName].sections[sectionName] = { sectionName, items: [] };
if (!grouped[categoryName].sections[sectionName]) {
grouped[categoryName].sections[sectionName] = { sectionName, items: [] };
}
acc[categoryName].sections[sectionName].items.push(service);
grouped[categoryName].sections[sectionName].items.push(item);
}
return acc;
}, {});
// Convert the grouped object into the desired array structure.
// And fuck it, let's use `any`
return Object.values(grouped).map((category: any) => ({
return Object.values(grouped).map((category) => ({
categoryName: category.categoryName,
sections: Object.values(category.sections),
}));
@ -74,7 +82,7 @@ const beer = putResultsIntoGroups();
</section>
<div class="results">
{
beer.map((category: any) => (
beer.map((category) => (
<div class="category">
<a class="category-title" href={`/${slugify(category.categoryName)}`}>
<h3>{category.categoryName}</h3>
@ -83,11 +91,11 @@ const beer = putResultsIntoGroups();
<FontAwesome iconName={slugify(category.categoryName)} />
</span>
<ul class="section-list">
{category.sections.map((section: any) => (
{category.sections.map((section) => (
<li class="section-item">
<h4>{section.sectionName}</h4>
<ul class="service-list">
{section.items.map((item: Service) => (
{section.items.map((item) => (
<li>
<a href={item.url}>{item.name}</a>
</li>

View file

@ -1,5 +1,6 @@
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 =>
@ -35,7 +36,7 @@ describe('prepareSearchItems', () => {
}),
] as Category[];
const items = prepareSearchItems(categories);
const section = items.find((i: { type: string }) => i.type === 'Section');
const section = items.find((i: SearchItem) => i.type === 'Section');
expect(section).toMatchObject({
type: 'Section',
sectionName: 'Messaging',
@ -66,7 +67,7 @@ describe('prepareSearchItems', () => {
}),
] as Category[];
const items = prepareSearchItems(categories);
const service = items.find((i: { type: string }) => i.type === 'Service');
const service = items.find((i: SearchItem) => i.type === 'Service');
expect(service).toMatchObject({
type: 'Service',
name: 'Signal',
@ -99,7 +100,7 @@ describe('prepareSearchItems', () => {
}),
] as Category[];
const items = prepareSearchItems(categories);
const cat = items.find((i: { type: string }) => i.type === 'Category');
expect(cat.itemCount).toBe(3);
const cat = items.find((i: SearchItem) => i.type === 'Category');
expect(cat?.itemCount).toBe(3);
});
});

View file

@ -1,7 +1,19 @@
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) => {
items.push({

View file

@ -61,7 +61,7 @@ interface Redirection {
found: boolean;
external: boolean;
url: string;
redirects: any[];
redirects: string[];
}
interface ResponseHeaders {