Compare commits

..

No commits in common. "main" and "static" have entirely different histories.
main ... static

107 changed files with 1093 additions and 5445 deletions

View file

@ -1,74 +0,0 @@
name: Build and Deploy Dark-theme Static Site
on:
push:
branches:
- main
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
build:
runs-on: docker
container: git.private.coffee/privatecoffee/static-site-builder:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
python3 -m pip install -r requirements.txt --break-system-packages
- name: Generate static site
run: python3 main.py --theme dark --is-primary --domains dark.private.coffee
- name: Set up SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -t ed25519 git.private.coffee >> ~/.ssh/known_hosts
# Create SSH config file to ensure the correct identity file is used
echo "Host git.private.coffee" > ~/.ssh/config
echo " IdentityFile ~/.ssh/id_ed25519" >> ~/.ssh/config
echo " IdentitiesOnly yes" >> ~/.ssh/config
chmod 600 ~/.ssh/config
- name: Deploy to pages-dark branch
run: |
# Configure Git
git config --global user.name "Forgejo"
git config --global user.email "noreply@private.coffee"
# Move generated static site files to a temporary location
mv build ../static_site_temp
cp .gitignore ../static_site_temp
# Create a new orphan branch named 'pages-dark'
git checkout --orphan pages-dark
# Remove all files from the working directory
git rm -rf .
# Move the static site files back to the working directory
mv ../static_site_temp/* ./
mv ../static_site_temp/.* ./ 2>/dev/null || true
# Add and commit the static site files
git add .
git commit -m "Deploy static site"
# Set the URL again
git remote set-url origin "git@git.private.coffee:PrivateCoffee/privatecoffee-website.git"
# Force push to the 'pages-dark' branch
git push origin pages-dark --force
- name: Save as artifact
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
with:
name: static-site.zip
path: .

View file

@ -1,76 +0,0 @@
name: Build and Deploy Development Static Site
on:
push:
branches:
- dev
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
build:
runs-on: docker
container: git.private.coffee/privatecoffee/static-site-builder:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: dev
- name: Install dependencies
run: |
python3 -m pip install -r requirements.txt --break-system-packages
- name: Generate static site
run: python3 main.py --dev --domains dev.private.coffee
- name: Set up SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -t ed25519 git.private.coffee >> ~/.ssh/known_hosts
# Create SSH config file to ensure the correct identity file is used
echo "Host git.private.coffee" > ~/.ssh/config
echo " IdentityFile ~/.ssh/id_ed25519" >> ~/.ssh/config
echo " IdentitiesOnly yes" >> ~/.ssh/config
chmod 600 ~/.ssh/config
- name: Deploy to pages-dev branch
run: |
# Configure Git
git config --global user.name "Forgejo"
git config --global user.email "noreply@private.coffee"
# Move generated static site files to a temporary location
mv build ../static_site_temp
cp .gitignore ../static_site_temp
# Create a new orphan branch named 'pages-dev'
git checkout --orphan pages-dev
# Remove all files from the working directory
git rm -rf .
# Move the static site files back to the working directory
mv ../static_site_temp/* ./
mv ../static_site_temp/.* ./ 2>/dev/null || true
# Add and commit the static site files
git add .
git commit -m "Deploy static site"
# Set the URL again
git remote set-url origin "git@git.private.coffee:PrivateCoffee/privatecoffee-website.git"
# Force push to the 'pages-dev' branch
git push origin pages-dev --force
- name: Save as artifact
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
with:
name: static-site.zip
path: .

View file

@ -1,74 +0,0 @@
name: Build and Deploy Pride-Theme Static Site
on:
push:
branches:
- main
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
build:
runs-on: docker
container: git.private.coffee/privatecoffee/static-site-builder:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
python3 -m pip install -r requirements.txt --break-system-packages
- name: Generate static site
run: python3 main.py --theme pride --domains pride.coffee,www.pride.coffee
- name: Set up SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -t ed25519 git.private.coffee >> ~/.ssh/known_hosts
# Create SSH config file to ensure the correct identity file is used
echo "Host git.private.coffee" > ~/.ssh/config
echo " IdentityFile ~/.ssh/id_ed25519" >> ~/.ssh/config
echo " IdentitiesOnly yes" >> ~/.ssh/config
chmod 600 ~/.ssh/config
- name: Deploy to pages-pride branch
run: |
# Configure Git
git config --global user.name "Forgejo"
git config --global user.email "noreply@private.coffee"
# Move generated static site files to a temporary location
mv build ../static_site_temp
cp .gitignore ../static_site_temp
# Create a new orphan branch named 'pages-pride'
git checkout --orphan pages-pride
# Remove all files from the working directory
git rm -rf .
# Move the static site files back to the working directory
mv ../static_site_temp/* ./
mv ../static_site_temp/.* ./ 2>/dev/null || true
# Add and commit the static site files
git add .
git commit -m "Deploy static site"
# Set the URL again
git remote set-url origin "git@git.private.coffee:PrivateCoffee/privatecoffee-website.git"
# Force push to the 'pages-pride' branch
git push origin pages-pride --force
- name: Save as artifact
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
with:
name: static-site.zip
path: .

View file

@ -3,39 +3,24 @@ name: Build and Deploy Static Site
on: on:
push: push:
branches: branches:
- main - static
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs: jobs:
build: build:
runs-on: docker container: node:20-bookworm
container: git.private.coffee/privatecoffee/static-site-builder:latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Install dependencies - name: Install dependencies
run: | run: |
apt update
apt install -y python3 python3-pip
python3 -m pip install -r requirements.txt --break-system-packages python3 -m pip install -r requirements.txt --break-system-packages
- name: Generate static site - name: Generate static site
run: python3 main.py --is-primary run: python3 main.py
- name: Set up SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -t ed25519 git.private.coffee >> ~/.ssh/known_hosts
# Create SSH config file to ensure the correct identity file is used
echo "Host git.private.coffee" > ~/.ssh/config
echo " IdentityFile ~/.ssh/id_ed25519" >> ~/.ssh/config
echo " IdentitiesOnly yes" >> ~/.ssh/config
chmod 600 ~/.ssh/config
- name: Deploy to pages branch - name: Deploy to pages branch
run: | run: |
@ -45,7 +30,6 @@ jobs:
# Move generated static site files to a temporary location # Move generated static site files to a temporary location
mv build ../static_site_temp mv build ../static_site_temp
cp .gitignore ../static_site_temp
# Create a new orphan branch named 'pages' # Create a new orphan branch named 'pages'
git checkout --orphan pages git checkout --orphan pages
@ -61,14 +45,5 @@ jobs:
git add . git add .
git commit -m "Deploy static site" git commit -m "Deploy static site"
# Set the URL again
git remote set-url origin "git@git.private.coffee:PrivateCoffee/privatecoffee-website.git"
# Force push to the 'pages' branch # Force push to the 'pages' branch
git push origin pages --force git push origin pages --force
- name: Save as artifact
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
with:
name: static-site.zip
path: .

1
.gitignore vendored
View file

@ -2,4 +2,3 @@ venv/
*.pyc *.pyc
__pycache__/ __pycache__/
build/ build/
/output

View file

@ -1,4 +0,0 @@
/assets/dist
/assets/img
/assets/*.asc
/blog/

3
.vscode/launch.json vendored
View file

@ -12,8 +12,7 @@
"console": "integratedTerminal", "console": "integratedTerminal",
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"env": { "env": {
"PRIVATECOFFEE_DEV": "1", "PRIVATECOFFEE_DEV": "1"
"PRIVATECOFFEE_DEBUG": "1"
} }
}, },
{ {

View file

@ -1,5 +0,0 @@
{
"files.associations": {
"*.html": "jinja-html"
}
}

View file

@ -1,4 +1,4 @@
Copyright (c) 2023-2025 Private.coffee Team <support@private.coffee> Copyright (c) 2023 Private.coffee Team <support@private.coffee>
Use of the Private.coffee / Private Coffee name or the Private.coffee logo Use of the Private.coffee / Private Coffee name or the Private.coffee logo
without express permission by the Private.coffee team is prohibited. However, without express permission by the Private.coffee team is prohibited. However,

View file

@ -1,15 +1,12 @@
# Private.coffee Website # Private.coffee Website
[![Support Private.coffee!](https://shields.private.coffee/badge/private.coffee-Support%20us!-pink?logo=coffeescript)](https://private.coffee) [![Support Private.coffee!](https://shields.private.coffee/badge/private.coffee-Support%20us!-pink?logo=coffeescript)](https://private.coffee)
[![MIT License](https://shields.private.coffee/badge/license-MIT-blue.svg)](LICENSE)
This is the source code for the [Private.coffee](https://private.coffee) This is the source code for the [Private.coffee](https://private.coffee)
website. website.
It is a simple Jinja2 static website generator that compiles the templates in It is a simple Flask application that generates the HTML for the website based
the `templates` directory in conjunction with the JSON files in the `data` on the services defined in the `services.json` file.
directory and Markdown blog entries in the `blog` directory to generate static
HTML files in the `build` directory.
## Development ## Development
@ -21,23 +18,13 @@ pip install -r requirements.txt
python main.py python main.py
``` ```
The website will be built into the `build` directory, and you can view it by The website will be available at `http://localhost:9810`.
opening the `index.html` file in your browser or using the included HTTP server
(`python main.py --serve`).
## License ## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) This project is licensed under the MIT License - see the [LICENSE](LICENSE)
file for details. file for details.
The assets in `assets/dist` are not part of this project and are subject to
their own licenses.
Blog posts in the `blog` directory are licensed under the [Creative Commons
Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/),
unless otherwise indicated. They may contain additional material under
different licenses - see the individual blog posts for details.
## Attribution ## Attribution
This website is built using the [Bootstrap](https://getbootstrap.com) framework This website is built using the [Bootstrap](https://getbootstrap.com) framework

View file

@ -1,8 +0,0 @@
{
"m.homeserver": {
"base_url": "https://matrix.private.coffee"
},
"org.matrix.msc3575.proxy": {
"url": "https://matrix.private.coffee"
}
}

View file

@ -1,3 +0,0 @@
{
"m.server": "matrix.private.coffee:443"
}

View file

@ -1,13 +0,0 @@
{
"contacts": [
{
"matrix_id": "@kumi:private.coffee",
"email_address": "kumi@private.coffee",
"role": "m.role.admin"
},
{
"email_address": "security@private.coffee",
"role": "m.role.security"
}
]
}

View file

@ -6,78 +6,50 @@
/* General styles */ /* General styles */
:root, :root,
[data-bs-theme="light"] { [data-bs-theme="light"] {
/* Primary color palette */
--bs-primary: #f570b9; --bs-primary: #f570b9;
--bs-primary-rgb: 245, 112, 185; --bs-primary-rgb: 245, 112, 185;
--bs-primary-text-emphasis: #622d4a; --bs-primary-text-emphasis: #622d4a;
--bs-primary-bg-subtle: #fde2f1; --bs-primary-bg-subtle: #fde2f1;
--bs-primary-border-subtle: #fbc6e3; --bs-primary-border-subtle: #fbc6e3;
/* Primary button variations */
--bs-primary-hover: #f785c4;
--bs-primary-active: #f78dc7;
/* Body colors */
--bs-body-color: #232323; --bs-body-color: #232323;
--bs-body-color-rgb: 35, 35, 35; --bs-body-color-rgb: 35, 35, 35;
--bs-secondary-color: rgba(35, 35, 35, 0.75); --bs-secondary-color: rgba(35, 35, 35, 0.75);
--bs-secondary-color-rgb: 35, 35, 35, 0.75; --bs-secondary-color-rgb: 35, 35, 35, 0.75;
--bs-tertiary-color: rgba(35, 35, 35, 0.5); --bs-tertiary-color: rgba(35, 35, 35, 0.5);
--bs-tertiary-color-rgb: 35, 35, 35, 0.5; --bs-tertiary-color-rgb: 35, 35, 35, 0.5;
/* Font family */
--bs-body-font-family: Inconsolata, monospace; --bs-body-font-family: Inconsolata, monospace;
/* UI element colors */
--border-color: #e0e0e0;
--card-bg: #f9f9f9;
--card-hover-bg: #e9e9e9;
--dropdown-bg: #f9f9f9;
--alert-warning-bg: #fff3cd;
--alert-warning-border: #ffeeba;
--alert-warning-color: #856404;
--alert-warning-hover: #604c2e;
--table-highlight-bg: rgb(234, 255, 255);
/* Gradient colors */
--gradient-start: #ba77fc;
--gradient-end: #ff7f8c;
} }
.btn-primary { .btn-primary {
--bs-btn-color: #000000; --bs-btn-color: #000000;
--bs-btn-bg: var(--bs-primary); --bs-btn-bg: #f570b9;
--bs-btn-border-color: var(--bs-primary); --bs-btn-border-color: #f570b9;
--bs-btn-hover-color: #000000; --bs-btn-hover-color: #000000;
--bs-btn-hover-bg: var(--bs-primary-hover); --bs-btn-hover-bg: #f785c4;
--bs-btn-hover-border-color: var(--bs-primary-hover); --bs-btn-hover-border-color: #f67ec0;
--bs-btn-focus-shadow-rgb: 37, 17, 28; --bs-btn-focus-shadow-rgb: 37, 17, 28;
--bs-btn-active-color: #000000; --bs-btn-active-color: #000000;
--bs-btn-active-bg: var(--bs-primary-active); --bs-btn-active-bg: #f78dc7;
--bs-btn-active-border-color: var(--bs-primary-hover); --bs-btn-active-border-color: #f67ec0;
--bs-btn-disabled-color: #000000; --bs-btn-disabled-color: #000000;
--bs-btn-disabled-bg: var(--bs-primary); --bs-btn-disabled-bg: #f570b9;
--bs-btn-disabled-border-color: var(--bs-primary); --bs-btn-disabled-border-color: #f570b9;
color: #fff; color: #fff;
} }
.button-wrapper:not(:last-child) {
margin-bottom: 1rem;
}
.btn-outline-primary { .btn-outline-primary {
--bs-btn-color: var(--bs-primary); --bs-btn-color: #f570b9;
--bs-btn-border-color: var(--bs-primary); --bs-btn-border-color: #f570b9;
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb); --bs-btn-focus-shadow-rgb: 245, 112, 185;
--bs-btn-hover-color: #000000; --bs-btn-hover-color: #000000;
--bs-btn-hover-bg: var(--bs-primary); --bs-btn-hover-bg: #f570b9;
--bs-btn-hover-border-color: var(--bs-primary); --bs-btn-hover-border-color: #f570b9;
--bs-btn-active-color: #000000; --bs-btn-active-color: #000000;
--bs-btn-active-bg: var(--bs-primary); --bs-btn-active-bg: #f570b9;
--bs-btn-active-border-color: var(--bs-primary); --bs-btn-active-border-color: #f570b9;
--bs-btn-disabled-color: var(--bs-primary); --bs-btn-disabled-color: #f570b9;
--bs-btn-disabled-bg: transparent; --bs-btn-disabled-bg: transparent;
--bs-btn-disabled-border-color: var(--bs-primary); --bs-btn-disabled-border-color: #f570b9;
} }
h2 .special-header { h2 .special-header {
@ -90,9 +62,8 @@ h2 .special-header {
} }
.fancy-text-primary { .fancy-text-primary {
background: -webkit-linear-gradient(45deg, var(--gradient-start), var(--gradient-end)); background: -webkit-linear-gradient(45deg, #ba77fc, #ff7f8c);
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
} }
@ -110,7 +81,7 @@ h5 {
font-size: x-large; font-size: x-large;
} }
.card-body :not(p):not(:first-child):not(.dropdown-content):not(.dropdown-toggle-area) { .card-body .btn-primary:not(:first-child) {
margin-top: 10px; margin-top: 10px;
} }
@ -126,59 +97,26 @@ h5 {
.section { .section {
padding: 20px 0; padding: 20px 0;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid #e0e0e0;
} }
.alert-warning { .alert-warning {
background-color: var(--alert-warning-bg); background-color: #fff3cd;
border-color: var(--alert-warning-border); border-color: #ffeeba;
color: var(--alert-warning-color); color: #856404;
padding: 15px; padding: 15px;
margin-bottom: 20px; margin-bottom: 20px;
border-radius: 4px; border-radius: 4px;
} }
.alert-warning .alert-link { .alert-warning .alert-link {
color: var(--alert-warning-color); color: #856404;
font-weight: bold; font-weight: bold;
text-decoration: underline; text-decoration: underline;
} }
.alert-warning .alert-link:hover { .alert-warning .alert-link:hover {
color: var(--alert-warning-hover); color: #604c2e;
}
/* Image Styles */
#logoContainer {
background-size: contain;
background-repeat: no-repeat;
max-width: 400px;
max-height: 400px;
width: 80vh;
height: 80vh;
}
#smallLogoContainer {
background-size: contain;
background-repeat: no-repeat;
width: 64px;
height: 64px;
}
.homemade,
.upstream,
.fork,
.members-only {
right: -0.5rem;
height: 1.5rem;
width: 1.5rem;
position: absolute;
}
.upstream svg,
.homemade svg,
.fork svg {
fill: var(--bs-primary-bg-subtle) !important;
} }
.bs-icon.bs-icon-primary svg { .bs-icon.bs-icon-primary svg {
@ -193,8 +131,16 @@ h5 {
fill: var(--bs-primary-bg-subtle); fill: var(--bs-primary-bg-subtle);
} }
.bg-pride-gradient {
background: linear-gradient(45deg, #FF7878, #FFC898, #FFF89A, #CDF2CA, #A2CDCD, #D1E8E4, #CAB8FF);
}
/* Responsive Styles */ /* Responsive Styles */
@media (max-width: 991px) { @media (max-width: 991px) {
p.text-center.special-header {
font-size: 3rem;
}
.navbar .container { .navbar .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -261,11 +207,6 @@ h5 {
.slogan { .slogan {
display: none; display: none;
} }
#logo-wrapper {
display: flex;
justify-content: center;
}
} }
} }
@ -280,7 +221,7 @@ h5 {
.dropdown-content { .dropdown-content {
display: none; display: none;
position: absolute; position: absolute;
background-color: var(--dropdown-bg); background-color: #f9f9f9;
min-width: 100%; min-width: 100%;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1; z-index: 1;
@ -322,133 +263,3 @@ h5 {
text-decoration: none; text-decoration: none;
color: black; color: black;
} }
/* Accordion Styles */
.accordion {
border: 1px solid var(--border-color);
border-radius: 5px;
}
.accordion-item {
border-bottom: 1px solid var(--border-color);
}
.accordion-header {
display: flex;
align-items: center;
padding: 0.25rem;
cursor: pointer;
background-color: var(--card-bg);
font-size: 1.25rem;
font-weight: bold;
border-bottom: 1px solid var(--border-color);
}
.accordion-header:hover {
background-color: var(--card-hover-bg);
}
.icon-container {
display: flex;
align-items: center;
margin-right: 1rem;
}
.bs-icon {
position: relative;
}
.accordion-body {
padding: 1rem;
display: none;
}
.accordion-item[open] .accordion-body {
display: block;
}
.accordion-header .bs-icon svg {
width: 32px;
}
.accordion-icon {
margin-left: auto;
padding: 0.5rem;
}
.accordion-item[open] .accordion-icon {
transform: rotate(180deg);
}
/* Table Styles */
.transparency-start-balance-row>td {
background-color: var(--table-highlight-bg) !important;
}
/* Service page styles */
.service-section {
scroll-margin-top: 80px;
}
.category-nav {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 2rem;
}
.category-nav a {
margin: 0.25rem;
}
.service-section h2 {
border-bottom: 2px solid var(--bs-primary);
padding-bottom: 0.5rem;
margin-bottom: 1.5rem;
}
/* Smooth scrolling for modern browsers (progressive enhancement) */
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}
/* Theme toggle styles */
.theme-toggle-container {
display: inline-flex;
align-items: center;
}
.theme-toggle-container .btn {
display: flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
padding: 0;
border-radius: 50%;
}
.theme-toggle-container .btn svg {
width: 20px;
height: 20px;
}
/* Dark theme specific toggle styles */
[data-bs-theme="dark"] .theme-toggle-container .btn-outline-primary {
color: var(--bs-primary-hover);
border-color: var(--bs-primary-hover);
}
[data-bs-theme="dark"] .theme-toggle-container .btn-outline-primary:hover {
background-color: var(--bs-primary-hover);
color: var(--bs-body-bg);
}
/* Footer styles */
footer a {
text-decoration: none;
}

View file

@ -1,29 +0,0 @@
/* Autistic Pride Flag Theme */
:root {
/* Override gradient colors */
--gradient-start: #e3505f;
--gradient-end: #63aa8e;
}
.bg-primary-gradient {
background: linear-gradient(to right,
#e3505f 0%,
#e3505f 16.66%,
#f8a67d 16.66%,
#f8a67d 33.32%,
#feda8d 33.32%,
#feda8d 66.64%,
#a4d186 66.64%,
#a4d186 83.3%,
#63aa8e 83.3%,
#63aa8e 100%);
}
#logoContainer {
background-image: url(../../img/logo-white.svg);
}
#smallLogoContainer {
background-image: url(../../img/logo-inv_grad.svg);
}

View file

@ -1,401 +0,0 @@
/* Dark theme */
:root,
[data-bs-theme="dark"] {
/* Primary color palette */
--bs-primary: #cd117b;
--bs-primary-rgb: 210, 94, 160;
--bs-primary-text-emphasis: #cd117b;
--bs-primary-bg-subtle: #291b24;
--bs-primary-border-subtle: #20171d;
/* Primary button variations */
--bs-primary-hover: #db74ab;
--bs-primary-active: #e07eb4;
/* Body colors */
--bs-body-color: #e0e0e0;
--bs-body-color-rgb: 224, 224, 224;
--bs-body-bg: #1c1c1c;
--bs-body-bg-rgb: 42, 42, 42;
--bs-secondary-color: rgba(224, 224, 224, 0.75);
--bs-secondary-color-rgb: 224, 224, 224;
--bs-tertiary-color: rgba(224, 224, 224, 0.5);
--bs-tertiary-color-rgb: 224, 224, 224;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
/* UI element colors */
--bs-border-color: #444444;
--bs-border-color-translucent: rgba(255, 255, 255, 0.1);
--bs-light-rgb: 50, 50, 50;
--bs-dark-rgb: 34, 34, 34;
--bs-white-rgb: 42, 42, 42;
/* Custom dark theme variables */
--border-color: #444444;
--card-bg: #222;
--card-hover-bg: #444444;
--dropdown-bg: #333333;
--alert-warning-bg: #3d3223;
--alert-warning-border: #594832;
--alert-warning-color: #e0c088;
--alert-warning-hover: #e0c088;
--table-highlight-bg: rgba(61, 41, 54, 0.3);
--text-muted: #aaaaaa;
--source-icon: #5ed2b7;
/* Gradient colors */
--gradient-start: #cd117b;
--gradient-end: #e07eb4;
}
/* Background gradient */
.bg-primary-gradient {
background: linear-gradient(135deg, var(--bs-primary-bg-subtle), #160e13);
}
/* Logo images */
#logoContainer {
background-image: url(../../img/logo-white.svg);
}
#smallLogoContainer {
background-image: url(../../img/logo-white.svg);
}
/* Card and accordion styles */
.card,
.accordion {
background-color: var(--card-bg);
border-color: var(--border-color);
}
.card-body,
.accordion-body {
color: var(--bs-body-color);
}
.accordion-header {
background-color: var(--card-bg);
color: var(--bs-body-color);
border-bottom: none !important;
}
.accordion-item {
border-bottom: var(--border-color) 1px solid !important;
}
.accordion-header:hover {
background-color: var(--card-hover-bg);
}
/* Dropdown styles */
.dropdown-content {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
}
.dropdown-content a {
color: var(--bs-body-color);
}
.dropdown-content a:hover {
background-color: var(--card-hover-bg);
}
/* Table styles */
.transparency-start-balance-row>td {
background-color: var(--table-highlight-bg) !important;
}
.table {
color: var(--bs-body-color);
}
.table-light {
--bs-table-bg: #3a3a3a;
--bs-table-color: var(--bs-body-color);
}
.table-secondary {
--bs-table-bg: #444444;
--bs-table-color: var(--bs-body-color);
}
.table-bordered {
border-color: var(--border-color);
}
/* Button styles */
.btn-primary {
--bs-btn-color: #222222;
--bs-btn-bg: var(--bs-primary);
--bs-btn-border-color: var(--bs-primary);
--bs-btn-hover-color: #222222;
--bs-btn-hover-bg: var(--bs-primary-hover);
--bs-btn-hover-border-color: var(--bs-primary-hover);
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
--bs-btn-active-color: #222222;
--bs-btn-active-bg: var(--bs-primary-active);
--bs-btn-active-border-color: var(--bs-primary-hover);
}
.btn-outline-primary {
--bs-btn-color: var(--bs-primary);
--bs-btn-border-color: var(--bs-primary);
--bs-btn-hover-color: #222222;
--bs-btn-hover-bg: var(--bs-primary);
--bs-btn-hover-border-color: var(--bs-primary);
--bs-btn-active-color: #222222;
--bs-btn-active-bg: var(--bs-primary);
--bs-btn-active-border-color: var(--bs-primary);
}
/* Link styles */
a {
color: var(--bs-primary);
}
a:hover {
color: var(--bs-primary-text-emphasis);
}
/* Text styles */
.text-muted {
color: var(--text-muted) !important;
}
.fancy-text-primary {
background: -webkit-linear-gradient(45deg, var(--gradient-start), var(--gradient-end));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
/* Navbar styles */
.navbar-light {
background-color: var(--card-bg) !important;
}
.navbar-light .navbar-nav .nav-link {
color: var(--bs-body-color);
}
.navbar-light .navbar-nav .nav-link:hover,
.navbar-light .navbar-nav .nav-link.active {
color: var(--bs-primary);
}
/* Fix for navbar brand text color */
.navbar-brand p {
color: var(--bs-body-color) !important;
}
.navbar-brand span[style*="color: rgb(35, 35, 35)"] {
color: var(--bs-body-color) !important;
}
/* Footer styles */
footer.bg-primary-gradient {
color: var(--bs-body-color);
}
footer a {
color: var(--bs-primary);
}
footer a:hover {
color: var(--bs-primary-text-emphasis);
}
footer .text-muted {
color: var(--text-muted) !important;
}
/* Alert styles */
.alert-warning {
background-color: var(--alert-warning-bg);
border-color: var(--alert-warning-border);
color: var(--alert-warning-color);
}
.alert-warning .alert-link {
color: var(--alert-warning-color);
}
/* Icon styles */
.bs-icon.bs-icon-primary svg {
fill: var(--bs-primary) !important;
}
.bs-icon-circle.bs-icon-primary {
background-color: var(--bs-primary-bg-subtle);
}
.bs-icon-circle.bs-icon-primary svg {
fill: var(--bs-primary) !important;
}
/* Fix for service icons */
.bs-icon-sm.bs-icon-circle.homemade svg,
.bs-icon-sm.bs-icon-circle.upstream svg,
.bs-icon-sm.bs-icon-circle.fork svg,
.bs-icon-sm.bs-icon-circle.members-only svg {
fill: var(--source-icon) !important;
}
.bs-icon-sm.bs-icon-circle.homemade,
.bs-icon-sm.bs-icon-circle.upstream,
.bs-icon-sm.bs-icon-circle.fork,
.bs-icon-sm.bs-icon-circle.members-only {
background-color: var(--card-bg) !important;
border: 1px solid var(--source-icon);
}
/* Fix for icon in accordion headers */
.accordion-header .bs-icon svg {
fill: var(--bs-primary) !important;
}
/* Background color overrides */
.bg-light {
background-color: var(--card-bg) !important;
}
.bg-white {
background-color: var(--bs-body-bg) !important;
}
.bg-secondary-subtle {
background-color: #3a3a3a !important;
}
.bg-primary-subtle {
background-color: var(--bs-primary-bg-subtle) !important;
}
/* Theme toggle styles */
.theme-toggle-container {
display: inline-flex;
align-items: center;
}
.theme-toggle-container .btn {
display: flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
padding: 0;
border-radius: 50%;
background-color: transparent;
border: 2px solid var(--bs-primary);
color: var(--bs-primary);
}
.theme-toggle-container .btn:hover {
background-color: var(--bs-primary);
color: #333333;
}
.theme-toggle-container .btn svg {
width: 20px;
height: 20px;
fill: currentColor;
}
/* Fix for any remaining white sections */
section.bg-white {
background-color: var(--bs-body-bg) !important;
}
section.bg-light {
background-color: var(--card-bg) !important;
}
.container.bg-white {
background-color: var(--bs-body-bg) !important;
}
[class*="bg-white"] {
background-color: var(--bs-body-bg) !important;
}
/* Code blocks */
code {
color: var(--bs-primary);
background-color: var(--card-bg);
padding: 0.2em 0.4em;
border-radius: 3px;
}
pre {
background-color: var(--card-bg);
color: var(--bs-body-color);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 1em;
}
/* Input fields */
input,
textarea,
select {
background-color: #3a3a3a !important;
border-color: var(--border-color) !important;
color: var(--bs-body-color) !important;
}
input::placeholder,
textarea::placeholder {
color: #888888 !important;
}
/* Blockquotes */
blockquote {
border-left: 4px solid var(--bs-primary-bg-subtle);
padding-left: 1em;
color: #cccccc;
}
/* Fix for inline styles that might set text to black */
[style*="color: rgb(35, 35, 35)"] {
color: var(--bs-body-color) !important;
}
[style*="color: #232323"] {
color: var(--bs-body-color) !important;
}
/* Make sure the body background is consistently applied */
body {
background-color: var(--bs-body-bg);
}
/* Ensure header bar is lighter */
#mainNav {
background-color: var(--card-bg) !important;
}
/* Legend section icons */
.col .d-flex .bs-icon-md svg {
fill: var(--bs-primary) !important;
}
/* Fix for any SVG icons that might be hard to see */
svg {
fill: currentColor;
}
/* Fix for service icons in the legend */
.services-legend .col .d-flex .bs-icon-md svg {
fill: var(--source-icon) !important;
}
.services-legend .bs-icon-circle.bs-icon-primary {
background-color: var(--card-bg) !important;
border: 1px solid var(--source-icon);
}

