awesome-privacy/web/src/pages/[...listing].astro

700 lines
19 KiB
Plaintext

---
import Layout from '@layouts/Layout.astro';
import ServiceList from '@components/things/ServiceList.astro';
import Comments from '@components/things/Comments.svelte';
import GitHubDetailedInfo from '@components/things/GitHubDetailedInfo.astro';
import PrivacyPolicyDetails from '@components/things/PrivacyPolicyDetails.astro';
import WebsiteDetailedInfo from '@components/things/WebsiteDetailedInfo.astro';
import DockerDetailedInfo from '@components/things/DockerDetailedInfo.astro';
import IosAppDetailedInfo from '@components/things/IosAppDetailedInfo.astro';
import AndroidAppDetailedInfo from '@components/things/AndroidDetailedInfo.astro';
import DiscordDetailedInfo from '@components/things/DiscordDetailedInfo.astro';
import RedditDetailedInfo from '@components/things/RedditDetailedInfo.astro';
import SocialShare from '@components/form/Social.astro';
import DataActions from '@components/things/DataActions.svelte';
import type { AwesomePrivacy, Section, Service } from '../types/Service';
import { fetchData, slugify } from '@utils/fetch-data';
import { fetchGitHubStats } from '@utils/fetch-repo-info';
import { fetchTosdrPrivacy } from '@utils/fetch-privacy-policy';
import { fetchWebsiteInfo } from '@utils/fetch-website-info';
import { fetchDockerData } from '@utils/fetch-docker-instructions';
import { fetchIosInfo } from '@utils/fetch-ios-info';
import { fetchAndroidInfo } from '@utils/fetch-android-info';
import { fetchDiscordInfo } from '@utils/fetch-discord-info';
import { fetchRedditInfo } from '@utils/fetch-reddit-info';
import { parseMarkdown, formatLink } from '@utils/parse-markdown';
import { site } from '@utils/config';
import FontAwesome from '@components/form/FontAwesome.svelte';
import Button from '@components/form/Button.astro';
import SaveListing from '@components/things/SaveListing.svelte';
interface Props extends Service {
slug: string;
parentSection: Section;
categoryName: string;
}
const {
name,
description,
url,
github,
tosdrId,
discordInvite,
subreddit,
iosApp,
androidApp,
icon,
followWith,
securityAudited,
openSource,
acceptsCrypto,
parentSection,
categoryName,
} = Astro.props;
/**
* Make a list of keywords, for the <meta keywords> tag
*/
const makeKeyWordTag = () => {
const keywords: string[] = [];
keywords.push(`open source ${parentSection.name}`);
keywords.push(`${parentSection.name} comparison`);
(parentSection.alternativeTo || []).forEach((alt: string) => {
keywords.push(`${alt} alternative`);
});
(parentSection.services || []).forEach((serv: Service) => {
keywords.push(`${serv.name} vs`);
});
keywords.push('private');
keywords.push('ad free');
keywords.push('encrypted');
keywords.push('open source software');
keywords.push('privacy respecting apps');
keywords.push('free tools');
return keywords.join(', ');
};
/**
* Make a page title
*/
const makePageTitle = () => {
return `${name} | ${parentSection.name} | ${categoryName} | Awesome Privacy`;
};
/**
* Make a string page intro, for the description tag
*/
const makeDescriptionTag = () => {
let description = `Analysis of ${name}, and other privacy-respecting ${parentSection.name} `;
if (parentSection.services) {
const serviceList = parentSection.services.map((serv: Service) => serv.name);
description += `Compare ${serviceList.join(' vs ')} more apps. `
}
if (parentSection.alternativeTo && parentSection.alternativeTo.length > 0) {
description += `The best secure encrypted alternatives to ${parentSection.alternativeTo.join(', ')} in 2024. `;
}
description += 'All this, and many more free and awesome tools and software.'
return description;
};
// Return a list of Services, except for the currtent one
const filterServices = () => {
return parentSection.services.filter((service: Service) => service.name !== name);
};
export async function getStaticPaths() {
const pages = await fetchData().then((data) => {
const results: Array<Props> = [];
if (!data || !data.categories) return results;
(data as AwesomePrivacy).categories.forEach((category) => {
category.sections.forEach((section) => {
const services = (section.services || []).map((service) => {
return {
slug: `${slugify(category.name)}/${slugify(section.name)}/${slugify(service.name)}`,
parentSection: section,
categoryName: category.name,
...service,
};
});
results.push(...services);
});
});
return results;
});
return pages.map((props: Props) => {
return {params: { listing: props.slug }, props };
});
}
const makePaginationLinks = () => {
const allServices = parentSection.services || [];
const index = allServices.findIndex(item => item.name === name);
const previousItem = index > 0 ? allServices[index - 1].name : null;
const nextItem = index < allServices.length - 1 ? allServices[index + 1].name : null;
return { previous: previousItem, next: nextItem };
};
const { previous, next } = makePaginationLinks();
const ignoredSites = ['github.gom', 'wikipedia.'];
// Fetch detailed data about the services GitHub repo, privacy policy and website
const githubData = github ? await fetchGitHubStats(github) : null;
const privacyData = tosdrId ? await fetchTosdrPrivacy(tosdrId) : null;
const iosData = iosApp ? await fetchIosInfo(iosApp) : null;
const androidData = androidApp ? await fetchAndroidInfo(androidApp) : null;
const discordData = discordInvite ? await fetchDiscordInfo(discordInvite) : null;
const redditData = subreddit ? await fetchRedditInfo(subreddit) : null;
const dockerData = await fetchDockerData(name);
const websiteData = (url && !ignoredSites.some(ignoredSite => url.includes(ignoredSite))) ? await fetchWebsiteInfo(url) : null;
const findPrivacyPolicyLink = () => {
if (!privacyData) return null;
const docs = privacyData.parameters.documents;
if (docs) {
const privacyPolicy = (docs || []).find((doc) => doc.name.toLowerCase().includes('privacy policy'));
if (privacyPolicy) return privacyPolicy.url;
}
return `https://tosdr.org/en/service/${tosdrId}`;
};
const privacyPolicyLink = findPrivacyPolicyLink();
const getApiEndpoint = () => {
return `https://api.awesome-privacy.xyz/${slugify(categoryName)}/${slugify(parentSection.name)}/${slugify(name)}`;
}
---
<Layout title={makePageTitle()} keywords={makeKeyWordTag()} description={makeDescriptionTag()} >
<main>
<section>
<div class="service-head">
<h2>{name}</h2>
<span class="url-wrap">
<a class="service-url" href={url}>{formatLink(url)}</a>
<span class="follow-with">{followWith}</span>
</span>
</div>
<div class="breadcrumbs">
<span>
<a href="/">Awesome Privacy</a>
➔ <a href={`/${slugify(categoryName)}`}>{categoryName}</a>
➔ <a href={`/${slugify(categoryName)}/${slugify(parentSection.name)}`}>{parentSection.name}</a>
➔ <a href={`/${slugify(categoryName)}/${slugify(parentSection.name)}/${slugify(name)}`}>{name}</a>
</span>
</div>
<div class="save-listing">
<SaveListing client:visible
categoryName={categoryName}
sectionName={parentSection.name}
serviceName={name}
showLabel={true}
/>
</div>
<div class="intro">
<img
width="60"
height="60"
loading="lazy"
decoding="async"
class="service-icon"
alt={`${name} Icon`}
data-service-url={formatLink(url)}
src={icon || `https://icon.horse/icon/${formatLink(url)}`}
/>
<div>
<p class="description" set:html={parseMarkdown(description)}></p>
<ul>
{ url && (
<li>
<b>Homepage:</b>
<a href={url}>{formatLink(url)}</a>
</li>
)}
{ github && (
<li>
<b>GitHub:</b>
<a href={`https://github.com/${github}`}>github.com/{github}</a>
</li>
)}
{ tosdrId && (
<li>
<b>Privacy:</b>
<a href={privacyPolicyLink}>{formatLink(privacyPolicyLink || '')}</a>
</li>
)}
{ iosApp && (
<li>
<b>iOS App:</b>
<a href={iosApp}>{formatLink(iosApp)}</a>
</li>
)}
{ androidApp && (
<li>
<b>Android App:</b>
<a href={`https://play.google.com/store/apps/details?id=${androidApp}`}>{`https://play.google.com/store/apps/details?id=${androidApp}`}</a>
</li>
)}
{ discordInvite && (
<li>
<b>Discord Invite:</b>
<a href={`https://discord.com/invite/${discordInvite}`}>{discordInvite}</a>
</li>
)}
{ subreddit && (
<li>
<b>Subreddit:</b>
<a href={`https://reddit.com/r/${subreddit}`}>r/{subreddit}</a>
</li>
)}
{ url && (
<li>
<b>Web info:</b>
<a href={`https://web-check.xyz/results/${formatLink(url)}`}>{`web-check.xyz/results/${formatLink(url).split('/')[0]}`}</a>
</li>
)}
</ul>
</div>
</div>
<div class="highlights">
{ securityAudited && (
<span class="meta-item great" title={`${name} has been security audited by an accredited auditor, with results published publicly`}>
<FontAwesome iconName="securityAudited" /> Security Audited
</span>
)}
{ acceptsCrypto && (
<span class="meta-item great" title={`${name} accepts anonymous payment methods`}>
<FontAwesome iconName="cryptoAccepted" /> Crypto Payments Accepted
</span>
)}
{ securityAudited === false && (
<span class="meta-item warning" title={`${name} has not been audited`}>
<FontAwesome iconName="notSecurityAudited" /> No Security Audit
</span>
)}
{ (openSource === false) && (
<span class="warning">
<FontAwesome iconName="closedSource" />
Not Open Source
</span>
)}
{ openSource || (github && openSource !== false) ? (
<span class="meta-item great" title={`${name} is open source`}>
<FontAwesome iconName="openSource" /> Open Source
</span>
) : null }
</div>
</section>
{ privacyData && privacyData.parameters.points.length > 0 && (
<section>
<h3>{name} Privacy Policy</h3>
<PrivacyPolicyDetails privacyData={privacyData} />
</section>
)}
{ github && githubData && (
<section class="github-wrap">
<h3>{name} Source Code</h3>
<GitHubDetailedInfo gitHubData={githubData} repo={github} />
</section>
)}
{ websiteData && (
<section>
<h3>{name} Website</h3>
<WebsiteDetailedInfo url={url} websiteInfo={websiteData} />
</section>
)}
{ androidData && (
<section>
<h3>{name} Android App</h3>
<AndroidAppDetailedInfo androidData={androidData} />
</section>
)}
{ iosData && (
<section>
<h3>{name} iOS App</h3>
<IosAppDetailedInfo url={url} iosData={iosData} />
</section>
)}
{ dockerData && dockerData.found && (
<section>
<h3>{name} Docker</h3>
<DockerDetailedInfo docker={dockerData} />
</section>
)}
{ (discordData || redditData) && (
<section>
<h3>{name} Socials</h3>
<div class="social-wrap">
{ discordData && <DiscordDetailedInfo discordData={discordData} /> }
{ redditData && <RedditDetailedInfo redditData={redditData} /> }
</div>
</section>
)}
<section>
<h3>{name} Reviews</h3>
<Comments client:visible />
</section>
<section>
<h3>More {parentSection.name}</h3>
<ServiceList
services={filterServices()}
subHeading={true}
noGitHubMetrics={true}
buttonLink={`/${slugify(categoryName)}/${slugify(parentSection.name)}`}
categoryName={categoryName}
sectionName={parentSection.name}
/>
</section>
<section>
<h3>About the Data: {name}</h3>
<DataActions client:load categoryName={categoryName} sectionName={parentSection.name} serviceName={name} />
<h4>API</h4>
<p>
You can access {name}'s data programmatically via our API.
Simply make a <code>GET</code> request to:
</p>
<pre><a href={getApiEndpoint()}><code>{getApiEndpoint()}</code></a></pre>
<p>
The REST API is free, no-auth and CORS-enabled.
To learn more, view the <a href="/api">Swagger Docs</a> or read the <a href="/about#api">API Usage Guide</a>.
</p>
<h4>About the Data</h4>
<p>
Beyond the user-submitted YAML you see above, we also augment each listing with
additional data dynamically fetched from several sources.
To learn more about where the rest of data included in this page comes from,
and how it is computed,
see the <a href="/about#our-data">About the Data</a> section of our About page.
</p>
</section>
<section>
<h3>Share {name}</h3>
<p>
Help your friends compare {parentSection.name}, and pick privacy-respecting software and services.<br />
Share {name} and Awesome Privacy with your network!
<div>
<SocialShare
url={`${site}/${slugify(categoryName)}/${slugify(parentSection.name)}/${slugify(name)} `}
title={`Checkout ${name}! \n`}
description={
`Compare #${name} and other privacy-respecting ${parentSection.name} at `
+ `Awesome-Privacy.xyz, the free and open source list of private software alternatives. `
+ `#privacy #opensource #awesomeprivacy by @Lissy_Sykes`}
/>
</div>
</p>
</section>
<div class="pagination-navigation">
{ previous ? (
<Button url={`/${slugify(categoryName)}/${slugify(parentSection.name)}/${slugify(previous)}`}>
<span>← Previous</span>
<p>{previous}</p>
</Button>
) : <p class="nothing"></p>}
<a href={`/${slugify(categoryName)}/${slugify(parentSection.name)}`} class="go-to-category">
View {parentSection.name} ({(parentSection.services || []).length})
</a>
{ next && (
<Button url={`/${slugify(categoryName)}/${slugify(parentSection.name)}/${slugify(next)}`}>
<span>Next →</span>
<p>{next}</p>
</Button>
)}
</div>
</main>
</Layout>
<style lang="scss">
main {
margin: 2rem auto;
padding: 1rem;
width: 1000px;
max-width: calc(100% - 5rem);
position: relative;
@media(max-width: 768px) {
max-width: 95%;
padding: 0.5rem;
margin: 0 auto;
}
}
section {
margin: 1rem 0 4rem 0;
padding: 1rem;
min-height: 8rem;
position: relative;
border: 2px solid var(--box-outline);
box-shadow: 6px 6px 0 var(--box-outline);
background: var(--accent-fg);
}
p {
margin: 0.25rem 0 0.5rem 0;
}
h3 {
font-size: 2rem;
margin: -2rem 0 1rem -2rem;
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 2rem auto;
}
}
.service-head {
h2 {
font-size: 3rem;
margin: 0;
@media(max-width: 768px) {
text-align: center;
}
}
.url-wrap {
display: flex;
align-items: center;
gap: 0.5rem;
a {
font-size: 1.2rem;
}
.follow-with {
font-style: italic;
opacity: 0.6;
}
@media(max-width: 768px) {
flex-direction: column;
}
}
}
.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;
}
}
.save-listing {
position: absolute;
top: 3rem;
right: 1rem;
}
h4 {
font-size: 1.4rem;
margin-bottom: 0;
}
.intro {
font-size: 1.2rem;
font-style: italic;
opacity: 0.9;
display: flex;
gap: 0.5rem;
@media(max-width: 768px) {
flex-direction: column;
}
.service-icon {
border-radius: var(--curve-md);
margin-top: 1rem;
@media(max-width: 768px) {
margin: 1rem auto 0 auto;
}
}
ul {
padding-left: 1rem;
list-style: circle;
font-style: normal;
font-size: 1rem;
margin-top: 0;
b {
font-size: 1rem;
font-weight: 400;
min-width: 85px;
display: inline-block;
@media(max-width: 768px) {
width: auto;
display: block;
}
}
}
}
.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;
@media (max-width: 768px) {
display: none;
}
}
}
.further-info, .notable-mentions, .word-of-warning {
h3 {
font-size: 1.4rem;
margin: 1rem 0;
}
:global(p) {
font-size: 0.9rem;
opacity: 0.7;
:global(strong) {
font-weight: 500;
}
:global(a) {
color: var(--foreground);
transition: all 0.15s ease-in-out;
&:hover {
color: var(--accent);
}
}
}
:global(strong) {
font-weight: 500;
}
:global(h4) {
font-size: 1.2rem;
margin: 0.5rem 0 0 0;
}
:global(ul) {
list-style: circle;
padding-left: 1rem;
font-size: 0.9rem;
opacity: 0.7;
li {
margin-bottom: 0.25rem;
:global(p) {
display: inline;
}
}
}
}
pre {
a {
text-decoration: none;
color: var(--foreground);
code {
background: #acabb782;
padding: 0.2rem 0.4rem;
border-radius: var(--curve-sm);
font-family: Courier New;
font-size: 0.9rem;
@media(max-width: 768px) {
white-space: break-spaces;
}
}
}
}
.social-wrap {
display: flex;
gap: 1rem;
flex-wrap: wrap;
justify-content: space-between;
}
.highlights {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
justify-content: end;
.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: #007930; // var(--success);
:global(svg) {
color: #007930; // var(--success);
}
}
}
</style>