Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c6e807653 | ||
|
|
0725d3eb1a | ||
|
|
bb8fd62041 | ||
|
|
1745ea9e93 | ||
|
|
2f1bb6deb5 | ||
|
|
40d5b54055 | ||
|
|
e2934f84b8 | ||
|
|
a4069d27e4 | ||
|
|
b33a72ced0 | ||
|
|
dde33d0907 | ||
|
|
fbf3dbe33e | ||
|
|
21785ed493 | ||
|
|
557fa92e1f | ||
|
|
9e730bbcfd | ||
|
|
05593346fc | ||
|
|
7bac8b4011 | ||
|
|
03b124f295 | ||
|
|
8f1d545ab4 | ||
|
|
f5cfa74dde | ||
|
|
d3d2421ac3 | ||
|
|
428398820c | ||
|
|
3ad2c47bb3 | ||
|
|
b9c7791f95 | ||
|
|
35c513d08b | ||
|
|
1a264905ea | ||
|
|
933720125a | ||
|
|
07056bd10b | ||
|
|
34625ceead | ||
|
|
a4197cc050 |
@@ -6,7 +6,7 @@ This is the static website for Protospace, a non-profit makerspace in Calgary, A
|
||||
|
||||
## Source of truth
|
||||
|
||||
- Edit the HTML, CSS, and metadata files directly.
|
||||
- Edit the HTML, CSS, and metadata files directly under `public/`.
|
||||
- Do not create or reintroduce a static-site generator.
|
||||
- Do not add a build script that overwrites manually maintained pages.
|
||||
- There is intentionally no `build.js` or equivalent generator.
|
||||
@@ -14,9 +14,10 @@ This is the static website for Protospace, a non-profit makerspace in Calgary, A
|
||||
|
||||
## Page structure
|
||||
|
||||
- Every page must be an `index.html` inside its own route directory:
|
||||
- Every page must be an `index.html` inside its own route directory under `public/`:
|
||||
- `/index.html`
|
||||
- `/the-space/index.html`
|
||||
- `/tools/index.html`
|
||||
- `/join-us/index.html`
|
||||
- `/contact/index.html`
|
||||
- `/wood-shop/index.html`
|
||||
@@ -66,7 +67,7 @@ Keep source code pleasant for humans to read and edit.
|
||||
|
||||
When adding or editing a page, match the existing structure:
|
||||
|
||||
- Header with Protospace logo and links to About, Membership, Wiki, Portal, and Contact
|
||||
- Header with Protospace logo and links to Spaces, Tools, Membership, Contact, Portal, and Wiki
|
||||
- Main page content with one meaningful `<h1>`
|
||||
- Sidebar containing the current open-house information, address, parking details, map iframe, and search control where applicable
|
||||
- Footer containing YouTube, FAQ, and Facebook links
|
||||
@@ -82,7 +83,7 @@ If this information changes, update the sidebar, relevant page copy, JSON-LD, an
|
||||
|
||||
## Assets
|
||||
|
||||
- Store local images in `/assets/`.
|
||||
- Store local images in `public/assets/`.
|
||||
- Use descriptive filenames rather than WordPress upload names such as `photo_6_2023-06-06_16-39-08.jpg`.
|
||||
- Remove unused downloaded image variants when it is safe to do so.
|
||||
- Update every reference when renaming or removing an asset.
|
||||
@@ -170,6 +171,7 @@ Before finishing an HTML change:
|
||||
- Run structural validation when available, for example:
|
||||
|
||||
```bash
|
||||
cd public/
|
||||
npx html-validate \
|
||||
--rule doctype-style:off \
|
||||
--rule void-style:off \
|
||||
@@ -183,6 +185,7 @@ Before finishing an HTML change:
|
||||
- Start a local static server and smoke-test every route:
|
||||
|
||||
```bash
|
||||
cd public/
|
||||
python3 -m http.server 8765
|
||||
```
|
||||
|
||||
|
||||
@@ -19,9 +19,10 @@ The Protospace WordPress site has been replaced with static HTML and CSS pages.
|
||||
git checkout -b update-page
|
||||
```
|
||||
|
||||
4. Edit the HTML pages, `styles.css`, or files in `assets/`. Preview the site locally with any static web server, for example:
|
||||
4. Edit the HTML pages, `public/styles.css`, or files in `public/assets/`. Preview the site locally from the public directory with any static web server, for example:
|
||||
|
||||
```bash
|
||||
cd public/
|
||||
python3 -m http.server
|
||||
```
|
||||
|
||||
@@ -33,19 +34,23 @@ The Protospace WordPress site has been replaced with static HTML and CSS pages.
|
||||
git push -u origin update-page
|
||||
```
|
||||
|
||||
6. Go to your fork on GitHub and click **Compare & pull request**. Set the base repository to `Protospace/main-website`, choose the appropriate base branch, describe what changed, and submit the pull request.
|
||||
6. Go to your fork on GitHub and click **Compare & pull request**. Set the base repository to `Protospace/main-website`, choose `master` as the base branch, describe what changed, and submit the pull request.
|
||||
|
||||
Maintainers will review the pull request, request changes if needed, and merge it when it is ready. After your pull request is merged, you can sync your fork before starting another change:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git checkout master
|
||||
git fetch upstream
|
||||
git reset --hard upstream/main
|
||||
git push origin main --force-with-lease
|
||||
git reset --hard upstream/master
|
||||
git push origin master --force-with-lease
|
||||
```
|
||||
|
||||
There is no build step. Edit the generated HTML and CSS files directly. Keep the existing responsive layout, local asset paths, accessibility text, metadata, and external Wiki/Portal links intact.
|
||||
|
||||
## Deploying changes
|
||||
|
||||
The production server will automatically pull changes from this repo every 5 minutes.
|
||||
|
||||
## Using a coding agent
|
||||
|
||||
A coding agent can be helpful for larger content, layout, accessibility, and consistency changes. Give it a focused task and ask it to inspect the existing files before editing. Follow the repository instructions in `AGENTS.md` when present, and review the resulting diff before opening a pull request.
|
||||
|
||||
|
Before Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 265 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 308 KiB |
|
Before Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 307 KiB |
|
Before Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 211 KiB |
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export the Protospace Wiki tool catalog and its thumbnails.
|
||||
|
||||
Usage:
|
||||
python3 export_tools.py
|
||||
|
||||
The wiki uses a small JavaScript challenge. The equivalent cookie is sent on
|
||||
both the page request and each thumbnail request.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
from urllib.parse import urljoin, urlparse
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
WIKI_URL = "https://wiki.protospace.ca/Tools_we_have"
|
||||
WIKI_ROOT = "https://wiki.protospace.ca"
|
||||
COOKIE = "human_check=verified"
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PUBLIC_ROOT = ROOT / "public"
|
||||
OUTPUT = PUBLIC_ROOT / "tools.json"
|
||||
IMAGE_DIR = PUBLIC_ROOT / "assets" / "tools"
|
||||
|
||||
|
||||
class ToolsParser(HTMLParser):
|
||||
"""Extract ordered category/gallery data from the MediaWiki HTML."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(convert_charrefs=True)
|
||||
self.category: str | None = None
|
||||
self.in_h2 = False
|
||||
self.h2_text: list[str] = []
|
||||
self.item: dict | None = None
|
||||
self.item_depth = 0
|
||||
self.anchor: dict | None = None
|
||||
self.items: list[dict] = []
|
||||
|
||||
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
||||
attrs_dict = dict(attrs)
|
||||
if tag == "h2":
|
||||
self.in_h2 = True
|
||||
self.h2_text = []
|
||||
elif tag == "li" and "gallerybox" in (attrs_dict.get("class") or "").split():
|
||||
self.item = {"category": self.category, "anchors": [], "src": None}
|
||||
self.item_depth = 1
|
||||
elif self.item is not None and tag == "li":
|
||||
self.item_depth += 1
|
||||
elif self.item is not None and tag == "a":
|
||||
self.anchor = {"href": attrs_dict.get("href"), "text": []}
|
||||
elif self.item is not None and tag == "img" and self.item["src"] is None:
|
||||
self.item["src"] = attrs_dict.get("src")
|
||||
|
||||
def handle_startendtag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
||||
self.handle_starttag(tag, attrs)
|
||||
self.handle_endtag(tag)
|
||||
|
||||
def handle_endtag(self, tag: str) -> None:
|
||||
if tag == "h2" and self.in_h2:
|
||||
category = " ".join("".join(self.h2_text).split())
|
||||
if category and category.lower() != "contents":
|
||||
self.category = category
|
||||
self.in_h2 = False
|
||||
elif tag == "a" and self.anchor is not None:
|
||||
self.anchor["text"] = " ".join("".join(self.anchor["text"]).split())
|
||||
if self.item is not None:
|
||||
self.item["anchors"].append(self.anchor)
|
||||
self.anchor = None
|
||||
elif tag == "li" and self.item is not None:
|
||||
self.item_depth -= 1
|
||||
if self.item_depth == 0:
|
||||
if self.item["category"] and self.item["category"].lower() != "removed equipment":
|
||||
self.items.append(self.item)
|
||||
self.item = None
|
||||
|
||||
def handle_data(self, data: str) -> None:
|
||||
if self.in_h2:
|
||||
self.h2_text.append(data)
|
||||
if self.anchor is not None:
|
||||
self.anchor["text"].append(data)
|
||||
|
||||
|
||||
def fetch(url: str) -> tuple[bytes, str]:
|
||||
request = Request(url, headers={"Cookie": COOKIE, "User-Agent": "Protospace tools exporter/1.0"})
|
||||
with urlopen(request, timeout=30) as response:
|
||||
content_type = response.headers.get_content_type()
|
||||
return response.read(), content_type
|
||||
|
||||
|
||||
def image_filename(src: str) -> str:
|
||||
parts = [part for part in urlparse(src).path.split("/") if part]
|
||||
# MediaWiki thumbnails end with e.g. 120px-182.jpg; the preceding part is
|
||||
# the original filename and is stable even when thumbnail dimensions change.
|
||||
if len(parts) >= 2:
|
||||
return parts[-2]
|
||||
return Path(parts[-1]).name
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
page_bytes, content_type = fetch(WIKI_URL)
|
||||
except Exception as exc:
|
||||
print(f"Could not fetch {WIKI_URL}: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
if content_type != "text/html" or b"Verifying you are human" in page_bytes:
|
||||
print("The wiki returned the human-verification page; check the cookie.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
parser = ToolsParser()
|
||||
parser.feed(page_bytes.decode("utf-8", errors="replace"))
|
||||
IMAGE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
exported: list[dict[str, str]] = []
|
||||
skipped = 0
|
||||
|
||||
for raw in parser.items:
|
||||
anchors = raw["anchors"]
|
||||
name_anchor = next((a for a in anchors if "_ID:" in (a["href"] or "") or " ID:" in a["text"]), None)
|
||||
name = name_anchor["text"] if name_anchor else ""
|
||||
id_match = re.search(r"\bID:(\d+)\s*$", name)
|
||||
photo_src = raw["src"]
|
||||
if not name or not id_match or not photo_src:
|
||||
skipped += 1
|
||||
continue
|
||||
photo_url = urljoin(WIKI_ROOT, photo_src)
|
||||
source_filename = image_filename(photo_url)
|
||||
filename = f"{id_match.group(1)}{Path(source_filename).suffix.lower()}"
|
||||
destination = IMAGE_DIR / filename
|
||||
try:
|
||||
image_bytes, image_type = fetch(photo_url)
|
||||
if not image_type.startswith("image/") or not image_bytes:
|
||||
raise ValueError(f"response was {image_type}, not an image")
|
||||
destination.write_bytes(image_bytes)
|
||||
except Exception as exc:
|
||||
print(f"Skipping {name}: broken photo ({exc})", file=sys.stderr)
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
link = urljoin(WIKI_ROOT, name_anchor["href"])
|
||||
exported.append(
|
||||
{
|
||||
"ID": id_match.group(1),
|
||||
"name": re.sub(r"\s*ID:\d+\s*$", "", name).strip(),
|
||||
"photo file": f"/assets/tools/{filename}",
|
||||
"link": link,
|
||||
"category": raw["category"],
|
||||
}
|
||||
)
|
||||
|
||||
OUTPUT.write_text(json.dumps(exported, ensure_ascii=False, indent=2) + "\n")
|
||||
print(f"Exported {len(exported)} tools to {OUTPUT}")
|
||||
print(f"Skipped {skipped} tools without valid metadata or photos")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -11,7 +11,7 @@
|
||||
<meta property="og:description" content="Explore Protospace’s 3D printer village in Calgary, with filament and resin printers available to trained members." />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://protospace.ca/3d-printers/" />
|
||||
<meta property="og:image" content="https://protospace.ca/assets/3d-printers-1-1024x768.jpg" />
|
||||
<meta property="og:image" content="https://protospace.ca/assets/3d-printers-1-800.jpg" />
|
||||
<meta property="og:site_name" content="Protospace" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<title>3D Printers – Protospace</title>
|
||||
@@ -66,13 +66,14 @@
|
||||
<body>
|
||||
<header>
|
||||
<div class="nav-wrap">
|
||||
<a class="brand" href="/"><img width="360" height="70" src="/assets/logo.png" alt="Protospace" /></a>
|
||||
<a class="brand" href="/"><img width="360" height="70" src="/assets/logo-white.svg" alt="Protospace" /></a>
|
||||
<nav class="nav">
|
||||
<a href="/the-space/">About</a>
|
||||
<a href="/the-space/">Spaces</a>
|
||||
<a href="/tools/">Tools</a>
|
||||
<a href="/join-us/">Membership</a>
|
||||
<a href="https://wiki.protospace.ca">Wiki</a>
|
||||
<a href="https://my.protospace.ca">Portal</a>
|
||||
<a href="/contact/">Contact</a>
|
||||
<a href="https://my.protospace.ca" class="external">Portal</a>
|
||||
<a href="https://wiki.protospace.ca" class="external">Wiki</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
@@ -83,26 +84,24 @@
|
||||
<div class="entry">
|
||||
<figure>
|
||||
<picture>
|
||||
<source type="image/webp" srcset="/assets/3d-printers-1-1024x768.webp 1024w, /assets/3d-printers-1.webp 1280w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" />
|
||||
<img width="1024" height="768" src="/assets/3d-printers-1-1024x768.jpg" srcset="/assets/3d-printers-1-1024x768.jpg 1024w, /assets/3d-printers-1.jpg 1280w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" alt="3D printer in the printer village" decoding="async" fetchpriority="high" />
|
||||
<source type="image/webp" srcset="/assets/3d-printers-1-400.webp 400w, /assets/3d-printers-1-800.webp 800w, /assets/3d-printers-1-1600.webp 1600w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" />
|
||||
<img width="800" height="602" src="/assets/3d-printers-1-800.jpg" srcset="/assets/3d-printers-1-400.jpg 400w, /assets/3d-printers-1-800.jpg 800w, /assets/3d-printers-1-1600.jpg 1600w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" alt="Resin 3D printers and printing supplies" fetchpriority="high" decoding="async" />
|
||||
</picture>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<picture>
|
||||
<source type="image/webp" srcset="/assets/3d-printers-2-1024x768.webp 1024w, /assets/3d-printers-2.webp 1280w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" />
|
||||
<img width="1024" height="768" src="/assets/3d-printers-2-1024x768.jpg" srcset="/assets/3d-printers-2-1024x768.jpg 1024w, /assets/3d-printers-2.jpg 1280w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" alt="3D printer equipment" loading="lazy" decoding="async" />
|
||||
<source type="image/webp" srcset="/assets/3d-printers-2-400.webp 400w, /assets/3d-printers-2-800.webp 800w, /assets/3d-printers-2-1600.webp 1600w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" />
|
||||
<img width="800" height="602" src="/assets/3d-printers-2-800.jpg" srcset="/assets/3d-printers-2-400.jpg 400w, /assets/3d-printers-2-800.jpg 800w, /assets/3d-printers-2-1600.jpg 1600w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" alt="3D printer village with filament and resin printers" loading="lazy" decoding="async" />
|
||||
</picture>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<picture>
|
||||
<source type="image/webp" srcset="/assets/3d-printers-3-1024x768.webp 1024w, /assets/3d-printers-3.webp 1280w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" />
|
||||
<img width="1024" height="768" src="/assets/3d-printers-3-1024x768.jpg" srcset="/assets/3d-printers-3-1024x768.jpg 1024w, /assets/3d-printers-3.jpg 1280w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" alt="3D printer in operation" loading="lazy" decoding="async" />
|
||||
<source type="image/webp" srcset="/assets/3d-printers-3-400.webp 400w, /assets/3d-printers-3-800.webp 800w, /assets/3d-printers-3-1600.webp 1600w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" />
|
||||
<img width="800" height="602" src="/assets/3d-printers-3-800.jpg" srcset="/assets/3d-printers-3-400.jpg 400w, /assets/3d-printers-3-800.jpg 800w, /assets/3d-printers-3-1600.jpg 1600w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" alt="Desktop 3D printers and print supplies" loading="lazy" decoding="async" />
|
||||
</picture>
|
||||
</figure>
|
||||
|
||||
<p></p>
|
||||
</div>
|
||||
</main>
|
||||
<aside class="sidebar">
|
||||
@@ -112,7 +111,7 @@
|
||||
Open House every Tuesday from 7-9 pm (even holidays). Just drop in for a tour!
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://youtu.be/B9w5G6Rn99A">Watch the Protospace mini tour video</a>
|
||||
<a href="https://youtu.be/B9w5G6Rn99A" class="external">Watch the Protospace mini tour video</a>
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
@@ -131,11 +130,11 @@
|
||||
<footer>
|
||||
<div class="footer-inner">
|
||||
<div class="footer-links">
|
||||
<div><a href="https://www.youtube.com/user/calgaryprotospace/playlists">YouTube</a></div>
|
||||
<div><a href="https://wiki.protospace.ca/Faq">F.A.Q.</a></div>
|
||||
<div><a href="https://www.facebook.com/CalgaryProtospace/">Facebook</a></div>
|
||||
<div><a href="https://www.youtube.com/user/calgaryprotospace/playlists" class="external">YouTube</a></div>
|
||||
<div><a href="https://github.com/Protospace" class="external">GitHub</a></div>
|
||||
<div><a href="https://www.facebook.com/CalgaryProtospace/" class="external">Facebook</a></div>
|
||||
</div>
|
||||
<p>PROTOSPACE 2026 · All rights reserved</p>
|
||||
<p>© PROTOSPACE 2026 · <a href="https://github.com/Protospace/main-website" class="external">Source Code</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
After Width: | Height: | Size: 461 KiB |
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 439 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 130 KiB |