View file

@ -1,19 +0,0 @@
/* Disability pride flag theme */
:root {
/* Override gradient colors */
--gradient-start: #4A4A4A;
--gradient-end: #4A4A4A;
}
header.bg-primary-gradient {
background: linear-gradient(to bottom right, #4A4A4A, #FF0000, #FFD700, #D3D3D3, #ADD8E6, #008000, #4A4A4A);
}
#logoContainer {
background-image: url(../../img/logo-white.svg);
}
#smallLogoContainer {
background-image: url(../../img/logo-inv_grad.svg);
}

View file

@ -1,183 +0,0 @@
/* Europe Day Theme */
:root {
--europe-blue: #003399; /* Official EU Blue */
--europe-yellow: #FFCC00; /* Official EU Yellow */
--bs-primary: var(--europe-blue);
--bs-primary-rgb: 0, 51, 153;
--bs-primary-text-emphasis: #001a4d;
--bs-primary-bg-subtle: #e6eaf4;
--bs-primary-border-subtle: #b3c0e8;
--bs-primary-hover: #002a7f;
--bs-primary-active: #002266;
--accent-yellow: var(--europe-yellow);
--bs-btn-color: #ffffff;
--bs-btn-bg: var(--bs-primary);
--bs-btn-border-color: var(--bs-primary);
--bs-btn-hover-color: #ffffff;
--bs-btn-hover-bg: var(--bs-primary-hover);
--bs-btn-hover-border-color: var(--bs-primary-hover);
--bs-btn-active-color: #ffffff;
--bs-btn-active-bg: var(--bs-primary-active);
--bs-btn-active-border-color: var(--bs-primary-active);
--bs-btn-disabled-color: #ffffff;
--bs-btn-disabled-bg: var(--bs-primary);
--bs-btn-disabled-border-color: var(--bs-primary);
--bs-btn-outline-color: var(--accent-yellow);
--bs-btn-border-color-override: var(--accent-yellow);
--bs-btn-hover-bg-override: var(--accent-yellow);
--bs-btn-hover-color-override: var(--europe-blue);
--bs-btn-active-bg-override: #e6b800;
--bs-btn-active-border-color-override: #e6b800;
--bs-btn-disabled-color-override: rgba(255, 204, 0, 0.5);
--bs-btn-disabled-border-color-override: rgba(255, 204, 0, 0.5);
}
.btn-outline-primary {
--bs-btn-color: var(--bs-btn-outline-color);
--bs-btn-border-color: var(--bs-btn-border-color-override);
--bs-btn-hover-color: var(--bs-btn-hover-color-override);
--bs-btn-hover-bg: var(--bs-btn-hover-bg-override);
--bs-btn-hover-border-color: var(--bs-btn-hover-bg-override);
--bs-btn-active-color: var(--bs-btn-hover-color-override);
--bs-btn-active-bg: var(--bs-btn-active-bg-override);
--bs-btn-active-border-color: var(--bs-btn-active-border-color-override);
--bs-btn-disabled-color: var(--bs-btn-disabled-color-override);
--bs-btn-disabled-bg: transparent;
--bs-btn-disabled-border-color: var(--bs-btn-disabled-border-color-override);
--bs-gradient: none;
}
.bg-primary-gradient {
background-color: var(--europe-blue);
color: #e6eaf4;
}
footer.bg-primary-gradient {
background-image: none;
}
footer.bg-primary-gradient a {
color: var(--accent-yellow);
}
footer.bg-primary-gradient a:hover {
color: #ffee99;
}
footer.bg-primary-gradient .text-muted {
color: #b3c0e8 !important;
}
header.bg-primary-gradient {
background-image: url(../../img/eu_flag.svg);
background-color: var(--europe-blue); /* Fallback color */
background-repeat: no-repeat;
background-position: center center;
background-size: cover;
color: #ffffff;
min-height: 600px;
}
header.bg-primary-gradient h2,
header.bg-primary-gradient p {
color: #ffffff;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.7);
}
header.bg-primary-gradient .fancy-text-primary {
color: var(--accent-yellow);
background: none;
-webkit-background-clip: initial;
background-clip: initial;
-webkit-text-fill-color: initial;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8);
}
#logoContainer {
background-image: url(../../img/logo-white.svg);
filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.6));
}
#smallLogoContainer {
background-image: url(../../img/logo-inv_grad.svg);
}
.bs-icon.bs-icon-primary {
background-color: var(--accent-yellow);
color: var(--europe-blue);
}
.bs-icon.bs-icon-primary svg {
fill: var(--europe-blue) !important;
}
.bs-icon-sm.bs-icon-circle.bs-icon-primary {
background-color: var(--accent-yellow);
}
.bs-icon-sm.bs-icon-circle.bs-icon-primary svg {
fill: var(--europe-blue) !important;
}
.homemade svg, .upstream svg, .fork svg, .members-only svg {
fill: var(--europe-blue) !important;
}
.bs-icon-sm.homemade, .bs-icon-sm.upstream, .bs-icon-sm.fork, .bs-icon-sm.members-only {
background-color: var(--accent-yellow) !important;
border: 1px solid var(--europe-blue);
}
.bs-icon.bs-icon-lg svg {
fill: var(--accent-yellow);
}
section a:not(.btn), .card a:not(.btn), .alert a:not(.btn) {
color: var(--europe-blue);
text-decoration: underline;
}
section a:not(.btn):hover, .card a:not(.btn):hover, .alert a:not(.btn):hover {
color: var(--bs-primary-hover);
}
header.bg-primary-gradient a:not(.btn) {
color: var(--accent-yellow);
text-decoration: underline;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
}
header.bg-primary-gradient a:not(.btn):hover {
color: #ffee99;
}
.text-primary {
color: var(--europe-blue) !important;
}
.card.bg-primary-subtle {
background-color: var(--bs-primary-bg-subtle) !important;
border-color: var(--bs-primary-border-subtle) !important;
color: var(--bs-primary-text-emphasis) !important;
}
.card.bg-primary-subtle a {
color: var(--bs-primary-bg-subtle) !important;
font-weight: bold;
}
.alert-primary {
--bs-alert-color: var(--bs-primary-text-emphasis);
--bs-alert-bg: var(--bs-primary-bg-subtle);
--bs-alert-border-color: var(--bs-primary-border-subtle);
--bs-alert-link-color: var(--bs-primary-text-emphasis);
}
.alert-warning {
--bs-alert-bg: #fff9e6;
--bs-alert-border-color: #ffecb3;
--bs-alert-color: #665200;
}
.theme-toggle-container .btn {
border-color: var(--accent-yellow);
color: var(--accent-yellow);
}
.theme-toggle-container .btn:hover {
background-color: var(--accent-yellow);
color: var(--europe-blue);
}
.theme-toggle-container .btn svg {
fill: currentColor;
}
@media (min-width: 1200px) {
.pt-xl-5 {
padding-top: 15rem !important;
padding-bottom: 15rem !important;
}
}

View file

@ -1,9 +0,0 @@
/* Default theme */
#logoContainer {
background-image: url(../../img/logo-inv_grad.svg);
}
#smallLogoContainer {
background-image: url(../../img/logo-inv_grad.svg);
}

View file

@ -1,19 +0,0 @@
/* Pride flag theme */
:root {
/* Override gradient colors */
--gradient-start: #FF7878;
--gradient-end: #CAB8FF;
}
.bg-primary-gradient {
background: linear-gradient(45deg, #FF7878, #FFC898, #FFF89A, #CDF2CA, #A2CDCD, #D1E8E4, #CAB8FF);
}
#logoContainer {
background-image: url(../../img/logo-white.svg);
}
#smallLogoContainer {
background-image: url(../../img/logo-inv_grad.svg);
}

View file

@ -1,19 +0,0 @@
/* Trans pride flag theme */
:root {
/* Override gradient colors */
--gradient-start: #87ddff;
--gradient-end: #F7A8B8;
}
.bg-primary-gradient {
background: linear-gradient(0deg, #87ddff, #F7A8B8, #FFFFFF, #F7A8B8, #87ddff);
}
#logoContainer {
background-image: url(../../img/logo-white.svg);
}
#smallLogoContainer {
background-image: url(../../img/logo-inv_grad.svg);
}

View file

@ -1,31 +0,0 @@
/* 4/20 Theme */
:root {
/* Override gradient colors */
--gradient-start: #fe8d8d;
--gradient-end: hsl(120, 100%, 79%);
}
.bg-primary-gradient {
background: linear-gradient(to bottom,
#fe8d8d 0%,
#fe8d8d 33.33%,
#ffff80 33.33%,
#ffff80 66.66%,
hsl(120, 100%, 79%) 66.66%,
hsl(120, 100%, 79%) 100%);
}
.special-header {
background: white;
-webkit-background-clip: text;
background-clip: text;
}
#logoContainer {
background-image: url(../../img/logo-white.svg);
}
#smallLogoContainer {
background-image: url(../../img/logo-inv_grad.svg);
}

View file

@ -1,72 +0,0 @@
@font-face {
font-family: 'Inconsolata';
font-style: normal;
font-weight: 200;
font-stretch: normal;
font-display: swap;
src: url(./QldgNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLYxYWI2qfdm7LppwU8aRo.ttf) format('truetype');
}
@font-face {
font-family: 'Inconsolata';
font-style: normal;
font-weight: 300;
font-stretch: normal;
font-display: swap;
src: url(./QldgNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLYxYWI2qfdm7Lpp9s8aRo.ttf) format('truetype');
}
@font-face {
font-family: 'Inconsolata';
font-style: normal;
font-weight: 400;
font-stretch: normal;
font-display: swap;
src: url(./QldgNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLYxYWI2qfdm7Lpp4U8aRo.ttf) format('truetype');
}
@font-face {
font-family: 'Inconsolata';
font-style: normal;
font-weight: 500;
font-stretch: normal;
font-display: swap;
src: url(./QldgNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLYxYWI2qfdm7Lpp7c8aRo.ttf) format('truetype');
}
@font-face {
font-family: 'Inconsolata';
font-style: normal;
font-weight: 600;
font-stretch: normal;
font-display: swap;
src: url(./QldgNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLYxYWI2qfdm7Lpp1s7aRo.ttf) format('truetype');
}
@font-face {
font-family: 'Inconsolata';
font-style: normal;
font-weight: 700;
font-stretch: normal;
font-display: swap;
src: url(./QldgNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLYxYWI2qfdm7Lpp2I7aRo.ttf) format('truetype');
}
@font-face {
font-family: 'Inconsolata';
font-style: normal;
font-weight: 800;
font-stretch: normal;
font-display: swap;
src: url(./QldgNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLYxYWI2qfdm7LppwU7aRo.ttf) format('truetype');
}
@font-face {
font-family: 'Inconsolata';
font-style: normal;
font-weight: 900;
font-stretch: normal;
font-display: swap;
src: url(./QldgNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLYxYWI2qfdm7Lppyw7aRo.ttf) format('truetype');
}

View file

@ -1,36 +0,0 @@
Copyright 2006 The Inconsolata Project Authors
This Font Software is licensed under the SIL Open Font License, Version 1.1 . This license is copied below, and is also available with a FAQ at: https://openfontlicense.org
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -1,6 +0,0 @@
Unless otherwise specified, the files in this directory are taken from
[Phosphor Icons](https://phosphoricons.com/) and are licensed under the
MIT License. For details, please see the Phosphor Icons website.
The file `rainbow.svg` was created by us at [Private.coffee](https://private.coffee)
and is also licensed under the MIT License.

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M231.65,194.55,198.46,36.75a16,16,0,0,0-19-12.39L132.65,34.42a16.08,16.08,0,0,0-12.3,19l33.19,157.8A16,16,0,0,0,169.16,224a16.25,16.25,0,0,0,3.38-.36l46.81-10.06A16.09,16.09,0,0,0,231.65,194.55ZM136,50.15c0-.06,0-.09,0-.09l46.8-10,3.33,15.87L139.33,66Zm6.62,31.47,46.82-10.05,3.34,15.9L146,97.53Zm6.64,31.57,46.82-10.06,13.3,63.24-46.82,10.06ZM216,197.94l-46.8,10-3.33-15.87L212.67,182,216,197.85C216,197.91,216,197.94,216,197.94ZM104,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V48A16,16,0,0,0,104,32ZM56,48h48V64H56Zm0,32h48v96H56Zm48,128H56V192h48v16Z"></path></svg>

Before

Width:  |  Height:  |  Size: 700 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M208,56H180.28L166.65,35.56A8,8,0,0,0,160,32H96a8,8,0,0,0-6.65,3.56L75.71,56H48A24,24,0,0,0,24,80V192a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V80A24,24,0,0,0,208,56Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V80a8,8,0,0,1,8-8H80a8,8,0,0,0,6.66-3.56L100.28,48h55.43l13.63,20.44A8,8,0,0,0,176,72h32a8,8,0,0,1,8,8ZM128,88a44,44,0,1,0,44,44A44.05,44.05,0,0,0,128,88Zm0,72a28,28,0,1,1,28-28A28,28,0,0,1,128,160Z"></path></svg>

Before

Width:  |  Height:  |  Size: 523 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48ZM80,192l12-16h72l12,16Zm144,0H196l-21.6-28.8A8,8,0,0,0,168,160H88a8,8,0,0,0-6.4,3.2L60,192H32V64H224V192ZM176,80H80a32,32,0,0,0,0,64h96a32,32,0,0,0,0-64ZM148.3,96a31.92,31.92,0,0,0,0,32H107.7a31.92,31.92,0,0,0,0-32ZM64,112a16,16,0,1,1,16,16A16,16,0,0,1,64,112Zm112,16a16,16,0,1,1,16-16A16,16,0,0,1,176,128Z"></path></svg>

Before

Width:  |  Height:  |  Size: 525 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M69.12,94.15,28.5,128l40.62,33.85a8,8,0,1,1-10.24,12.29l-48-40a8,8,0,0,1,0-12.29l48-40a8,8,0,0,1,10.24,12.3Zm176,27.7-48-40a8,8,0,1,0-10.24,12.3L227.5,128l-40.62,33.85a8,8,0,1,0,10.24,12.29l48-40a8,8,0,0,0,0-12.29ZM162.73,32.48a8,8,0,0,0-10.25,4.79l-64,176a8,8,0,0,0,4.79,10.26A8.14,8.14,0,0,0,96,224a8,8,0,0,0,7.52-5.27l64-176A8,8,0,0,0,162.73,32.48Z"></path></svg>

Before

Width:  |  Height:  |  Size: 475 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M249.66,46.34l-40-40a8,8,0,0,0-11.31,11.32L200.69,20,140.52,80.16C117.73,68.3,92.21,69.29,76.75,84.74a42.27,42.27,0,0,0-9.39,14.37A8.24,8.24,0,0,1,59.81,104c-14.59.49-27.26,5.72-36.65,15.11C11.08,131.22,6,148.6,8.74,168.07,11.4,186.7,21.07,205.15,36,220s33.34,24.56,52,27.22A71.13,71.13,0,0,0,98.1,248c15.32,0,28.83-5.23,38.76-15.16,9.39-9.39,14.62-22.06,15.11-36.65a8.24,8.24,0,0,1,4.92-7.55,42.12,42.12,0,0,0,14.37-9.39c15.45-15.46,16.44-41,4.58-63.77L236,55.31l2.34,2.34a8,8,0,1,0,11.32-11.31ZM160,167.93a26.12,26.12,0,0,1-8.95,5.83,24.24,24.24,0,0,0-15,21.89c-.36,10.46-4,19.41-10.43,25.88-8.44,8.43-21,11.95-35.36,9.89C75,229.25,59.73,221.19,47.27,208.73S26.75,181,24.58,165.81c-2-14.37,1.46-26.92,9.89-35.36C40.94,124,49.89,120.37,60.35,120h0a24.22,24.22,0,0,0,21.89-15,26.12,26.12,0,0,1,5.83-9c5.49-5.49,13-8.13,21.38-8.13a49.38,49.38,0,0,1,19.13,4.19L108.5,112.19a32,32,0,1,0,35.31,35.31l20.08-20.08C170.41,142.71,169.47,158.41,160,167.93Zm-10.4-61.48a72.9,72.9,0,0,1,5.93,6.75l-15.42,15.42a32.22,32.22,0,0,0-12.68-12.68l15.42-15.43A73,73,0,0,1,149.55,106.45ZM112,128a16,16,0,0,1,16,16h0a16,16,0,1,1-16-16Zm48.85-32.85a86.94,86.94,0,0,0-6.68-6L176,67.31,188.69,80l-21.83,21.82A86.94,86.94,0,0,0,160.86,95.14ZM200,68.68,187.32,56,212,31.31,224.69,44ZM93.66,194.33a8,8,0,0,1-11.31,11.32l-32-32a8,8,0,0,1,11.32-11.31Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M224,152V136a96.37,96.37,0,0,0-64-90.51V40a16,16,0,0,0-16-16H112A16,16,0,0,0,96,40v5.49A96.37,96.37,0,0,0,32,136v16a16,16,0,0,0-16,16v24a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V168A16,16,0,0,0,224,152Zm-16-16v16H160V62.67A80.36,80.36,0,0,1,208,136ZM144,40V152H112V40ZM48,136A80.36,80.36,0,0,1,96,62.67V152H48Zm176,56H32V168H224v24Z"></path></svg>

Before

Width:  |  Height:  |  Size: 455 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M128,112a28,28,0,0,0-8,54.83V184a8,8,0,0,0,16,0V166.83A28,28,0,0,0,128,112Zm0,40a12,12,0,1,1,12-12A12,12,0,0,1,128,152Zm80-72H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80ZM96,56a32,32,0,0,1,64,0V80H96ZM208,208H48V96H208V208Z"></path></svg>

Before

Width:  |  Height:  |  Size: 417 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M228.92,49.69a8,8,0,0,0-6.86-1.45L160.93,63.52,99.58,32.84a8,8,0,0,0-5.52-.6l-64,16A8,8,0,0,0,24,56V200a8,8,0,0,0,9.94,7.76l61.13-15.28,61.35,30.68A8.15,8.15,0,0,0,160,224a8,8,0,0,0,1.94-.24l64-16A8,8,0,0,0,232,200V56A8,8,0,0,0,228.92,49.69ZM104,52.94l48,24V203.06l-48-24ZM40,62.25l48-12v127.5l-48,12Zm176,131.5-48,12V78.25l48-12Z"></path></svg>

Before

Width:  |  Height:  |  Size: 454 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M244.24,60a8,8,0,0,0-7.75-.4c-42.93,21-73.59,11.16-106,.78-34-10.89-69.25-22.14-117.95,1.64A8,8,0,0,0,8,69.24V189.17a8,8,0,0,0,11.51,7.19c42.93-21,73.59-11.16,106.05-.78,19.24,6.15,38.84,12.42,61,12.42,17.09,0,35.73-3.72,56.91-14.06a8,8,0,0,0,4.49-7.18V66.83A8,8,0,0,0,244.24,60ZM232,181.67c-40.6,18.17-70.25,8.69-101.56-1.32-19.24-6.15-38.84-12.42-61-12.42a122,122,0,0,0-45.4,9V74.33c40.6-18.17,70.25-8.69,101.56,1.32S189.14,96,232,79.09ZM128,96a32,32,0,1,0,32,32A32,32,0,0,0,128,96Zm0,48a16,16,0,1,1,16-16A16,16,0,0,1,128,144ZM56,96v48a8,8,0,0,1-16,0V96a8,8,0,1,1,16,0Zm144,64V112a8,8,0,1,1,16,0v48a8,8,0,1,1-16,0Z"></path></svg>

Before

Width:  |  Height:  |  Size: 740 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M233.54,142.23a8,8,0,0,0-8-2,88.08,88.08,0,0,1-109.8-109.8,8,8,0,0,0-10-10,104.84,104.84,0,0,0-52.91,37A104,104,0,0,0,136,224a103.09,103.09,0,0,0,62.52-20.88,104.84,104.84,0,0,0,37-52.91A8,8,0,0,0,233.54,142.23ZM188.9,190.34A88,88,0,0,1,65.66,67.11a89,89,0,0,1,31.4-26A106,106,0,0,0,96,56,104.11,104.11,0,0,0,200,160a106,106,0,0,0,14.92-1.06A89,89,0,0,1,188.9,190.34Z"></path></svg>

Before

Width:  |  Height:  |  Size: 491 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M104,40H56A16,16,0,0,0,40,56v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V56A16,16,0,0,0,104,40Zm0,64H56V56h48v48Zm0,32H56a16,16,0,0,0-16,16v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V152A16,16,0,0,0,104,136Zm0,64H56V152h48v48ZM200,40H152a16,16,0,0,0-16,16v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V56A16,16,0,0,0,200,40Zm0,64H152V56h48v48Zm-64,72V144a8,8,0,0,1,16,0v32a8,8,0,0,1-16,0Zm80-16a8,8,0,0,1-8,8H184v40a8,8,0,0,1-8,8H144a8,8,0,0,1,0-16h24V144a8,8,0,0,1,16,0v8h24A8,8,0,0,1,216,160Zm0,32v16a8,8,0,0,1-16,0V192a8,8,0,0,1,16,0Z"></path></svg>

Before

Width:  |  Height:  |  Size: 654 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M140,180a12,12,0,1,1-12-12A12,12,0,0,1,140,180ZM128,72c-22.06,0-40,16.15-40,36v4a8,8,0,0,0,16,0v-4c0-11,10.77-20,24-20s24,9,24,20-10.77,20-24,20a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-.72c18.24-3.35,32-17.9,32-35.28C168,88.15,150.06,72,128,72Zm104,56A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"></path></svg>

Before

Width:  |  Height:  |  Size: 466 B

View file

@ -1,16 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="135" height="20" role="img" aria-label="LGBTIQ+"> <svg xmlns="http://www.w3.org/2000/svg" width="94" height="20" role="img" aria-label="LGBTIQ+">
<!-- This file was created as part of the Private.coffee project
It is licensed under the MIT license
For more information, please visit https://private.coffee -->
<title>LGBTIQ+</title> <title>LGBTIQ+</title>
<rect rx="3" width="135" height="20" fill="#555"></rect> <rect rx="3" width="94" height="20" fill="#555"/>
<rect x="53" width="82" height="20" fill="#e05d44"></rect> <rect x="37" width="57" height="20" fill="#e05d44"/>
<rect x="66.5" width="68.5" height="20" fill="#fecc00"></rect> <rect x="46.5" width="47.5" height="20" fill="#fecc00"/>
<rect x="80" width="55" height="20" fill="#61c354"></rect> <rect x="56" width="37" height="20" fill="#61c354"/>
<rect x="93.5" width="41.5" height="20" fill="#007ec6"></rect> <rect x="65.5" width="27" height="20" fill="#007ec6"/>
<rect x="107" width="28" height="20" fill="#744ca1"></rect> <rect x="75" width="17" height="20" fill="#744ca1"/>
<rect x="120.5" width="14.5" height="20" rx="3" ry="3" fill="#8b00ff"></rect> <rect x="84.5" width="9.5" height="20" rx="3" ry="3" fill="#8b00ff"/>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="8"> <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="8">
<text x="27" y="15" fill="#fff">LGBTIQ+</text> <text x="18.5" y="15" fill="#fff">LGBTIQ+</text>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 956 B

After

Width:  |  Height:  |  Size: 757 B

Before After
Before After

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M200,48H136V16a8,8,0,0,0-16,0V48H56A32,32,0,0,0,24,80V192a32,32,0,0,0,32,32H200a32,32,0,0,0,32-32V80A32,32,0,0,0,200,48Zm16,144a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V80A16,16,0,0,1,56,64H200a16,16,0,0,1,16,16Zm-52-56H92a28,28,0,0,0,0,56h72a28,28,0,0,0,0-56Zm-24,16v24H116V152ZM80,164a12,12,0,0,1,12-12h8v24H92A12,12,0,0,1,80,164Zm84,12h-8V152h8a12,12,0,0,1,0,24ZM72,108a12,12,0,1,1,12,12A12,12,0,0,1,72,108Zm88,0a12,12,0,1,1,12,12A12,12,0,0,1,160,108Z"></path></svg>

Before

Width:  |  Height:  |  Size: 576 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M106.91,149.09A71.53,71.53,0,0,1,128,200a8,8,0,0,1-16,0,56,56,0,0,0-56-56,8,8,0,0,1,0-16A71.53,71.53,0,0,1,106.91,149.09ZM56,80a8,8,0,0,0,0,16A104,104,0,0,1,160,200a8,8,0,0,0,16,0A120,120,0,0,0,56,80Zm118.79,1.21A166.9,166.9,0,0,0,56,32a8,8,0,0,0,0,16A151,151,0,0,1,163.48,92.52,151,151,0,0,1,208,200a8,8,0,0,0,16,0A166.9,166.9,0,0,0,174.79,81.21ZM60,184a12,12,0,1,0,12,12A12,12,0,0,0,60,184Z"></path></svg>

Before

Width:  |  Height:  |  Size: 516 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M239.18,97.26A16.38,16.38,0,0,0,224.92,86l-59-4.76L143.14,26.15a16.36,16.36,0,0,0-30.27,0L90.11,81.23,31.08,86a16.46,16.46,0,0,0-9.37,28.86l45,38.83L53,211.75a16.38,16.38,0,0,0,24.5,17.82L128,198.49l50.53,31.08A16.4,16.4,0,0,0,203,211.75l-13.76-58.07,45-38.83A16.43,16.43,0,0,0,239.18,97.26Zm-15.34,5.47-48.7,42a8,8,0,0,0-2.56,7.91l14.88,62.8a.37.37,0,0,1-.17.48c-.18.14-.23.11-.38,0l-54.72-33.65a8,8,0,0,0-8.38,0L69.09,215.94c-.15.09-.19.12-.38,0a.37.37,0,0,1-.17-.48l14.88-62.8a8,8,0,0,0-2.56-7.91l-48.7-42c-.12-.1-.23-.19-.13-.5s.18-.27.33-.29l63.92-5.16A8,8,0,0,0,103,91.86l24.62-59.61c.08-.17.11-.25.35-.25s.27.08.35.25L153,91.86a8,8,0,0,0,6.75,4.92l63.92,5.16c.15,0,.24,0,.33.29S224,102.63,223.84,102.73Z"></path></svg>

Before

Width:  |  Height:  |  Size: 834 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M120,40V16a8,8,0,0,1,16,0V40a8,8,0,0,1-16,0Zm72,88a64,64,0,1,1-64-64A64.07,64.07,0,0,1,192,128Zm-16,0a48,48,0,1,0-48,48A48.05,48.05,0,0,0,176,128ZM58.34,69.66A8,8,0,0,0,69.66,58.34l-16-16A8,8,0,0,0,42.34,53.66Zm0,116.68-16,16a8,8,0,0,0,11.32,11.32l16-16a8,8,0,0,0-11.32-11.32ZM192,72a8,8,0,0,0,5.66-2.34l16-16a8,8,0,0,0-11.32-11.32l-16,16A8,8,0,0,0,192,72Zm5.66,114.34a8,8,0,0,0-11.32,11.32l16,16a8,8,0,0,0,11.32-11.32ZM48,128a8,8,0,0,0-8-8H16a8,8,0,0,0,0,16H40A8,8,0,0,0,48,128Zm80,80a8,8,0,0,0-8,8v24a8,8,0,0,0,16,0V216A8,8,0,0,0,128,208Zm112-88H216a8,8,0,0,0,0,16h24a8,8,0,0,0,0-16Z"></path></svg>

Before

Width:  |  Height:  |  Size: 709 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M184,24H72A32,32,0,0,0,40,56V184a32,32,0,0,0,32,32h8L65.6,235.2a8,8,0,1,0,12.8,9.6L100,216h56l21.6,28.8a8,8,0,1,0,12.8-9.6L176,216h8a32,32,0,0,0,32-32V56A32,32,0,0,0,184,24ZM56,120V80h64v40Zm80-40h64v40H136ZM72,40H184a16,16,0,0,1,16,16v8H56V56A16,16,0,0,1,72,40ZM184,200H72a16,16,0,0,1-16-16V136H200v48A16,16,0,0,1,184,200ZM96,172a12,12,0,1,1-12-12A12,12,0,0,1,96,172Zm88,0a12,12,0,1,1-12-12A12,12,0,0,1,184,172Z"></path></svg>

Before

Width:  |  Height:  |  Size: 536 B

View file

@ -1,27 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 900 600" width="900" height="600">
<defs>
<polygon id="star" points="0,-100 29.389,-30.902 95.106,-30.902 42.863,19.098 58.779,80.902 0,40 -58.779,80.902 -42.863,19.098 -95.106,-30.902 -29.389,-30.902" fill="#FFCC00"/>
</defs>
<!-- Blue background rectangle -->
<rect width="900" height="600" fill="#003399"/>
<!-- Group for stars, centered and scaled -->
<g transform="translate(450, 300) scale(0.3)">
<use xlink:href="#star" transform="translate(0, -700)"/>
<use xlink:href="#star" transform="translate(350, -606.21778)"/>
<use xlink:href="#star" transform="translate(606.21778, -350)"/>
<use xlink:href="#star" transform="translate(700, 0)"/>
<use xlink:href="#star" transform="translate(606.21778, 350)"/>
<use xlink:href="#star" transform="translate(350, 606.21778)"/>
<use xlink:href="#star" transform="translate(0, 700)"/>
<use xlink:href="#star" transform="translate(-350, 606.21778)"/>
<use xlink:href="#star" transform="translate(-606.21778, 350)"/>
<use xlink:href="#star" transform="translate(-700, 0)"/>
<use xlink:href="#star" transform="translate(-606.21778, -350)"/>
<use xlink:href="#star" transform="translate(-350, -606.21778)"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,74 +0,0 @@
document.addEventListener('DOMContentLoaded', function() {
// Get the theme toggle button
const themeToggleBtn = document.querySelector('.theme-toggle-btn');
if (!themeToggleBtn) return; // Exit if button not found
const lightIcon = themeToggleBtn.querySelector('.light-icon');
const darkIcon = themeToggleBtn.querySelector('.dark-icon');
const themeStylesheet = document.getElementById('theme-stylesheet');
const relativePath = themeStylesheet.href.substring(0, themeStylesheet.href.lastIndexOf('/') + 1);
const timestamp = themeStylesheet.href.split('?v=')[1] || '';
// Function to toggle theme
function toggleTheme(e) {
e.preventDefault(); // Prevent the default link behavior
const currentTheme = document.documentElement.getAttribute('data-bs-theme') || 'light';
const newTheme = (currentTheme === 'dark') ? 'plain' : 'dark';
// Update HTML attribute
document.documentElement.setAttribute('data-bs-theme', newTheme);
// Update the theme stylesheet
themeStylesheet.href = relativePath + newTheme + '.css?v=' + timestamp;
// Update button icon visibility and aria-label
if (newTheme === 'dark') {
lightIcon.style.display = '';
darkIcon.style.display = 'none';
themeToggleBtn.setAttribute('aria-label', 'Switch to light theme');
themeToggleBtn.href = 'https://private.coffee' + window.location.pathname;
} else {
lightIcon.style.display = 'none';
darkIcon.style.display = '';
themeToggleBtn.setAttribute('aria-label', 'Switch to dark theme');
themeToggleBtn.href = 'https://dark.private.coffee' + window.location.pathname;
}
// Store preference in localStorage
localStorage.setItem('privateCoffeeTheme', newTheme);
}
// Add click event listener to toggle theme
themeToggleBtn.addEventListener('click', toggleTheme);
// Check for saved theme preference and apply it
const savedTheme = localStorage.getItem('privateCoffeeTheme');
if (savedTheme) {
// Get the current theme from the stylesheet filename
const currentThemeMatch = themeStylesheet.href.match(/\/theme\/([^.]+)\.css/);
const currentTheme = currentThemeMatch ? currentThemeMatch[1] : 'plain';
// Only apply the saved theme if it's different from the current theme
if (savedTheme !== currentTheme) {
document.documentElement.setAttribute('data-bs-theme', savedTheme);
// Update the theme stylesheet
themeStylesheet.href = relativePath + savedTheme + '.css?v=' + timestamp;
// Update button icon visibility and aria-label based on saved theme
if (savedTheme === 'dark') {
lightIcon.style.display = '';
darkIcon.style.display = 'none';
themeToggleBtn.setAttribute('aria-label', 'Switch to light theme');
themeToggleBtn.href = 'https://private.coffee' + window.location.pathname;
} else {
lightIcon.style.display = 'none';
darkIcon.style.display = '';
themeToggleBtn.setAttribute('aria-label', 'Switch to dark theme');
themeToggleBtn.href = 'https://dark.private.coffee' + window.location.pathname;
}
}
}
});

View file

@ -1,14 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEZrBjKxYJKwYBBAHaRw8BAQdAi0eeVvyyhWeX1onfGUav7Mz3r8NNAlJxEvDx
22OvTGG0L1ByaXZhdGUuY29mZmVlIFN1cHBvcnQgPHN1cHBvcnRAcHJpdmF0ZS5j
b2ZmZWU+iJkEExYIAEEWIQTyYs8i1UDNu5DXqCul9eeqMhlB+gUCZrBjKwIbAwUJ
BaOagAULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRCl9eeqMhlB+rUcAQCf
BIl1FBPsA07rzhdzBbxZkuj3UvXm+BqEC1yLb1w22gD/d4ikXrxKVhzKKYIKQCG9
hKCu8tRRLnFkU2BnPi5YxwG4OARmsGMrEgorBgEEAZdVAQUBAQdADbyWdMIUSmt8
yEXInauT0MrEU4rApdfJWkOf8Ob8oSoDAQgHiH4EGBYIACYWIQTyYs8i1UDNu5DX
qCul9eeqMhlB+gUCZrBjKwIbDAUJBaOagAAKCRCl9eeqMhlB+rI9AQCfR5urqni2
s5pHUk5Zm1MFn5WxJpSqpw7jPJU5V4QmPgD/XzZYfWL/oLd3gKSKhxsZe5TtNWeb
qX23VXJGJXvhmQU=
=3YnV
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -1,112 +0,0 @@
---
title: Board Meeting 24/03/2024
date: 2024-03-24 23:59:59
author: jupfi
license: CC BY-SA 4.0
license_url: https://creativecommons.org/licenses/by-sa/4.0/
tags: board-meeting
excerpt: Minutes of the board meeting on March 24th, 2024.
---
_Disclaimer: This report was retrospectively translated and formatted into Markdown solely for publication purposes._
# Board Meeting 24/03/2024
## Attendance
- kumi
- jupfi
The quorum is thereby established.
## Agenda Points
### 1. **Account Management for private.coffee**
a. **Opening a Bank Account**:
- Scheduled for 25/03/2024 at Steiermärkische Sparkasse.
- kumi and jupfi will attend the appointment.
- One card will be issued for the board's use, with jupfi's name on the card.
b. **PayPal Account**:
- Initially, private.coffee will proceed without a PayPal account.
- If necessary, a PayPal account can be opened later.
- _Unanimously approved._
c. **Cryptocurrency Donations**:
- Plan technical aspects (by kumi) and bookkeeping aspects (by jupfi).
- _Unanimously approved._
d. **Membership Payments via Direct Debit**:
- Inquiry to the banking advisor will take place tomorrow.
- _Unanimously approved._
e. **SumUp Device**:
- Considering acquiring a SumUp device with no monthly costs and 2.75% transaction fees.
- A device is already owned by Kumi Systems. European alternatives will also be evaluated.
### 2. **Server and Domain Costs**
- Kumi is authorized to spend up to EUR 300 per month without prior approval.
- _Unanimously approved._
### 3. **Bookkeeping Software**
a. **GNUcash Setup**:
- GNUcash will be implemented for bookkeeping purposes.
- _Unanimously approved._
b. **Data Storage**:
- Data will be stored in a GNUcash-compatible SQL database.
- _Unanimously approved._
### 4. **Sponsoring a Formula 1 Team**
- The board discussed sponsoring the Williams F1 team. Inquiry by kumi.
- Kumi supported the idea, jupfi opposed it.
- **Proposal rejected.**
### 5. **Legal Notice and Policy Updates**
- Update legal notice with association details: [https://private.coffee/legal.html](https://private.coffee/legal.html).
- Privacy policy update: [https://private.coffee/privacy.html](https://private.coffee/privacy.html).
- Kumi will draft the updated privacy policy using Overleaf.
- Terms of Service update: [https://private.coffee/terms.html](https://private.coffee/terms.html).
- Add “Join Verein” and “Donate” pages to the private.coffee website.
- _Unanimously approved._
### 6. **E-Government Representative**
- Kumi will be designated as the E-Government representative.
- _Unanimously approved._
### 7. **Tax Office Registration**
- jupfi will handle the tax office registration for the club.
### 8. **Email and Support Requests**
- No email or support requests have been received so far.
### 9. **DMARC Reports**
- DMARC reports will now automatically be set to “Closed” in the ticket system.
### 10. **Membership Fees**
- A discussion was held about ordinary and extraordinary membership fees.
- Initial idea: Ask members how much they would like to contribute.
- Decision on fixed rates (monthly/annual payment) is postponed to after member consultation.
### 11. **private.coffee Shared CryptPad Folder**
- Create a shared CryptPad folder for private.coffee.
- _Unanimously approved._
### 12. **Budget for Stickers and Mugs**
- A budget of EUR 200 is allocated for stickers and mugs.
- _Unanimously approved._
### 13. **Club Insurances**
- Inquiry already made with Generali for liability and legal protection insurance.
- Additional inquiry to Zurich will follow in the future.

View file

@ -1,66 +0,0 @@
---
title: Board Meeting 16/06/2024
date: 2024-06-16 23:59:59
author: jupfi
license: CC BY-SA 4.0
license_url: https://creativecommons.org/licenses/by-sa/4.0/
tags: board-meeting
excerpt: Minutes of the board meeting on June 16th, 2024.
---
_Disclaimer: This report was retrospectively translated and formatted into Markdown solely for publication purposes._
# Board Meeting 16/06/2024
## Attendance
- kumi
- jupfi
The quorum is thus established.
## Agenda Points
### 1. **New Supporting Member**
- The board approves the new supporting member's membership application.
### 2. **Update on Technical Matters**
- **Database Server**:
- A database server is now operational for various services.
- **Backup Solution**:
- The Matrix database is too large for MinIO. It's estimated that 3TB of storage is realistically needed.
- Backup storage budget: ~EUR 25/month.
- Total monthly budget now increased to EUR 325.
- **Budget Exceedance in May**:
- Due to the new database server, the monthly budget was exceeded by EUR 130.04 in May.
- This exceedance is retrospectively approved by the board.
### 3. **New Tasks**
- Website update.
- Membership form creation.
- Transparency report preparation.
### 4. **New Services**
- **transfer.coffee**: File sharing service.
- **MyIP.coffee**: Public IP address lookup.
- **HedgeDoc**: Collaborative Markdown editor.
- **<https://pcof.fi/>**: URL shortener.
### 5. **Registrar Status**
- The organization is now an official registrar for **.fi TLDs**.
### 6. **ID Austria Hackathon**
- Financial support of up to EUR 200 for food and beverages.
- Likely location: Vienna.
- Tentative timeline: Late summer.
- Further details to be discussed.
### 7. **Protocol Management**
- Protocols will be uploaded as PDFs to the Git repository.

View file

@ -1,26 +0,0 @@
---
title: Board Meeting 30/06/2024
date: 2024-06-30 23:59:59
author: jupfi
license: CC BY-SA 4.0
license_url: https://creativecommons.org/licenses/by-sa/4.0/
tags: board-meeting
excerpt: Minutes of the board meeting on June 30th, 2024.
---
_Disclaimer: This report was retrospectively translated and formatted into Markdown solely for publication purposes._
# Board Meeting 30/06/2024
## Attendance
- kumi
- jupfi
The quorum is thus established.
## Agenda Points
1. **New Supporting Member**
- The board approves the membership application of a new supporting member.
- A confirmation email has been sent out.

View file

@ -1,51 +0,0 @@
---
title: Board Meeting 27/10/2024
date: 2024-10-27 23:59:59
author: jupfi
license: CC BY-SA 4.0
license_url: https://creativecommons.org/licenses/by-sa/4.0/
tags: board-meeting
excerpt: Minutes of the board meeting on October 27th, 2024.
---
_Disclaimer: This report was retrospectively translated and formatted into Markdown solely for publication purposes._
# Board Meeting 27/10/2024
## Attendance
- kumi
- jupfi
- ogalbnafets
The quorum is thus established.
## Agenda Points
1. **Christmas Celebration: General Assembly**
- **Location**: Vienna
- **Date**: February (Semester Break)
- **Budget**: EUR 250 (including food and drinks)
- _Unanimously approved_
2. **Liability Insurance (Generali)**
- Does not sufficiently cover potential damage cases.
- Currently not required.
- _Unanimously approved_
- Inquiry about legal protection with Generali (jupfi).
3. **Admission of Supporting Members without Board Meeting**
- Admission by an individual board member is allowed.
- _Unanimously approved_
- Admission of current supporting members.
- _Unanimously approved_
4. **Game Hosting**
- Conduct a survey in the Matrix chat.
5. **Sticker Design**
- Design files will be made available in the Git repository for all members.
6. **Private Coffee Merchandise**
- Spreadshirt shop for mugs and shirts.
- Pre-produced mugs for members and stickers for everyone.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

View file

@ -1,14 +0,0 @@
---
title: Test Post
date: 2049-11-27 09:00:00
image: image.png
author: Kumi
author_url: https://kumi.website
license: CC BY-SA 4.0
license_url: https://creativecommons.org/licenses/by-sa/4.0/
tags: test
excerpt: This is the excerpt of a test post. It is displayed on the index pages.
---
This is a test post.
You can embed images: ![Duck image](image.png)

View file

@ -1,34 +0,0 @@
---
title: Board Meeting 30/11/2024
date: 2024-11-30 23:59:59
author: jupfi
license: CC BY-SA 4.0
license_url: https://creativecommons.org/licenses/by-sa/4.0/
tags: board-meeting
excerpt: Minutes of the board meeting on November 30th, 2024.
---
_Disclaimer: This report was retrospectively translated and formatted into Markdown solely for publication purposes._
# Board Meeting 30/11/2024
## Attendance
- kumi
- jupfi
The quorum is thus established.
## Agenda Points
1. **New Member Admission**
- Admission of new members will only take place after the membership fee (either the first monthly payment or annual payment, depending on the member's preference) has been paid.
- _Unanimously approved._
2. **Expense for Custom Mugs**
- EUR 75.80 was spent on 10 mugs with private.coffee branding.
3. **General Assembly Date and Location**
- Date: February 22nd
- Location: Vienna, _Die Antwort_ (Office headquarters).
- _Unanimously approved._

View file

@ -1,75 +0,0 @@
---
title: Using the private.coffee Matrix homeserver
date: 2025-12-28 13:00:00
#image: image.png
author: jupfi
#author_url: https://kumi.website
license: CC BY-SA 4.0
license_url: https://creativecommons.org/licenses/by-sa/4.0/
excerpt: This blog post will give you a short overview of Matrix in general and our Private.coffee homeserver!
---
## Private.coffee Matrix homeserver
Hi, this blog post will give you a short overview of our Matrix in general and our private.coffee homeserver!
Feel free to jump to the section you are interested in.
## Table of contents
- [Why Matrix?](#why-matrix)
- [Server Information](#server-information)
- [Registration](#registration)
- [Why did we move to conditional registration?](#why-did-we-move-to-conditional-registration)
## Why Matrix?
First of all we want to give a short overview of what Matrix is and why we chose it.
### Decentralized communication
Matrix is an open standard for communication over the internet. It is also a decentralized platform meaning there isn't a single corporation that controls it (like WhatsApp or Telegram), but different servers can be hosted by different organizations and those servers can communicate with each other. This means that you can choose the server that you trust and still communicate with people on other servers. In that regard it is similar to email, but with a focus on real-time communication.
### End-to-end encryption
You can use end-to-end encyption to
### Features
You can use Matrix for a variety of different things:
- Instant Messaging
- Voice and Video Calls
- File Sharing
- Group Chats
- Bots
- Bridges to other platforms (e.g. Telegram, WhatsApp, IRC, Discord)
## Clients
To use Matrix you need a client.
## Server Information
You can find some more information about our homeserver on the following pages:
- [Terms & Privacy](https://matrix.private.coffee/_matrix/consent?v=1.0)
- [Private Coffee Terms](https://private.coffee/terms.html)
## Registration
We provide a Matrix homeserver with conditional registration. This means you can create an account via our [online form](), but you will have to provide the following:
- A valid email address
- A short description of why you want to join our homeserver and what you expect from it
## Why did we move to conditional registration?
Short answer: Nazis, bots and spammers. However we noticed that moving to a conditional registration actually made the server feel more like a community and people feel safer.
Maybe that is how the internet ought to be, a place of community where people can feel safe and not be harassed easily.
You can still stay anonymous on our server and we respect your privacy.
## The private.coffee association
## References and further reading
[Wikipedia: Matrix](https://en.wikipedia.org/wiki/Matrix_(protocol))

View file

@ -1,23 +0,0 @@
---
title: Board Meeting 01/01/2025
date: 2025-01-01 17:00:00
author: jupfi
license: CC BY-SA 4.0
license_url: https://creativecommons.org/licenses/by-sa/4.0/
tags: board-meeting
excerpt: Minutes of the board meeting on January 1st, 2025.
---
# Board Meeting 01/01/2025
## Attendance
- kumi
- jupfi
The quorum is thus established.
## Agenda Points
1. **Change of Date for General Assembly**
a. Date: February 1st 2025 (Semester Break), Location: Vienna

View file

@ -1,17 +0,0 @@
# Mitgliedsbeiträge
Nach § 10 Z 6 der Statuten obliegt die Festsetzung der Mitgliedsbeiträge der Generalversammlung.
Die Mitgliedsbeiträge werden in einer Mindesthöhe festgesetzt, einzelne Mitglieder können jedoch höhere Beiträge leisten.
Die Zahlung kann entweder monatlich oder jährlich im Voraus erfolgen.
Die Mindestbeiträge werden wie folgt festgesetzt:
| Mitgliedskategorie | monatlich | jährlich |
| -------------------------------------------------- | --------- | -------- |
| Außerordentliche Mitglieder (natürliche Personen) | 5 € | 50 € |
| Außerordentliche Mitglieder (juristische Personen) | 100 € | 1000 € |
| Ordentliche Mitglieder | 10 € | 100 € |
Der Vorstand ist berechtigt, in Einzelfällen die Beiträge zu erlassen oder zu reduzieren.

View file

@ -1,29 +0,0 @@
# Funktionsperioden
Aktuell sind die Funktionsperioden der Organe auf "ein Jahr" festgelegt. Grundsätzlich sehen die Statuten zwar vor, dass damit der Zeitraum zwischen zwei Generalversammlungen gemeint ist, allerdings macht das Probleme in der Praxis, weil "falsche" Werte im Vereinsregister eingetragen werden.
Daher wird vorgeschlagen, die Funktionsperioden auf 18 Monate festzulegen, aber bei Neuwahl enden zu lassen.
## Statutenänderung
Die Statutenänderung wird wie folgt vorgeschlagen:
- § 11 Z 4 wird wie folgt geändert:
> Die Funktionsperiode des Vorstandes beträgt 18 Monate. Sie endet jedoch jedenfalls mit der Wahl eines neuen Vorstandes. Wiederwahl ist möglich. Jede Funktion im Vorstand ist persönlich auszuüben.
- § 14 Z 1 S 2 wird wie folgt geändert:
> In Bezug auf Funktionsperioden und Wiederwahl gelten die Bestimmungen des § 11 Z 4 sinngemäß.
- § 15 Z 2 wird wie folgt geändert:
> In Bezug auf Funktionsperioden und Wiederwahl gelten die Bestimmungen des § 11 Z 4 sinngemäß.
- In § 15 wird folgender Absatz als Z 3 eingefügt:
> Das Schiedsgericht wählt aus seiner Mitte einen Vorsitz. Werden weniger als drei Mitglieder von der Generalversammlung gewählt, so ist das Schiedsgericht handlungsunfähig und kann nicht angerufen werden.
Die nachfolgenden Ziffern werden entsprechend angepasst.
- § 20 Z 2 wird ersatzlos gestrichen.

View file

@ -1,176 +0,0 @@
---
title: General Assembly 01/02/2025
date: 2025-02-02 12:00:00
author: jupfi
license: CC BY-SA 4.0
license_url: https://creativecommons.org/licenses/by-sa/4.0/
tags: general-assembly
excerpt: Blog post about the general assembly on February 1st, 2025 in Vienna.
---
## Table of contents
- [TLDR:](#tldr)
- [Introduction](#introduction)
- [Attendance](#attendance)
- [Financial Recap and Goals for 2025](#financial-recap-and-goals-for-2025)
- [Auditing](#auditing)
- [Statue Amendments](#statue-amendments)
- [Change of Function Periods](#change-of-function-periods)
- [Change of Membership Fees](#change-of-membership-fees)
- [Election of the Board & Auditors](#election-of-the-board--auditors)
- [Open Discussion](#open-discussion)
- [Emergency Strategy](#emergency-strategy)
- [Services](#services)
- [LLM Chat Bot](#llm-chat-bot)
- ["Coffee Machine" Gaming Server](#coffee-machine-gaming-server)
- [Quackscape](#quackscape)
- [Mechandise](#mechandise)
- [Hackathon](#hackathon)
- [Collaborations](#collaborations)
- [Blog Posts](#blog-posts)
- [Conclusion](#conclusion)
- [Documents](#documents)
## <a name="tldr"></a>TLDR:
- The general assembly was held on February 1st, 2025 in Vienna.
- The financial recap of 2024 was presented and the budget for 2025 was discussed.
- The audit was carried out and no objections were found.
- The function periods of the board members were extended to 18 months.
- Minimum membership fees were introduced.
- The board and auditors were elected.
- We discussed emergency strategies, services, merchandise, a hackathon, collaborations, and blog posts.
- We will host a hackathon on April 19th in Vienna.
## <a name="introduction"></a>Introduction
It's been about a year since private.coffee is a registered association in Austria, so it was time for our first general assembly.
We held our general assembly on February 1st, 2025 in Vienna. After some introductory rounds of pinball and coffee in the office of [DIE ANTWORT](https://die-antwort.eu/) we were ready to start.
<img src="pictures/pinball.jpg" alt="ogalbnafets plays a round of pinball before the general assembly" style="max-width: 50%;">
*ogalbnafets plays a round of pinball before the general assembly*
## <a name="attendance"></a>Attendance
- kumi
- jupfi
- ogalbnafets
- noniq
Location: Die Antwort, Obere Weißgerberstraße 4, 1030 Vienna, Austria.
## <a name="financial-recap-and-goals-for-2025"></a>Financial Recap and Goals for 2025
The general assembly started with some words from the chairperson kumi and the treasurer jupfi presented a recap of the financial year 2024.
<img src="pictures/2024_financial_year.png" alt="The 2024 financial year. Expenses were lower from March to May and improved due to a stronger database server which was added in summer. The expenses stayed quite constant over the last quarter of the year. Income due to memberships increased throughout the year." style="max-width: 75%;">
*The 2024 financial year. Expenses were lower from March to May and increased due to a stronger database server which was added in summer. The expenses stayed quite constant over the last quarter of the year. Income due to memberships increased throughout the year.*
---
After the recap of 2024 we continued with budget goals for 2025, which mainly included reducing the dependence on corporate memberships and increasing the number of individual members.
Another goal for 2025 will be to have a small emergency fund to cover unexpected costs.
<img src="pictures/2025_financial_budget.png" alt="The budget for 2025." style="max-width: 75%;">
*The budget for 2025.The main goal is to decrease the dependence on corporate members while having constant expenses.*
## <a name="auditing"></a>Auditing
Auditor ogalbnafets reported that the audit was carried out and no objections were found. He recommended that the actions of the Board be approved.
## <a name="statute-amendments"></a>Statute Amendments
Some changes to the statutes were necessary to clarify some points and reduce some administrative inconveniences.
### <a name="change-of-function-periods"></a>Change of Function Periods
To have an easier time with handling bank accounts, we extended the function periods of the board members to **18 months**. This change was unanimously accepted by the general assembly.
### <a name="change-of-membership-fees"></a>Change of Membership Fees
Another point was the introduction of minimum membership fees.
- **Supporting members:** 5€/month or 50€/year
- **Regular members:** 10€/month or 100€/year
- **Corporate members:** 100€/month or 1000€/year
This change was also unanimously accepted by the general assembly.
## <a name="election-of-the-board--auditors"></a>Election of the Board & Auditors
- **Chairperson:** Previous chairperson kumi was up for vote and was re-elected unanimously. The election was confirmed by the election committee (noniq, ogalbnafets).
- **Treasurer:** jupfi was up for vote and was re-elected unanimously. The election was confirmed by the election committee (noniq, ogalbnafets).
- **Audit:** In addition to ogalbnafets who was auditor in 2024, noniq was elected as a second auditor. The election was confirmed by the election committee (kumi, jupfi).
## <a name="open-discussion"></a>Open Discussion
The open discussion covered a wide range of topics, including collaborations with other organization, emergency strategies, and services we want to provide in the future.
### <a name="emergency-strategy"></a>Emergency Strategy
kumi is mainly responsible for the server administration right now and while there are already some emergency strategies in place, we want to improve on the documentation and actually have test runs for the emergency strategies.
One of the proposed strategies was to have a rescue account set up on the servers which can be used to access the servers in case of an emergency. This account would be set up with a private key which is shared among trusted members, requiring a certain number of members to be present to access the account.
### <a name="services"></a>Services
#### <a name="llm-chat-bot"></a>LLM Chatbot ("CoffeeGPT")
The greatest interest was in a Private.coffee-hosted chatbot as an alternative to ChatGPT and other similar services for Large Language Models (LLM). We actually want to have the hardware on site (Private.coffee location) to host services like this.
We already have a server housing and servers for this purpose. However we will still need some SSDs and other auxiliary hardware.
jupfi will look into the hardware setup and electrical installation while kumi will look into the software setup. Right now the plan ist to host [Ollama models](https://ollama.com/) since we already have experience with hosting them.
Access to the service will be provided to members of Private.coffee so hopefully we'll see you there soon!
#### <a name="coffee-machine-gaming-server"></a>"Coffee Machine" Gaming Server
We already have [ongoing development](https://git.private.coffee/PrivateCoffee/coffeemachine) on a service for hosting a variety of game servers.
ogalbnafets proposed that we could rotate through different games and have a "game of the month" where we host a server for a specific game for a month. People could then join and play together, maybe with some fixed times for playing together. This would not have to be exclusive to members of Private.coffee.
#### <a name="quackscape"></a>Quackscape
In the past kumi and ogalbnafets were working on an online panoramic content management system called [Quackscape](https://git.private.coffee/PrivateCoffee/quackscape). Development has been on hold for a while but we want to pick it up again.
### <a name="merchandise"></a>Merchandise
The Private.coffee mugs were a great success and we want to continue with merchandise. Highest priority right now are stickers which people can pass around and put on their laptops.
Another important merchandise item would be t-shirts. We would like to have the designs embroidered on the shirts so they are more durable.
The final merchandise item we discussed are coffee-to-go cups.
ogalbnafets will look into the production of the different merchandise items.
### <a name="hackathon"></a>Hackathon
We want to host a hackathon in the future and already decided on a date: **April 19th in Vienna**. We have not yet decided on a topic but we will keep you updated. One idea that was proposed was to have a hackathon on the topic of hardware, since there are 3D printers and CNC mills available at the DIE ANTWORT office.
### <a name="collaborations"></a>Collaborations
noniq has a couple of contacts in the field of digital rights and privacy and we want to collaborate with them in the future. We don't have any concrete plans yet but we will keep you updated.
We also want to attend the [Grazer Linuxtage](https://www.linuxtage.at/) in April but probably only as visitors.
### <a name="blog-posts"></a>Blog Posts
Kumi did great work on the website and we now have a [blog section](https://private.coffee/blog/) and we want to have more blog posts in the future. If you have any ideas or want to write a blog post yourself, please let us know! Right now we maybe want to introduce the different services we are hosting and give Private.coffee a face by introducing the people behind it.
## <a name="conclusion"></a>Conclusion
After the general assembly we had some more discussions at a nearby restaurant. We all agreed that the general assembly was lots of fun and we are looking forward to the next year.
---
## <a name="documents"></a>Documents
- You can find the protocol of the general assembly [here](documents/protocol_general_assembly_2025.pdf).
- Some statues were changed, you can find the official german text for the [change of function periods here](documents/0402_function-periods.md) and the [change of membership fees here](documents/0401_membership-fees.md).
- The slides that were presented at the general assembly can be found [here](documents/Private.coffee_slides.pdf).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 471 KiB

View file

@ -1,31 +0,0 @@
---
title: Linuxtage 26/04/2025
date: 2025-05-01 17:00:00
author: jupfi
license: CC BY-SA 4.0
license_url: https://creativecommons.org/licenses/by-sa/4.0/
tags: events
excerpt: Private.coffee is at the Grazer Linuxtage
---
# Grazer Linuxtage
The [Grazer Linuxtage](blog/20250101-board_meeting/index.md) is conference about open-source software and hardware and was held in Graz, Austria from April 25th to April 26th, 2025.
Naturally we felt like it was the perfect place to give a quick overview about private.coffee and it's services.
You can find the _lightning talk_ here (in german):
<iframe title="Lightning Talks" width="560" height="315" src="https://cuddly.tube/videos/embed/7MT1n4H2xU3GwmP2SUT3N1?start=2107s" frameborder="0" allowfullscreen="" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>
We met quite a few people who actually already knew us and our services which was pretty cool!
A lesson that we learned is that we should probably reach out more to other similar associations and collectives to coordinate our efforts.
We also had quite a bit of private.coffee merchandise with us, from stickers to shirts and hoodies that we wore during our time there. For next year we will try to get some pamphlets or flyers printed out, to hand them out to people. And maybe we'll even go for our own booth, but let's see how that goes.
Kumi, jupfi and ogalbnafets were there and we had a great time - here are some pictures from the talk:
<img src="pictures/kumi_talk.jpg" alt="Kumi at the lightning talk. A crowd is visible watching Kumi giving the lightning talk. The Linuxtage mascot (a penguin with a Steirerhut) is visible on a chalkboard." width="75%" >
<img src="pictures/jupfi_talk.png" alt="Jupfi at the lightning talk. A crowd is visible watching jupfi giving the lightning talk. The Linuxtage mascot (a penguin with a Steirerhut) is visible on a chalkboard." width="75%">
Hopefully we'll see some of you there next year as well!

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

View file

@ -1,28 +0,0 @@
---
title: Transfer.coffee Peer-to-peer file sharing
date: 2025-05-30 17:00:00
author: jupfi
license: CC BY-SA 4.0
license_url: https://creativecommons.org/licenses/by-sa/4.0/
tags: services
excerpt: Transfer.coffee is a simple web application that allows users to share files.
---
Have you ever been annoyed by one of your colleagues sending you links to WeTransfer and the likes for sharing files?
Then you had to suffer through huge ad banners that companies will actually advertise as the most notable on the internet [1].
Worry no longer - [private.coffee](private.coffee) has got you covered. Transfer.coffee is a simple web application that allows users to share files. It's really easy to use and ad-free:
1. Upload your file to the [Transfer.coffee](transfer.coffee) web interface
2. [Transfer.coffee](transfer.coffee) generates a simple mnemonic seed (a series of easy-to-remember words)
3. Share this seed with your recipient
4. They enter the seed on [Transfer.coffee](transfer.coffee) to download the file directly from your device
The files are shared using WebTorrent, a peer-to-peer file sharing protocol [2]. This means that the files are not stored on a central server and are instead shared directly between the sender and the recipient.
You can find the source code [here](https://git.private.coffee/PrivateCoffee/transfer.coffee), including some instructions on how to host your own instance!
---
- [1] <https://wetransfer.com/explore/advertising>
- [2] <https://en.wikipedia.org/wiki/WebTorrent>

View file

@ -1,16 +0,0 @@
---
title: Statement on the draft of the Bundestrojaner (state-sponsored trojan horse) law 2025
date: 2025-06-02 17:00:00
author: jupfi
license: CC BY-SA 4.0
license_url: https://creativecommons.org/licenses/by-sa/4.0/
tags: statement
excerpt: Private.coffee has issued a statement on the draft of the Bundestrojaner (state-sponsored trojan horse) law 2025. We urgently request that this draft in its current form and the concept of state surveillance software as a whole not be pursued further.
---
Private.coffee has issued a statement on the draft of the Bundestrojaner (state-sponsored trojan horse) law 2025.
We urgently request that this draft in its current form and the concept of state surveillance software as a whole not be pursued further.
You can find the (german) statement here:
[https://www.parlament.gv.at/gegenstand/XXVIII/SNME/610/](https://www.parlament.gv.at/gegenstand/XXVIII/SNME/610/)

View file

@ -1,31 +0,0 @@
---
title: Board Meeting 13/07/2025
date: 2025-07-13 12:00:00
author: jupfi
license: CC BY-SA 4.0
license_url: https://creativecommons.org/licenses/by-sa/4.0/
tags: board-meeting
excerpt: Minutes of the board meeting on July 13th, 2025.
---
## Attendance
- kumi
- jupfi
The quorum is thereby established.
## Agenda Points
### 1. **Private.coffee Office**
From August 1st, 2025 the private.coffee office will move to the new location at Brandhofgasse 7, 8010 Graz.
Monthly rent is 391.60EUR including utilities and heating. The office will be used for hosting infrastructure and events.
The safety deposit is 1200EUR and the fee for the real estate agent is approximately 1000EUR.
Monthly cost will additionally include insurance of approximately 30EUR and internet costs of approximately 30EUR.
The electricity costs are not yet known, but are expected to be around 50EUR per month.
Additionally a small workshop area will be set up for hardware projects. The equipment for this will be provided by jupfi.
The board unanimously approves the move to the new office location and the associated costs.

View file

@ -1,44 +0,0 @@
{
"bridges": [
{
"name": "Telegram",
"mxid": "@telegrambot:private.coffee"
},
{
"name": "WhatsApp",
"mxid": "@whatsappbot:private.coffee"
},
{
"name": "Signal",
"mxid": "@signalbot:private.coffee"
},
{
"name": "Discord",
"mxid": "@discordbot:private.coffee"
},
{
"name": "Slack",
"mxid": "@slackbot:private.coffee"
},
{
"name": "LinkedIn",
"mxid": "@linkedinbot:private.coffee"
},
{
"name": "GPT-4o",
"mxid": "@gptbot:private.coffee"
},
{
"name": "RSS/Atom feeds",
"mxid": "@rssbot:private.coffee"
},
{
"name": "Instagram",
"mxid": "@instagrambot:private.coffee"
},
{
"name": "Facebook",
"mxid": "@facebookbot:private.coffee"
}
]
}

View file

@ -1,2 +0,0 @@
private.coffee
www.private.coffee

View file

@ -1,18 +1,19 @@
{ {
"2024": { "2024": {
"04": { "4": {
"Membership Fees": { "Membership Fees": {
"EUR": 365 "EUR": 365
}, },
"Server Costs": { "Server Costs": {
"EUR": -216.57 "EUR": -216.57
}, },
"Domain Names": {},
"Administrative Expenses": { "Administrative Expenses": {
"EUR": -36.10, "EUR": -36.10,
"Notes": "Administrative fee for the formation of the association" "Notes": "Administrative fee for the formation of the association"
} }
}, },
"05": { "5": {
"Membership Fees": { "Membership Fees": {
"EUR": 390 "EUR": 390
}, },
@ -25,7 +26,7 @@
"Notes": "Includes setup costs and two monthly payments for new server" "Notes": "Includes setup costs and two monthly payments for new server"
} }
}, },
"06": { "6": {
"Membership Fees": { "Membership Fees": {
"EUR": 382.42 "EUR": 382.42
}, },
@ -36,198 +37,9 @@
"EUR": -49.05 "EUR": -49.05
} }
}, },
"07": { "7": {
"Membership Fees": { "Membership Fees": {
"EUR": 422.42 "EUR": 20
},
"Donations": {
"XMR": 1.0
},
"Server Costs": {
"EUR": -264.99
}
},
"08": {
"Membership Fees": {
"EUR": 402.42
},
"Server Costs": {
"EUR": -416.47
}
},
"09": {
"Membership Fees": {
"EUR": 468.11
},
"Server Costs": {
"EUR": -243.46
},
"Bank Fees": {
"EUR": -53.32
}
},
"10": {
"Membership Fees": {
"EUR": 407.65
},
"Server Costs": {
"EUR": -440.98
},
"Miscellaneous": {
"EUR": 0.01,
"Notes": "Bank account verification"
}
},
"11": {
"Membership Fees": {
"EUR": 387.65
},
"Server Costs": {
"EUR": -366.93
}
},
"12": {
"Membership Fees": {
"EUR": 427.65
},
"Donations": {
"EUR": 80
},
"Bank Fees": {
"EUR": -56.48
},
"Server Costs": {
"EUR": -399.29
},
"Marketing": {
"EUR": -75.8,
"Notes": "Branded coffee mugs"
}
}
},
"2025": {
"01": {
"Membership Fees": {
"EUR": 458.87
},
"Donations": {
"EUR": 15
},
"Bank Fees": {
"EUR": -0.62
},
"Server Costs": {
"EUR": -405.48
}
},
"02": {
"Membership Fees": {
"EUR": 373.46
},
"Donations": {
"EUR": 175
},
"Server Rent": {
"EUR": -403.96,
"Notes": "Renamed from 'Server Costs' to better differentiate between rented and owned servers"
},
"Hardware Costs": {
"EUR": -209.53,
"Notes": "Components for server rack"
},
"Events": {
"EUR": -175,
"Notes": "Food and drinks for General Assembly"
}
},
"03": {
"Membership Fees": {
"EUR": 373.46
},
"Donations": {
"EUR": 79
},
"Server Rent": {
"EUR": -423.44
},
"Bank Fees": {
"EUR": -60.76
},
"Administrative Expenses": {
"EUR": -36.4,
"Notes": "Administrative fee for statute changes"
}
},
"04": {
"Membership Fees": {
"EUR": 423.46
},
"Donations": {
"EUR": 300
},
"Server Rent": {
"EUR": -382.98
},
"Domains": {
"EUR": -35.22,
"Notes": "Now separated from server costs for better clarity"
},
"Marketing": {
"EUR": -296.02,
"Notes": "Branded shirts/hoodies for events, stickers"
}
},
"05": {
"Membership Fees": {
"EUR": 473.46
},
"Donations": {
"EUR": 112
},
"Bank Fees": {
"EUR": -4.08
},
"Reimbursements": {
"EUR": 160,
"Notes": "Reimbursements for hoodies"
},
"Server Rent": {
"EUR": -386.53
},
"Domains": {
"EUR": -73.66
}
},
"06": {
"Membership Fees": {
"EUR": 438.46
},
"Donations": {
"EUR": 1
},
"Server Rent": {
"EUR": -480.85
},
"Domains": {
"EUR": -53.55
},
"Bank Fees": {
"EUR": -60.6
},
"Marketing": {
"EUR": -126,
"Notes": "Embroidery for branded shirts (see April 2025)"
}
},
"07": {
"Membership Fees": {
"EUR": 420.23
},
"Donations": {
"EUR": 75
},
"Server Rent": {
"EUR": -10.10
} }
} }
} }

View file

@ -1,4 +0,0 @@
/security.txt https://security.private.coffee/security.txt
/.well-known/security.txt https://security.private.coffee/security.txt
/metrics /metrics.txt
/metrics/ /metrics.txt

File diff suppressed because it is too large Load diff

View file

@ -1,57 +0,0 @@
// Schedule of special themes for the main page
// Please feel free to suggest new special themes or changes to the existing ones!
//
// The format is an array of objects, each object representing a special theme/occasion.
// Each object has the following properties:
// - name: the name of the theme, used to identify it, must match the .css file name
// - start: the start date of the theme, in the format "YYYY-MM-DD"
// - end: the end date of the theme, in the format "YYYY-MM-DD"
//
// The start and end dates are inclusive. If the start and end dates are the same, the theme will only be active on that day.
// The dates are in the format "YYYY-MM-DD" (ISO 8601). If the year is set to anything before 2020, the theme will be active every year.
//
// If multiple themes are listed as active on the same day, the first match will be used.
[
{
// Trans Day of Visibility
"name": "trans",
"start": "1970-03-30",
"end": "1970-04-01"
},
{
// 4/20
"name": "weed",
"start": "1970-04-20",
"end": "1970-04-20"
},
{
// Autism Acceptance Month
"name": "autism",
"start": "1970-04-01",
"end": "1970-04-30"
},
{
// International Day Against Homophobia, Biphobia, Interphobia and Transphobia
"name": "pride",
"start": "1970-05-17",
"end": "1970-05-17"
},
{
// Europe Day
"name": "europe",
"start": "1970-05-05",
"end": "1970-05-09"
},
{
// Pride Month
"name": "pride",
"start": "1970-06-01",
"end": "1970-06-30"
},
{
// Disability Pride Month
"name": "disability",
"start": "1970-07-01",
"end": "1970-07-31"
}
]

View file

@ -4,21 +4,20 @@ from datetime import datetime
def get_latest_month(data, allow_current=False): def get_latest_month(data, allow_current=False):
years = sorted(data.keys()) years = sorted(data.keys())
latest_year = years[-1]
months = sorted(data[latest_year].keys())
latest_month = months[-1]
if not years: if (
raise ValueError("No data found") not allow_current
and latest_year == str(datetime.now().year)
current_date = datetime.now() and latest_month == str(datetime.now().month)
latest_year = max(years) ):
latest_month = max(data[latest_year].keys()) try:
latest_month = months[-2]
if not allow_current: except IndexError:
if latest_year == str(current_date.year) and latest_month == str(current_date.month).zfill(2): latest_year = years[-2]
if len(data[latest_year]) > 1: latest_month = months[-1]
latest_month = sorted(data[latest_year].keys())[-2]
else:
latest_year = str(int(latest_year) - 1)
latest_month = max(data[latest_year].keys())
return int(latest_month), int(latest_year) return int(latest_month), int(latest_year)
@ -31,12 +30,12 @@ def get_transparency_data(data, year=None, month=None, allow_current=False):
month = max(data[year].keys()) month = max(data[year].keys())
year = str(year) year = str(year)
month = str(month).zfill(2) month = str(month)
if ( if (
not allow_current not allow_current
and year == str(datetime.now().year) and year == str(datetime.now().year)
and month == str(datetime.now().month).zfill(2) and month == str(datetime.now().month)
): ):
try: try:
month = max([m for m in data[year].keys() if m != str(datetime.now().month)]) month = max([m for m in data[year].keys() if m != str(datetime.now().month)])
@ -184,7 +183,7 @@ def generate_transparency_table(result, currencies=None):
""" """
# Add start balance row # Add start balance row
html += "<tr class=\"transparency-start-balance-row\"><td>Account Balance (start of month)</td>" html += "<tr><td>Account Balance (start of month)</td>"
for currency in currencies: for currency in currencies:
value = result["start_balance"].get(currency, Decimal(0)) value = result["start_balance"].get(currency, Decimal(0))
html += f"<td>{format_value(value, currency)}</td>" html += f"<td>{format_value(value, currency)}</td>"

606
main.py
View file

@ -1,22 +1,11 @@
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
import json import json
import pathlib import pathlib
import os
import datetime import datetime
import shutil import shutil
import math
import os
import logging
from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
from threading import Thread
from argparse import ArgumentParser from argparse import ArgumentParser
from typing import Optional
import yaml
import markdown2
from PIL import Image
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
from helpers.finances import ( from helpers.finances import (
generate_transparency_table, generate_transparency_table,
@ -24,403 +13,91 @@ from helpers.finances import (
get_latest_month, get_latest_month,
) )
class StaticPageHandler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory="build", **kwargs)
# Configure logging
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
level=logging.INFO,
)
# Configure Jinja2 environment # Configure Jinja2 environment
env = Environment(loader=FileSystemLoader("templates")) env = Environment(loader=FileSystemLoader('templates'))
# Set up the output directory for static files # Set up the output directory for static files
output_dir = pathlib.Path("build") output_dir = pathlib.Path('build')
output_dir.mkdir(exist_ok=True, parents=True) output_dir.mkdir(exist_ok=True, parents=True)
# Define the icon filter # Define the icon filter
def icon(icon_name): def icon(icon_name):
icon_path = pathlib.Path("assets") / f"dist/icons/{icon_name}.svg" icon_path = pathlib.Path('assets') / f"dist/icons/{icon_name}.svg"
try: try:
with open(icon_path, "r", encoding="utf-8") as file: with open(icon_path, 'r', encoding='utf-8') as file:
file_content = file.read() file_content = file.read()
except FileNotFoundError: except FileNotFoundError:
file_content = "" file_content = ''
return file_content return file_content
env.filters['icon'] = icon
env.filters["icon"] = icon
# Filter for rendering a month name from a number
def month_name(month_number):
return datetime.date(1900, int(month_number), 1).strftime("%B")
env.filters["month_name"] = month_name
def render_template_to_file(template_name, output_name, **kwargs): def render_template_to_file(template_name, output_name, **kwargs):
"""Render a template to a file.
Args:
template_name (str): The name of the template file.
output_name (str): The name of the output file.
**kwargs: Additional keyword arguments to pass to the template.
"""
try: try:
template = env.get_template(template_name) template = env.get_template(template_name)
output_path = output_dir / output_name output_path = output_dir / output_name
kwargs.setdefault("theme", "plain") with open(output_path, 'w', encoding='utf-8') as f:
path = "/" + output_name
if path.endswith("/index.html"):
path = path[:-10]
elif path == "/index.html":
path = "/"
kwargs.setdefault("request", {"path": path})
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w", encoding="utf-8") as f:
f.write(template.render(**kwargs)) f.write(template.render(**kwargs))
except TemplateNotFound: except TemplateNotFound:
logging.error(f"Template {template_name} not found.") print(f"Template {template_name} not found.")
def generate_static_site(development_mode=False):
def create_thumbnail(input_image_path, output_image_path, size=(150, 150)): # Common context
with Image.open(input_image_path) as img: kwargs = {}
img.thumbnail(size) if development_mode:
img.save(output_image_path) kwargs.update(
{
"warning": env.get_template("prod-warning.html").render(),
def calculate_relative_path(depth):
return "../" * depth
def copy_assets(src_dir, dest_dir):
for item in src_dir.iterdir():
if item.is_dir():
# Recurse for subdirectories
item_dest_dir = dest_dir / item.name
item_dest_dir.mkdir(parents=True, exist_ok=True)
copy_assets(item, item_dest_dir)
elif item.is_file():
shutil.copy(item, dest_dir)
def parse_markdown_file(filepath):
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
# Split the front matter and markdown content
parts = content.split("---", 2)
if len(parts) == 3:
_, fm_text, md_content = parts
front_matter = yaml.safe_load(fm_text)
else:
front_matter, md_content = {}, content
return front_matter, md_content
def generate_blog_html(template_kwargs={}, posts_per_page=5):
blog_dir = pathlib.Path("blog")
blog_posts = []
blog_tags = {}
for post_dir in blog_dir.iterdir():
if post_dir.is_dir():
md_path = post_dir / "index.md"
if md_path.exists():
front_matter, md_content = parse_markdown_file(md_path)
html_content = markdown2.markdown(md_content)
# Only process future posts in development mode
if front_matter.get("date"):
if isinstance(front_matter["date"], str):
post_date = datetime.datetime.strptime(
front_matter["date"], "%Y-%m-%d %H:%M:%S"
)
else:
post_date = front_matter["date"]
front_matter["date"] = post_date.strftime("%Y-%m-%d %H:%M:%S")
if post_date > datetime.datetime.now():
if not args.dev:
logging.info(f"Skipping future post: {post_dir.name}")
continue
front_matter["date"] = front_matter["date"] + " (future)"
front_matter["content"] = html_content
front_matter["slug"] = post_dir.name
# Add post to relevant tag lists
if "tags" in front_matter:
for tag in front_matter["tags"].split(","):
tag = tag.strip()
if tag not in blog_tags:
blog_tags[tag] = []
blog_tags[tag].append(front_matter)
# Create excerpt if not present
if "excerpt" not in front_matter:
excerpt = html_content.split("</p>")[0]
front_matter["excerpt"] = excerpt
blog_posts.append(front_matter)
# Ensure the build directory structure exists
output_post_dir = output_dir / "blog" / post_dir.name
output_post_dir.mkdir(parents=True, exist_ok=True)
# Generate thumbnail if image is present
if "image" in front_matter:
original_image = post_dir / front_matter["image"]
thumbnail_image_name = f"thumb_{original_image.name}"
thumbnail_image = (
output_dir / "blog" / post_dir.name / thumbnail_image_name
)
create_thumbnail(original_image, thumbnail_image)
front_matter["thumbnail"] = thumbnail_image_name
# Copy non-markdown assets
copy_assets(post_dir, output_post_dir)
# Sort posts by date, descending
blog_posts.sort(key=lambda x: x.get("date", ""), reverse=True)
# Render each individual post
for post in blog_posts:
post.setdefault("license", "CC BY-SA 4.0")
post.setdefault(
"license-url", "https://creativecommons.org/licenses/by-sa/4.0/"
)
post.setdefault("author", "Private.coffee Team")
post.setdefault("author-url", "https://private.coffee")
post["tags"] = [tag.strip() for tag in post.get("tags", "").split(",") if tag]
post_slug = post["slug"]
render_template_to_file(
"blog/post.html",
f"blog/{post_slug}/index.html",
**{**post, "relative_path": calculate_relative_path(2)},
**template_kwargs,
)
# Add tags to template kwargs
template_kwargs["tags"] = blog_tags.keys()
# Generate each index page
total_posts = len(blog_posts)
total_pages = math.ceil(total_posts / posts_per_page)
for page in range(total_pages):
start = page * posts_per_page
end = start + posts_per_page
paginated_posts = blog_posts[start:end]
context = {
"posts": paginated_posts,
"current_page": page + 1,
"total_pages": total_pages,
"relative_path": calculate_relative_path(1 if page == 0 else 3),
**template_kwargs,
} }
output_path = (
"blog/index.html" if page == 0 else f"blog/page/{page + 1}/index.html"
)
render_template_to_file("blog/index.html", output_path, **context)
if page == 0:
pathlib.Path("build/blog/page/1").mkdir(parents=True, exist_ok=True)
context["relative_path"] = calculate_relative_path(3)
render_template_to_file(
"blog/index.html", "blog/page/1/index.html", **context
) )
# Generate tag pages # Load services data
for tag, posts in blog_tags.items(): services = json.loads(
tag_posts = sorted(posts, key=lambda x: x.get("date", ""), reverse=True) (pathlib.Path(__file__).parent / "data" / "services.json").read_text()
total_tag_posts = len(tag_posts)
total_tag_pages = math.ceil(total_tag_posts / posts_per_page)
for page in range(total_tag_pages):
start = page * posts_per_page
end = start + posts_per_page
paginated_posts = tag_posts[start:end]
context = {
"posts": paginated_posts,
"current_page": page + 1,
"total_pages": total_tag_pages,
"tag": tag,
"relative_path": calculate_relative_path(3),
**template_kwargs,
}
output_path = (
f"blog/tag/{tag}/index.html"
if page == 0
else f"blog/tag/{tag}/page/{page + 1}/index.html"
)
render_template_to_file("blog/index.html", output_path, **context)
if page == 0:
pathlib.Path(f"build/blog/tag/{tag}/page/1").mkdir(
parents=True, exist_ok=True
)
context["relative_path"] = calculate_relative_path(5)
render_template_to_file(
"blog/index.html", f"blog/tag/{tag}/page/1/index.html", **context
) )
logging.info("Blog section generated successfully.") # Load finances data
finances = json.loads(
(pathlib.Path(__file__).parent / "data" / "finances.json").read_text()
def generate_blog_rss(development_mode=False):
blog_dir = pathlib.Path("blog")
blog_posts = []
for post_dir in blog_dir.iterdir():
if post_dir.is_dir():
md_path = post_dir / "index.md"
if md_path.exists():
front_matter, _ = parse_markdown_file(md_path)
# Ensure date is RFC 822 compliant
if "date" in front_matter:
if isinstance(front_matter["date"], str):
post_date = datetime.datetime.strptime(
front_matter["date"], "%Y-%m-%d %H:%M:%S"
) )
else:
post_date = front_matter["date"]
if post_date.tzinfo is None:
post_date = post_date.astimezone()
front_matter["date"] = post_date.strftime(
"%a, %d %b %Y %H:%M:%S %z"
)
front_matter["link"] = (
f"https://{"dev." if development_mode else ""}private.coffee/blog/{post_dir.name}/"
)
blog_posts.append(front_matter)
blog_posts.sort(key=lambda x: x.get("date", ""), reverse=True)
context = {
"development_mode": development_mode,
"posts": blog_posts,
"current_time": datetime.datetime.now()
.astimezone()
.strftime("%a, %d %b %Y %H:%M:%S %z"),
}
render_template_to_file("blog/rss.xml", "blog/rss.xml", **context)
logging.info("RSS feed generated successfully.")
def generate_static_pages(development_mode=False, data={}, template_kwargs={}):
# Iterate over all templates in the templates directory # Iterate over all templates in the templates directory
templates_path = pathlib.Path("templates") templates_path = pathlib.Path('templates')
for template_file in templates_path.glob("*.html"): for template_file in templates_path.glob('*.html'):
template_name = template_file.stem template_name = template_file.stem
context = template_kwargs.copy() context = kwargs.copy()
context["path"] = f"{template_name}.html" if template_name != "index" else "" if template_name in ["index", "simple"]:
context.update({"services": services})
if template_name == "index": if template_name == "membership":
# Get featured services for the homepage
featured_services = [
service
for service in data["services"]["services"]
if service.get("featured", False)
and not service.get("exclude_from_index", False)
]
# Limit to 3 featured services if there are more
# TODO: Do something smarter here
if len(featured_services) > 6:
featured_services = featured_services[:3]
context.update(
{"services": data["services"], "featured_services": featured_services}
)
if template_name == "services":
# Extract all categories from services
categories = sorted(
list(
set(
[
service.get("category", "Uncategorized")
for service in data["services"]["services"]
if not service.get("exclude_from_index", False)
]
)
)
)
context.update({"services": data["services"], "categories": categories})
if template_name == "simple":
context.update({"services": data["services"]})
if template_name == "bridges":
context.update({"bridges": data["bridges"]})
if template_name.startswith("membership"):
allow_current = development_mode allow_current = development_mode
finances_month, finances_year = get_latest_month( finances_month, finances_year = get_latest_month(finances, allow_current)
data["finances"], allow_current
)
finances_period = datetime.date(finances_year, finances_month, 1) finances_period = datetime.date(finances_year, finances_month, 1)
finances_period_str = finances_period.strftime("%B %Y") finances_period_str = finances_period.strftime("%B %Y")
finances_table = generate_transparency_table( finances_table = generate_transparency_table(
get_transparency_data( get_transparency_data(finances, finances_year, finances_month, allow_current)
data["finances"], finances_year, finances_month, allow_current
) )
) context.update({
context.update(
{
"finances": finances_table, "finances": finances_table,
"finances_period": finances_period_str, "finances_period": finances_period_str,
} })
)
if template_name == "transparency": if template_name == "transparency":
finance_data = {} finance_data = {}
for year in sorted(data["finances"].keys(), reverse=True): for year in sorted(finances.keys(), reverse=True):
for month in sorted(data["finances"][year].keys(), reverse=True): for month in sorted(finances[year].keys(), reverse=True):
if year not in finance_data: if year not in finance_data:
finance_data[year] = {} finance_data[year] = {}
finance_data[year][month] = generate_transparency_table( finance_data[year][month] = generate_transparency_table(
get_transparency_data(data["finances"], year, month, True) get_transparency_data(finances, year, month)
) )
context.update({"finances": finance_data}) context.update({"finances": finance_data})
render_template_to_file( render_template_to_file(f"{template_name}.html", f"{template_name}.html", **context)
f"{template_name}.html", f"{template_name}.html", **context
)
logging.info("Static pages generated successfully.") # Generate metrics
balances = get_transparency_data(finances, allow_current=True)["end_balance"]
def generate_metrics(data):
balances = get_transparency_data(data["finances"], allow_current=True)[
"end_balance"
]
response = ( response = (
"# HELP privatecoffee_balance The balance of the private.coffee account\n" "# HELP privatecoffee_balance The balance of the private.coffee account\n"
@ -431,210 +108,21 @@ def generate_metrics(data):
response += f'privatecoffee_balance{{currency="{currency}"}} {balance}\n' response += f'privatecoffee_balance{{currency="{currency}"}} {balance}\n'
metrics_path = output_dir / "metrics.txt" metrics_path = output_dir / "metrics.txt"
with open(metrics_path, "w", encoding="utf-8") as f: with open(metrics_path, 'w', encoding='utf-8') as f:
f.write(response) f.write(response)
logging.info("Metrics generated successfully.")
def autoselect_theme():
# Load schedule from file
json_content = (
pathlib.Path(__file__).parent / "data" / "theme_schedule.jsonc"
).read_text()
# Remove comments
json_content = "\n".join(
line.split("//", 1)[0] for line in json_content.split("\n")
)
schedule = json.loads(json_content)
# Find the current theme
current_time = datetime.datetime.now().timestamp()
for theme in schedule:
start_time = datetime.datetime.strptime(theme["start"], "%Y-%m-%d").replace(
hour=0, minute=0, second=0
)
# Special case for recurring themes - if the start time is *far* in the past, assume it's recurring
if start_time.year < 2000:
start_time = start_time.replace(year=datetime.datetime.now().year)
end_time = datetime.datetime.strptime(theme["end"], "%Y-%m-%d").replace(
hour=23, minute=59, second=59
)
if end_time.year < 2000:
end_time = end_time.replace(year=datetime.datetime.now().year)
# Special case for themes that span the new year
if start_time > end_time:
end_time = end_time.replace(year=end_time.year + 1)
if start_time.timestamp() <= current_time <= end_time.timestamp():
logging.info(f"Selected theme: {theme['name']}")
return theme["name"]
logging.info("No theme selected or scheduled, defaulting to plain.")
return "plain"
def generate_static_site(
development_mode: bool = False,
theme: Optional[str] = None,
domains: Optional[str] = None,
is_primary: bool = False,
):
if not theme:
theme = autoselect_theme()
# Common context
template_kwargs = {
"timestamp": int(datetime.datetime.now().timestamp()),
"theme": theme,
"is_primary": is_primary,
}
if development_mode:
template_kwargs.update(
{
"warning": env.get_template("prod-warning.html").render(),
}
)
data = {}
# Load services data
data["services"] = json.loads(
(pathlib.Path(__file__).parent / "data" / "services.json").read_text()
)
# Load finances data
data["finances"] = json.loads(
(pathlib.Path(__file__).parent / "data" / "finances.json").read_text()
)
# Load bridges data
data["bridges"] = json.loads(
(pathlib.Path(__file__).parent / "data" / "bridges.json").read_text()
)
# Generate static pages
generate_static_pages(development_mode, data, template_kwargs)
# Generate blog section
generate_blog_html(template_kwargs)
generate_blog_rss(development_mode)
# Generate metrics
generate_metrics(data)
# Copy static assets # Copy static assets
for folder in ["assets", "data"]: assets_src = pathlib.Path('assets')
src = pathlib.Path(folder) assets_dst = output_dir / 'assets'
dst = output_dir / folder if assets_dst.exists():
if dst.exists(): shutil.rmtree(assets_dst)
shutil.rmtree(dst) shutil.copytree(assets_src, assets_dst)
shutil.copytree(src, dst)
# Create .domains for Forgejo Pages
domains_dest_path = output_dir / ".domains"
if domains_dest_path.exists():
os.remove(domains_dest_path)
if domains:
for domain in domains.split(","):
domain = domain.strip()
if domain:
with open(domains_dest_path, "a", encoding="utf-8") as f:
f.write(f"{domain}\n")
else:
# Default to domains from data/domains.txt
domains_path = pathlib.Path("data/domains.txt")
if domains_path.exists():
with open(domains_dest_path, "w", encoding="utf-8") as f:
f.write(domains_path.read_text())
# Copy the .well-known directory
well_known_src = pathlib.Path("assets") / ".well-known"
well_known_dst = output_dir / ".well-known"
if well_known_src.exists():
if well_known_dst.exists():
shutil.rmtree(well_known_dst)
shutil.copytree(well_known_src, well_known_dst)
# Create _redirects file for Forgejo Pages
redirects_dst = output_dir / "_redirects"
redirects_src = pathlib.Path("data") / "redirects.txt"
if redirects_src.exists():
if redirects_dst.exists():
os.remove(redirects_dst)
with open(redirects_dst, "w", encoding="utf-8") as f:
f.write(redirects_src.read_text())
logging.info("Static site generated successfully.")
print("Static site generated successfully.")
if __name__ == "__main__": if __name__ == "__main__":
parser = ArgumentParser(description="Generate the private.coffee static site.") parser = ArgumentParser(description="Generate the private.coffee static site.")
parser.add_argument("--dev", action="store_true", help="Enable development mode") parser.add_argument("--dev", action="store_true", help="Enable development mode")
parser.add_argument(
"--serve", action="store_true", help="Serve the site after building"
)
parser.add_argument(
"--port", type=int, default=8000, help="Port to serve the site on"
)
parser.add_argument("--theme", type=str, help="Theme to use for the site")
parser.add_argument(
"--domains",
type=str,
help="Domains to use for Forgejo Pages (default: domains from data/domains.txt)",
)
parser.add_argument(
"--is-primary",
action="store_true",
help="This is the primary instance",
)
parser.add_argument("--debug", action="store_true", help="Enable debug output")
args = parser.parse_args() args = parser.parse_args()
if os.environ.get("PRIVATECOFFEE_DEV"): generate_static_site(development_mode=args.dev)
args.dev = True
if os.environ.get("PRIVATECOFFEE_THEME"):
args.theme = os.environ["PRIVATECOFFEE_THEME"]
if os.environ.get("PRIVATECOFFEE_PORT"):
args.serve = True
args.port = int(os.environ["PRIVATECOFFEE_PORT"])
if os.environ.get("PRIVATECOFFEE_DOMAINS"):
args.domains = os.environ["PRIVATECOFFEE_DOMAINS"]
if os.environ.get("PRIVATECOFFEE_DEBUG"):
logging.getLogger().setLevel(logging.DEBUG)
if os.environ.get("PRIVATECOFFEE_IS_PRIMARY"):
args.is_primary = True
generate_static_site(
development_mode=args.dev,
theme=args.theme,
domains=args.domains,
is_primary=args.is_primary,
)
if args.serve:
server = TCPServer(("", args.port), StaticPageHandler)
logging.info(f"Serving on http://localhost:{args.port}")
thread = Thread(target=server.serve_forever)
thread.start()
thread.join()

View file

@ -1,6 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}

View file

@ -1,4 +1,2 @@
flask
jinja2 jinja2
markdown2[all]
pyyaml
pillow

View file

@ -3,152 +3,94 @@
<!-- This file was created as part of the Private.coffee project <!-- This file was created as part of the Private.coffee project
It is licensed under the MIT license It is licensed under the MIT license
For more information, please visit https://private.coffee --> For more information, please visit https://private.coffee -->
<head> <head>
<meta charset="utf8" /> <meta charset="utf8" />
<meta name="viewport" <meta
content="width=device-width, initial-scale=1.0, shrink-to-fit=no" /> name="viewport"
<meta name="description" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"
content="Private.coffee is a privacy-focused non-profit association, dedicated to supporting privacy and digital sovereignty." /> />
<meta name="keywords" <title>{% block title %}{% endblock %} - Private.coffee</title>
content="privacy, digital sovereignty, non-profit, association, privacy-focused" /> <link rel="stylesheet" href="/assets/dist/css/bootstrap.min.css" />
<meta name="author" content="Private.coffee" /> <link rel="stylesheet" href="/assets/css/base.css" />
<meta property="og:title"
content="Private.coffee - Open-source software is best served hot" />
<meta property="og:description"
content="Private.coffee is a privacy-focused non-profit association, dedicated to supporting privacy and digital sovereignty." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://private.coffee/" />
<meta property="og:image"
content="https://private.coffee/assets/img/logo-inv_grad.png" />
<meta property="og:site_name" content="Private.coffee" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="Private.coffee" />
<meta name="twitter:description"
content="Private.coffee is a privacy-focused non-profit association, dedicated to supporting privacy and digital sovereignty." />
<meta name="twitter:image"
content="https://private.coffee/assets/img/logo-inv_grad.png" />
<link rel="icon"
type="image/png"
href="{{ relative_path }}assets/img/logo-inv_grad.png" />
<title>
{% block title %}
{% endblock title %}
- Private.coffee</title>
<link rel="stylesheet"
href="{{ relative_path }}assets/dist/css/bootstrap.min.css?v={{ timestamp }}" />
<link rel="stylesheet"
href="{{ relative_path }}assets/css/base.css?v={{ timestamp }}" />
<link rel="stylesheet"
href="{{ relative_path }}assets/css/theme/{{ theme }}.css?v={{ timestamp }}"
id="theme-stylesheet">
<link rel="stylesheet"
href="{{ relative_path }}assets/dist/fonts/fonts.css?v={{ timestamp }}" />
</head> </head>
<body> <body>
<nav class="navbar navbar-expand-md py-3 navbar-light" id="mainNav"> <nav
class="navbar navbar-expand-md py-3 navbar-light"
id="mainNav"
>
<div class="container"> <div class="container">
<div class="row d-lg-flex align-items-lg-center"> <div class="row d-lg-flex align-items-lg-center">
<div class="col p-0" id="logo-wrapper"> <div class="col p-0">
<a href="/"> <a href="/"
<div id="smallLogoContainer"></div> ><img src="/assets/img/logo-inv_grad.svg" style="height: 60px"
</a> /></a>
</div> </div>
<div class="col d-flex"> <div class="col d-flex">
<a class="navbar-brand d-flex align-items-center" href="/"> <a class="navbar-brand d-flex align-items-center" href="/">
<p class="mb-0" <p
style="line-height: 1.2rem; class="mb-0"
color: var(--bs-tertiary-color)"> style="line-height: 1.2rem; color: var(--bs-tertiary-color)"
<span class="ps-2 fancy-text-primary"><span style="color: rgb(35, 35, 35)">Private.coffee</span></span> >
<br class="that-br" /> <span class="ps-2 fancy-text-primary"
<span class="ps-2 slogan">Open-source software is best served hot</span> ><span style="color: rgb(35, 35, 35)"
>Private.coffee</span
></span
><br class="that-br" /><span class="ps-2 slogan"
>Empowering Privacy with Open Source</span
>
</p> </p>
</a> </a>
</div> </div>
</div> </div>
<div class="navbar" id="navcol-1"> <div class="navbar" id="navcol-1">
<ul class="navbar-nav mx-auto"> <ul class="navbar-nav mx-auto">
<li class="nav-item"> <li class="nav-item"><a class="nav-link active" href="/index.html">Home</a></li>
<a class="nav-link active" href="{{ relative_path }}index.html">Home</a> <li class="nav-item"><a class="nav-link" href="https://status.private.coffee/">Status</a></li>
</li> </ul><a class="btn btn-primary shadow navbar-btn" role="button" href="/membership.html">JOIN &amp; SUPPORT</a>
<li class="nav-item">
<a class="nav-link" href="{{ relative_path }}services.html">Services</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ relative_path }}blog/">Blog</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://status.private.coffee/">Status</a>
</li>
</ul>
{% if is_primary %}
<div class="theme-toggle-container me-2">
{% if theme == 'dark' %}
<a href="https://private.coffee{{ request.path }}"
class="btn btn-outline-primary theme-toggle-btn"
aria-label="Switch to light theme">
<span class="theme-icon light-icon">{{ "sun" | icon | safe }}</span>
</a>
{% else %}
<a href="https://dark.private.coffee{{ request.path }}"
class="btn btn-outline-primary theme-toggle-btn"
aria-label="Switch to dark theme">
<span class="theme-icon dark-icon">{{ "moon" | icon | safe }}</span>
<span class="theme-icon light-icon" style="display: none;">{{ "sun" | icon | safe }}</span>
</a>
{% endif %}
</div>
{% endif %}
<a class="btn btn-primary shadow navbar-btn"
role="button"
href="{{ relative_path }}membership.html">JOIN &amp; SUPPORT</a>
</div>
</div> </div>
</nav> </nav>
{% if warning %}{{ warning|safe }}{% endif %} {% if warning %}{{ warning|safe }}{% endif %}
{% block content %} {% block content %}{% endblock %}
{% endblock content %}
<footer class="bg-primary-gradient"> <footer class="bg-primary-gradient">
<div class="container py-4 py-lg-5"> <div class="container py-4 py-lg-5">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-sm-4 col-md-3 text-center text-lg-start d-flex flex-column"> <div
class="col-sm-4 col-md-3 text-center text-lg-start d-flex flex-column"
>
<h3 class="fs-6 fw-bold">Legal Stuff</h3> <h3 class="fs-6 fw-bold">Legal Stuff</h3>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li> <li><a href="/legal.html">Legal Notice</a></li>
<a href="{{ relative_path }}legal.html">Legal Notice</a> <li><a href="/privacy.html">Privacy Notice</a></li>
</li> <li><a href="/terms.html">Terms of Service</a></li>
<li>
<a href="{{ relative_path }}privacy.html">Privacy Notice</a>
</li>
<li>
<a href="{{ relative_path }}terms.html">Terms of Service</a>
</li>
</ul> </ul>
</div> </div>
<div class="col-sm-4 col-md-3 text-center text-lg-start d-flex flex-column"></div> <div
<div class="col-lg-3 text-center text-lg-start d-flex flex-column align-items-center order-first align-items-lg-start order-lg-last"> class="col-sm-4 col-md-3 text-center text-lg-start d-flex flex-column"
></div>
<div
class="col-lg-3 text-center text-lg-start d-flex flex-column align-items-center order-first align-items-lg-start order-lg-last"
>
<div class="fw-bold d-flex align-items-center mb-2"> <div class="fw-bold d-flex align-items-center mb-2">
<span class="bs-icon-sm bs-icon-circle bs-icon-primary d-flex justify-content-center align-items-center bs-icon me-2">{{ "coffee" | icon | safe }}</span><span>Private.coffee</span> <span
class="bs-icon-sm bs-icon-circle bs-icon-primary d-flex justify-content-center align-items-center bs-icon me-2"
>{{ "coffee" | icon | safe }}</span
><span>Private.coffee</span>
</div> </div>
<p class="text-muted"> <p class="text-muted">
Private.coffee is a privacy-focused non-profit association, Private.coffee is a privacy-focused non-profit association, dedicated
dedicated to supporting privacy and digital sovereignty. to supporting privacy and digital sovereignty.
</p> </p>
</div> </div>
</div> </div>
<hr /> <hr />
<div class="text-muted d-flex justify-content-between align-items-center pt-3"> <div
class="text-muted d-flex justify-content-between align-items-center pt-3"
>
<p class="mb-0">Made with ❤️ and ☕ by Private.coffee</p> <p class="mb-0">Made with ❤️ and ☕ by Private.coffee</p>
<p class="mb-0"> <p class="mb-0">{{ "rainbow" | icon | safe }}</p>
<a href="https://git.private.coffee/privatecoffee/privatecoffee-website">
<img src="https://shields.private.coffee/gitea/last-commit/privatecoffee/privatecoffee-website?gitea_url=https://git.private.coffee&logo=forgejo" />
</a>
<a href="https://pride.coffee">{{ "rainbow" | icon | safe }}</a>
</p>
</div>
</div> </div>
</footer> </footer>
{% if is_primary and not theme == "dark" %}
<script src="{{ relative_path }}assets/js/theme-toggle.js?v={{ timestamp }}"></script>
{% endif %}
</body> </body>
</html> </html>

View file

@ -1,58 +0,0 @@
{% extends "base.html" %}
{% block title %}
Blog
{% endblock title %}
{% block content %}
<div class="container my-5">
<h1>Blog{% if tag %} index for tag {{ tag }}{% endif %}</h1>
<ul class="list-unstyled">
{% for post in posts %}
<li class="d-flex align-items-center mb-4 p-3 border-bottom">
{% if post.thumbnail %}
<img src="{{ relative_path }}blog/{{ post.slug }}/{{ post.thumbnail }}"
alt="{{ post.title }} thumbnail"
class="me-3"
style="width: 150px;
height: 150px">
{% endif %}
<div>
<h2>
<a href="{{ relative_path }}blog/{{ post.slug }}/index.html">{{ post.title }}</a>
</h2>
<small class="text-muted">by {{ post.author }}, published {{ post.date }}</small>
{% if post.tags %}
<br>
<small class="text-muted">tags:
{% for tag in post.tags %}<a href="{{ relative_path }}blog/tag/{{ tag }}">{{ tag }}</a>{% endfor %}
</small>
{% endif %}
<p>
{{ post.excerpt }} <a href="{{ relative_path }}blog/{{ post.slug }}/index.html">[read more]</a>
</p>
</div>
</li>
{% endfor %}
</ul>
<nav class="mt-4">
<ul class="pagination justify-content-center">
{% if current_page > 1 %}
<li class="page-item">
<a class="page-link"
href="{{ relative_path }}blog/page/{{ current_page - 1 }}/">Previous</a>
</li>
{% endif %}
{% for i in range(1, total_pages + 1) %}
<li class="page-item {% if i == current_page %}active{% endif %}">
<a class="page-link" href="{{ relative_path }}blog/page/{{ i }}/">{{ i }}</a>
</li>
{% endfor %}
{% if current_page < total_pages %}
<li class="page-item">
<a class="page-link"
href="{{ relative_path }}blog/page/{{ current_page + 1 }}/">Next</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endblock content %}

View file

@ -1,17 +0,0 @@
{% extends "base.html" %}
{% block title %}
{{ title }}
{% endblock title %}
{% block content %}
<div class="container my-5">
<h1>{{ title }}</h1>
<p>
<small>by <a href="{{ author_url }}">{{ author }}</a>, published {{ date }}</small>
</p>
<div>{{ content|safe }}</div>
<hr>
<p>
<small>This post by <a href="{{ author_url }}">{{ author }}</a> is licensed under the <a href="{{ license_url }}">{{ license }}</a>.</small>
</p>
</div>
{% endblock content %}

View file

@ -1,24 +0,0 @@
<?xml version="1.0"?>
<rss version="2.0"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>Private.coffee Blog</title>
<link>https://{% if development_mode %}dev.{% endif %}private.coffee/blog</link>
<atom:link href="https://{% if development_mode %}dev.{% endif %}private.coffee/blog/rss.xml" rel="self" type="application/rss+xml" />
<description>Your dose of private caffeine!</description>
<pubDate>{{ current_time }}</pubDate>
<docs>http://blogs.law.harvard.edu/tech/rss</docs>
{% for post in posts %}
<item>
<title>{{ post.title }}</title>
<description>{{ post.description|safe }}</description>
<pubDate>{{ post.date }}</pubDate>
<guid>{{ post.link }}</guid>
<link>{{ post.link }}</link>
<dc:creator><![CDATA[{{ post.author }}]]></dc:creator>
</item>
{% endfor %}
</channel>
</rss>

View file

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bridges and bots</title>
</head>
<body>
<div style="font-family: Arial, sans-serif;
background-color: #f2f2f2;
color: #333;
line-height: 1.6;
padding: 20px;
max-width: 600px;
margin: auto">
<h2 style="color: #333;">Bridges and bots</h2>
<p>These are the bridges and bots available to Private.coffee Matrix users:</p>
<ul style="list-style: none; padding: 0;">
{% for bridge in bridges.bridges %}
<li style="padding: 8px 0;">
<a style="color: #1a5dab;
text-decoration: none"
href="https://matrix.pcof.fi/#/{{ bridge.mxid }}">{{ bridge.name }}</a> - {{ bridge.mxid }}
</li>
{% endfor %}
</ul>
</div>
</body>
</html>

View file

@ -1,85 +1,76 @@
{% extends "base.html" %} {% extends "base.html" %} {% block title %}Home{% endblock %} {% block content
{% block title %}
Home
{% endblock title %}
{% block content
%} %}
<header class="bg-primary-gradient"> <header class="bg-primary-gradient">
<div class="container pt-4 pt-xl-5 pb-4 pb-xl-5"> <div class="container pt-4 pt-xl-5 pb-4 pb-xl-5">
<div class="row gy-5 pt-5"> <div class="row gy-5 pt-5">
<div class="col-md-8 col-xl-6 text-center text-md-start mx-auto"> <div class="col-md-8 col-xl-6 text-center text-md-start mx-auto">
<div class="text-center"> <div class="text-center">
<h2>Open-source software is best served hot</h2> <h2>Empowering Privacy with Open Source</h2>
</div> </div>
<p class="text-center special-header fancy-text-primary mb-0" <p class="text-center special-header fancy-text-primary mb-0" style="font-weight: 500;">Private.coffee</p>
style="font-weight: 500">Private.coffee</p>
</div>
<div class="col-12 col-lg-10 mx-auto justify-content-center d-flex">
<div id="logoContainer"></div>
</div> </div>
<div class="col-12 col-lg-10 mx-auto justify-content-center d-flex"><img class="mx-auto" src="/assets/img/logo-inv_grad.svg" style="max-width: 400px;width: 80vw;"></div>
</div> </div>
</div> </div>
</header> </header>
<section class="bg-light">
<div class="container py-5">
<div class="row">
<div class="col-md-8 col-xl-6 text-center mx-auto">
<p class="fw-bold mb-2 text-primary">Featured Services</p>
<h3 class="fw-bold">Privacy-respecting alternatives to common services</h3>
<p class="text-muted">
These are just a few of our services. <a href="/services.html">View all services →</a>
</p>
</div>
</div>
<div class="row row-cols-1 row-cols-md-3 g-4 mt-2">
{% for service in featured_services %}
<div class="col">
<div class="card h-100 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center mb-3">
{% if service.icon %}
<div class="bs-icon-md bs-icon-circle bs-icon-primary me-3">{{ service.icon | icon | safe }}</div>
{% endif %}
<h5 class="card-title mb-0">{{ service.name }}</h5>
</div>
<p class="card-text">{{ service.short_description }}</p>
</div>
<div class="card-footer bg-transparent border-top-0">
<a href="{{ service.links[0].url }}" class="btn btn-primary btn-sm">Try it now</a>
{% if service.homemade %}
<a href="{{ service.homemade }}"
class="btn btn-outline-secondary btn-sm">
<small>Source Code</small>
</a>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
<div class="text-center mt-4">
<a href="/services.html" class="btn btn-outline-primary">View All Services</a>
</div>
</div>
</section>
<section class="bg-white"> <section class="bg-white">
<div class="container bg-white py-5"> <div class="container bg-white py-5">
<div class="row"> <div class="row">
<div class="col-md-8 col-xl-6 text-center mx-auto"> <div class="col-md-8 col-xl-6 text-center mx-auto">
<p class="fw-bold mb-2 text-primary">Our Mission</p> <p class="fw-bold mb-2 text-primary">Our self-hosted Services</p>
<h3 class="fw-bold"> <h3 class="fw-bold">
Private.coffee is a non-profit association dedicated to supporting privacy and digital sovereignty. Private.coffee provides a collection of services that respect your
privacy.
</h3> </h3>
<p class="text-muted"> </div>
We provide privacy-respecting alternatives to common services, educate users about digital privacy, </div>
and advocate for a more private and secure internet. <div class="py-5 p-lg-5">
<div
class="row row-cols-1 row-cols-md-2 row-cols-lg-3 mx-auto"
style="max-width: 1200px"
>
{% for service in services.services %} {% if not
service.exclude_from_index %}
<div class="col mb-5">
<div class="card shadow-sm">
<div class="card-body px-4 py-5 px-md-5">
{% if service.icon %}
<div
class="bs-icon-lg d-flex justify-content-center align-items-center mb-3 bs-icon"
style="top: 1rem; right: 1rem; position: absolute"
>
{{ service.icon | icon | safe }}
</div>
{% endif %}
<h5 class="fw-bold card-title">{{ service.name }}</h5>
<p class="text-muted card-text mb-4">
{{ service.long_description }}
</p> </p>
<p class="text-muted"> {% for link in service.links %} {% if link.alternatives %}
We are always looking for volunteers and contributors, and you can also support us by donating or becoming <div class="dropdown">
a member of our association. Your support enables us to provide free and open-source software solutions <div class="btn btn-primary shadow">
that respect your privacy and digital rights. <a class="main-link" href="{{ link.url }}">
</p> {{ link.name }}
<a href="/membership.html" class="btn btn-primary btn-sm">Join us</a> </a>
<div class="dropdown-toggle-area">&#9660;</div>
</div>
<div class="dropdown-content">
{% for alternative in link.alternatives %}
<a href="{{ alternative.url }}">{{ alternative.name }}</a>
{% endfor %}
</div>
</div>
{% else %}
<a
class="btn btn-primary shadow w-100 text-center"
href="{{ link.url }}"
>{{ link.name }}</a
>
{% endif %} {% endfor %}
</div>
</div>
</div>
{% endif %} {% endfor %}
</div> </div>
</div> </div>
</div> </div>
@ -105,17 +96,29 @@
<div class="card bg-primary-subtle"> <div class="card bg-primary-subtle">
<div class="card-body text-center px-4 py-5 px-md-5"> <div class="card-body text-center px-4 py-5 px-md-5">
<p class="fw-bold text-primary card-text mb-2">Join or donate</p> <p class="fw-bold text-primary card-text mb-2">Join or donate</p>
<h5 class="fw-bold card-title mb-3">Be a part of the open source community!</h5> <h5 class="fw-bold card-title mb-3">
<a class="btn btn-primary btn-sm" href="/membership.html">Learn more</a> Be a part of the open source community!
</h5>
<a class="btn btn-primary btn-sm" href="/membership.html"
>Learn more</a
>
</div> </div>
</div> </div>
</div> </div>
<div class="col mb-4"> <div class="col mb-4">
<div class="card bg-secondary-subtle"> <div class="card bg-secondary-subtle">
<div class="card-body text-center px-4 py-5 px-md-5"> <div class="card-body text-center px-4 py-5 px-md-5">
<p class="fw-bold text-secondary card-text mb-2">Private Hosting</p> <p class="fw-bold text-secondary card-text mb-2">
<h5 class="fw-bold card-title mb-3">Interested in Hosting Services?</h5> Private Hosting
<a class="btn btn-secondary btn-sm" href="mailto:support@private.coffee">Get in touch</a> </p>
<h5 class="fw-bold card-title mb-3">
Interested in Hosting Services?
</h5>
<a
class="btn btn-secondary btn-sm"
href="mailto:support@private.coffee"
>Get in touch</a
>
</div> </div>
</div> </div>
</div> </div>
@ -132,34 +135,46 @@
</div> </div>
</div> </div>
<div class="row d-flex justify-content-center"> <div class="row d-flex justify-content-center">
<div class="col-md-4 col-xl-4 d-flex justify-content-center justify-content-xl-start"> <div
<div class="d-flex flex-wrap flex-md-column justify-content-md-start align-items-md-start h-100"> class="col-md-4 col-xl-4 d-flex justify-content-center justify-content-xl-start"
>
<div
class="d-flex flex-wrap flex-md-column justify-content-md-start align-items-md-start h-100"
>
<div class="d-flex align-items-center p-3"> <div class="d-flex align-items-center p-3">
<div class="bs-icon-md bs-icon-circle bs-icon-primary shadow d-flex flex-shrink-0 justify-content-center align-items-center d-inline-block bs-icon bs-icon-md"> <div
class="bs-icon-md bs-icon-circle bs-icon-primary shadow d-flex flex-shrink-0 justify-content-center align-items-center d-inline-block bs-icon bs-icon-md"
>
{{ "envelope" | icon | safe }} {{ "envelope" | icon | safe }}
</div> </div>
<div class="px-2"> <div class="px-2">
<h6 class="fw-bold mb-0">Email</h6> <h6 class="fw-bold mb-0">Email</h6>
<p class="text-muted mb-0"> <p class="text-muted mb-0">support@private.coffee</p>
<a href="mailto:support@private.coffee">support@private.coffee</a>
<small><a href="{{ relative_path }}assets/support@private.coffee.asc">[PGP]</a></small>
</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row d-flex justify-content-center"> <div class="row d-flex justify-content-center">
<div class="col-md-4 col-xl-4 d-flex justify-content-center justify-content-xl-start"> <div
<div class="d-flex flex-wrap flex-md-column justify-content-md-start align-items-md-start h-100"> class="col-md-4 col-xl-4 d-flex justify-content-center justify-content-xl-start"
>
<div
class="d-flex flex-wrap flex-md-column justify-content-md-start align-items-md-start h-100"
>
<div class="d-flex align-items-center p-3"> <div class="d-flex align-items-center p-3">
<div class="bs-icon-md bs-icon-circle bs-icon-primary shadow d-flex flex-shrink-0 justify-content-center align-items-center d-inline-block bs-icon bs-icon-md"> <div
class="bs-icon-md bs-icon-circle bs-icon-primary shadow d-flex flex-shrink-0 justify-content-center align-items-center d-inline-block bs-icon bs-icon-md"
>
{{ "matrix-logo" | icon | safe }} {{ "matrix-logo" | icon | safe }}
</div> </div>
<div class="px-2"> <div class="px-2">
<h6 class="fw-bold mb-0">Matrix</h6> <h6 class="fw-bold mb-0">Matrix</h6>
<p class="text-muted mb-0"> <p class="text-muted mb-0">
<a href="https://matrix.pcof.fi/#/#private.coffee:private.coffee">#private.coffee:private.coffee</a> <a
href="https://matrix.pcof.fi/#/#private.coffee:private.coffee"
>#private.coffee:private.coffee</a
>
</p> </p>
</div> </div>
</div> </div>
@ -168,4 +183,4 @@
</div> </div>
</div> </div>
</section> </section>
{% endblock content %} {% endblock %}

View file

@ -1,11 +1,13 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Legal Notice{% endblock title %} {% block title %}Legal Notice{% endblock %}
{% block content %} {% block content %}
<section> <section>
<div class="container py-5"> <div class="container py-5">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col"> <div class="col">
<p class="text-end special-header fancy-text-primary mb-0">Legal Notice</p> <p class="text-end special-header fancy-text-primary mb-0">
Legal Notice
</p>
</div> </div>
<div class="col"> <div class="col">
<p class="text-start mb-1" style="font-size: 1.6rem"> <p class="text-start mb-1" style="font-size: 1.6rem">
@ -16,32 +18,24 @@
</p> </p>
</div> </div>
</div> </div>
<div class="container"> <div class="container">
<p>This is a legal notice for the website private.coffee</p> <p>This is a legal notice for the website private.coffee</p>
<p>The website private.coffee is run by the following entity:</p> <p>The website private.coffee is run by the following entity:</p>
<address> <address>
Private.coffee &dash; Verein zur Förderung von Privatsphäre und Private.coffee &dash; Verein zur Förderung von Privatsphäre und
digitaler Souver&auml;nit&auml;t digitaler Souver&auml;nit&auml;t<br />
<br /> c/o Klaus-Uwe Mitterer<br />
c/o Klaus-Uwe Mitterer Gartengasse 22/7/3<br />
<br /> 8010 Graz<br />
Gartengasse 22/7/3 Austria<br />
<br />
8010 Graz
<br />
Austria
<br />
</address> </address>
<p>Central Register of Associations (ZVR) Number: 1758485319</p> <p>Central Register of Associations (ZVR) Number: 1758485319</p>
<p> <p>
Email: Email:
<a href="mailto:support@private.coffee">support@private.coffee</a> <a href="mailto:support@private.coffee">support@private.coffee</a>
(PGP key: <a href="{{ relative_path }}assets/support@private.coffee.asc">F262CF22D540CDBB90D7A82BA5F5E7AA321941FA</a>)
</p>
<p>
For security-related issues, please see <a href="https://security.private.coffee/">our security page</a>.
</p> </p>
</div> </div>
</div> </div>
</section> </section>
{% endblock content %} {% endblock %}

View file

@ -1,11 +0,0 @@
{% extends "membership.html" %}
{% block serviceinfo %}
<div class="alert alert-info">
<p>
<strong>Welcome to Private.coffee!</strong> Rallly Pro is free for all logged-in users. Upon first login, it may take up to five minutes for your account to be upgraded. If you have any issues, please contact us at <a href="mailto:support@private.coffee">support@private.coffee</a>
</p>
<p>
If you find our services useful, please consider supporting us through a donation or becoming a supporting member. This will help us keep our free services running and improve our offerings. See below for more information.
</p>
</div>
{% endblock serviceinfo %}

View file

@ -1,7 +1,4 @@
{% extends "base.html" %} {% extends "base.html" %} {% block title %}Membership / Donations{% endblock %}
{% block title %}
Membership / Donations
{% endblock title %}
{% block content %} {% block content %}
<div class="container my-5"> <div class="container my-5">
<div class="text-center mb-5"> <div class="text-center mb-5">
@ -13,10 +10,9 @@
our services and reach more people. our services and reach more people.
</p> </p>
</div> </div>
{% block serviceinfo %}
{% endblock serviceinfo %}
<div class="row"> <div class="row">
<div class="col-md-6 mb-4"> <div class="col-md-4 mb-4">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Membership</h5> <h5 class="card-title">Membership</h5>
@ -26,12 +22,17 @@
expenses. This allows us to provide services to our members and the expenses. This allows us to provide services to our members and the
public. public.
</p> </p>
<p class="card-text">Membership starts at € 5 / month!</p> <p class="card-text">
<a href="https://pcof.fi/join" class="btn btn-primary">Join us now!</a> Membership starts at € 5 / month!
</p>
<a href="https://pcof.fi/join" class="btn btn-primary">
Join us now!
</a>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6 mb-4">
<div class="col-md-4 mb-4">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Bank Donations</h5> <h5 class="card-title">Bank Donations</h5>
@ -40,39 +41,14 @@
direct donation to our bank account. Your donation will be used to direct donation to our bank account. Your donation will be used to
fund our activities and expand our services. fund our activities and expand our services.
</p> </p>
<p class="card-text"> <p class="card-text"><b>Account holder:</b> Private.coffee</p>
<b>Account holder:</b> Private.coffee <p class="card-text"><b>IBAN:</b> AT35 2081 5000 4554 0812</p>
</p> <p class="card-text"><b>BIC:</b> STSPAT2GXXX</p>
<p class="card-text">
<b>IBAN:</b> AT35 2081 5000 4554 0812
</p>
<p class="card-text">
<b>BIC:</b> STSPAT2GXXX
</p>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6 mb-4">
<div class="card shadow-sm"> <div class="col-md-4 mb-4">
<div class="card-body">
<h5 class="card-title">Credit card, PayPal</h5>
<p class="card-text">
If you prefer to use a credit card, PayPal, or other payment methods,
you can use one of the links below. At Stripe, you can use different
payment methods depending on your location, including credit cards,
Apple Pay, Google Pay, and more. At PayPal, you can use your PayPal
account or a credit card. Unfortunately, those options are not the
most privacy-friendly, but since they are convenient for many people,
we offer them as an option. You can also use both options on Ko-fi,
which also allows you to set up a recurring donation.
</p>
<a href="https://donate.stripe.com/3cs7tfdmp2wq7II9AA" class="btn btn-primary">Donate via Stripe</a>
<a href="https://paypal.me/privatecoffee" class="btn btn-primary">Donate via PayPal</a>
<a href="https://ko-fi.com/privatecoffee" class="btn btn-primary">Donate via Ko-fi</a>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Crypto Donations</h5> <h5 class="card-title">Crypto Donations</h5>
@ -86,7 +62,9 @@
</p> </p>
<p class="card-text"> <p class="card-text">
<b>Monero (XMR):</b> <b>Monero (XMR):</b>
<code>487Ny4iBk2pKGJwjyYrumFD8xFmrS6jCSXNA8e5EvVJ49GyS54CRDVz514MBnXgNT1EioKYiagHs33sLzUAFj8i3Pwg3AMS</code> <code
>487Ny4iBk2pKGJwjyYrumFD8xFmrS6jCSXNA8e5EvVJ49GyS54CRDVz514MBnXgNT1EioKYiagHs33sLzUAFj8i3Pwg3AMS</code
>
</p> </p>
<p class="card-text"> <p class="card-text">
<b>Ethereum (ETH):</b> <code>Coming soon&trade;</code> <b>Ethereum (ETH):</b> <code>Coming soon&trade;</code>
@ -99,6 +77,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card shadow-sm mt-4"> <div class="card shadow-sm mt-4">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Transparency Report for {{ finances_period }}</h5> <h5 class="card-title">Transparency Report for {{ finances_period }}</h5>
@ -115,22 +94,30 @@
</p> </p>
</div> </div>
</div> </div>
<div class="card shadow-sm mt-4"> <div class="card shadow-sm mt-4">
<div class="card-body"> <div class="card-body">
<h5>Central Register of Associations (ZVR) Number: 1758485319</h5> <h5>Central Register of Associations (ZVR) Number: 1758485319</h5>
<p> <p>
Our statutes can be found in our Git Our statutes can be found in our Git
<a href="https://git.private.coffee/PrivateCoffee/statuten">in German (legally binding)</a> <a href="https://git.private.coffee/PrivateCoffee/statuten"
>in German (legally binding)</a
>
and and
<a href="https://git.private.coffee/PrivateCoffee/Statuten/src/branch/english">in English</a>. <a
href="https://git.private.coffee/PrivateCoffee/Statuten/src/branch/english"
>in English</a
>.
</p> </p>
<p class="contact-info" id="contact-info"> <p class="contact-info" id="contact-info">
Interested in joining the association? Reach out via Interested in joining the association? Reach out via
<a href="mailto:support@private.coffee">email</a> or <a href="mailto:support@private.coffee">email</a> or
<a href="https://matrix.pcof.fi/#/#private.coffee:private.coffee">Matrix</a> <a href="https://matrix.pcof.fi/#/#private.coffee:private.coffee"
>Matrix</a
>
for more information. for more information.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
{% endblock content %} {% endblock %}

View file

@ -1,5 +1,4 @@
{% extends "base.html" %} {% extends "base.html" %} {% block title %}Privacy Policy{% endblock %}
{% block title %}Privacy Policy{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="text-center mb-5"> <div class="text-center mb-5">
@ -24,16 +23,11 @@
<p>The data controller for private.coffee is:</p> <p>The data controller for private.coffee is:</p>
<address> <address>
Private.coffee &dash; Verein zur Förderung von Privatsphäre und digitaler Private.coffee &dash; Verein zur Förderung von Privatsphäre und digitaler
Souver&auml;nit&auml;t Souver&auml;nit&auml;t<br />
<br /> c/o Klaus-Uwe Mitterer<br />
c/o Klaus-Uwe Mitterer Gartengasse 22/7/3<br />
<br /> 8010 Graz<br />
Gartengasse 22/7/3 Austria<br />
<br />
8010 Graz
<br />
Austria
<br />
</address> </address>
<p>Central Register of Associations (ZVR) Number: 1758485319</p> <p>Central Register of Associations (ZVR) Number: 1758485319</p>
<p> <p>
@ -77,7 +71,9 @@
<li>Providing support to you</li> <li>Providing support to you</li>
<li>Processing donations and memberships</li> <li>Processing donations and memberships</li>
</ul> </ul>
<p>All of those are what is called "legitimate interests" in legal speak.</p> <p>
All of those are what is called "legitimate interests" in legal speak.
</p>
<p> <p>
We do not use your data for any other purposes, including marketing, We do not use your data for any other purposes, including marketing,
advertising, or tracking, we do not share your data with third parties advertising, or tracking, we do not share your data with third parties
@ -157,7 +153,10 @@
If you are not satisfied with our response, or believe that we are If you are not satisfied with our response, or believe that we are
processing your data in a way that is not compliant with the law, you have processing your data in a way that is not compliant with the law, you have
the right to lodge a complaint with the supervisory authority in your the right to lodge a complaint with the supervisory authority in your
country. In Austria, this is the Austrian Data Protection Authority (<a href="https://www.dsb.gv.at/">https://www.dsb.gv.at/</a>). However, we like to think of ourselves as nice people and will try to country. In Austria, this is the Austrian Data Protection Authority (<a
href="https://www.dsb.gv.at/"
>https://www.dsb.gv.at/</a
>). However, we like to think of ourselves as nice people and will try to
help you out directly, and are always open to suggestions for improvement, help you out directly, and are always open to suggestions for improvement,
so please do get in touch with us if you have any questions or concerns. so please do get in touch with us if you have any questions or concerns.
</p> </p>

View file

@ -1,5 +1,6 @@
<div class="alert alert-warning text-center" role="alert"> <div class="alert alert-warning text-center" role="alert">
This is a development version of the Private.coffee website. For the live This is a development version of the Private.coffee website. For the live
version, please visit version, please visit
<a href="https://private.coffee" class="alert-link">https://private.coffee</a>. <a href="https://private.coffee" class="alert-link">https://private.coffee</a
>.
</div> </div>

Some files were not shown because too many files have changed in this diff Show more