Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Find collaborators or share an idea
url: https://github.com/orgs/THD-AI/discussions/categories/ideas
about: Meet people before you have a finished project.
- name: Membership or private questions
url: https://github.com/THD-AI/.github/blob/main/SUPPORT.md
about: Find the right contact without publishing personal information.
72 changes: 72 additions & 0 deletions .github/ISSUE_TEMPLATE/project-submission.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Share a project
description: Add a personal or university project to the THD AI showroom.
title: "[Showroom] "
labels: [showcase-submission]
body:
- type: markdown
attributes:
value: "Share work you care about. Your repository can stay where it is. A maintainer reviews submissions before they appear in the showroom. Do not include credentials, private coursework, or personal documents."
- type: input
id: name
attributes:
label: Project name
validations:
required: true
- type: input
id: repository
attributes:
label: Public GitHub repository
placeholder: https://github.com/your-name/your-project
validations:
required: true
- type: textarea
id: purpose
attributes:
label: What does it do, and why did you make it?
description: A few clear sentences are enough. Include the current status and limitations.
validations:
required: true
- type: textarea
id: creators
attributes:
label: Creators and credits
description: Name the people who worked on it, link their public profiles, and credit material you used.
validations:
required: true
- type: input
id: demo
attributes:
label: Demo, screenshots, or setup instructions
description: Give someone a way to understand or try the project.
validations:
required: true
- type: dropdown
id: category
attributes:
label: Where does it fit?
multiple: true
options:
- Campus life
- AI & ML
- Learning
validations:
required: true
- type: textarea
id: collaboration
attributes:
label: What kind of company are you looking for?
description: Feedback, contributors, a new maintainer, or simply sharing something you enjoyed making.
- type: textarea
id: context
attributes:
label: A little context
description: How does the work connect to THD? You may also explain your process or tools, including AI assistance, if useful. No proof of enrolment or private documents are needed.
- type: checkboxes
id: permission
attributes:
label: Permission to share
options:
- label: I created this project or have its creators' permission to submit it, and the image/description can be displayed with credit.
required: true
- label: I have not included credentials, private student information, or restricted university material.
required: true
10 changes: 10 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## What changes?

For a project submission, link its issue and explain the listing.

## Checks

- Project title, description, creators, status, and links are accurate.
- Image permission and source credit are recorded in `asset-sources.json` and the credits page when applicable.
- No credentials, private coursework, or private personal information are included.
- `node scripts/check.mjs` passes, and visible changes were checked in a browser.
43 changes: 43 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Check and publish showroom
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
pages: read
concurrency:
group: showroom-${{ github.ref }}
cancel-in-progress: true
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Validate project data and public site
run: node scripts/check.mjs
- name: Check browser JavaScript syntax
run: node --check site/app.js
- name: Configure GitHub Pages
if: github.event_name != 'pull_request'
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
- name: Package only the public website
if: github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: site
publish:
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
needs: check
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Publish the checked website
id: deployment
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
33 changes: 33 additions & 0 deletions .github/workflows/submissions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Welcome a project submission
on:
issues:
types: [opened]
permissions:
contents: read
issues: write
jobs:
welcome:
if: contains(github.event.issue.labels.*.name, 'showcase-submission')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const body = context.payload.issue.body || '';
const match = body.match(/https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)/);
let repositoryNote = 'Please include a public GitHub repository so people can explore the work.';
if (match) {
try {
const { data } = await github.rest.repos.get({ owner: match[1], repo: match[2] });
repositoryNote = data.private
? 'The repository is private. Please share a public project link; private material is not published here.'
: `The linked repository is publicly readable: **${data.full_name}**.`;
} catch {
repositoryNote = 'The linked repository could not be read publicly. Please check its URL and visibility.';
}
}
await github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: `Thanks for sharing your work with THD AI.\n\n${repositoryNote}\n\nA maintainer will review the description, creator credits, image permissions, project status, and a way to try it. Acceptance is recorded in a pull request updating the showroom catalogue. This reply does not publish or approve the project.\n\nYour repository can stay in your own account. In the meantime, you can meet collaborators in [Discussions](https://github.com/orgs/THD-AI/discussions/categories/ideas).`
});
27 changes: 27 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Help shape the showroom

THD AI is an independent place for people connected to Deggendorf Institute of Technology to share work, find inspiration, and collaborate in their own time. Personal work and university projects belong alongside one another. AI-assisted work is welcome; quality, care, accurate credit, and clear limitations matter.

## Share a project

Use the [project submission form](https://github.com/THD-AI/thd-ai.github.io/issues/new?template=project-submission.yml). Include a public repository, its creators, a clear description, a way to try or understand it, and what kind of collaboration you want. You can keep the repository in your own account. Organization membership is not required to submit or contribute.

The submission workflow leaves an acknowledgement and checks whether the repository is publicly readable. It does not approve or automatically publish a listing. A maintainer reviews the information and image permission, then opens a pull request to update `site/projects.json`. Keep source attribution in `asset-sources.json` and `site/credits/index.html`. Close the submission issue when the listing is published. An experimental project is welcome when it is described honestly and has a useful way in.

To update or remove your own listing, open an issue or pull request. If you need to discuss something privately, [email Alexander](mailto:alex.rudaev@outlook.com).

## Change the website

The website is buildless HTML, CSS, and JavaScript in `site/`. It uses a local JSON catalogue, with no visitor accounts or database. Search and filters run in the browser. An external Google Fonts stylesheet provides the fonts; system sans-serif fonts are the fallback.

1. Fork the repository or create a branch if you have write access.
2. Edit a focused piece of the site, add a credited project, or improve the documentation.
3. Run `node scripts/check.mjs` and `node --check site/app.js`.
4. Serve `site/` locally, for example with `python -m http.server 4173 --directory site`, and check the relevant desktop/mobile flow.
5. Open a pull request. The same validation runs in GitHub Actions. A checked merge to `main` publishes only `site/` to GitHub Pages.

Do not put private archive files or credentials in `site/`. Use text content for project metadata, not injected HTML. Keep the approved campus banner and existing project branding intact unless a change is agreed with its creators.

## Work together

Members can create and lead public and private repositories. Use [Discussions](https://github.com/orgs/THD-AI/discussions) to meet people, share a work in progress, or ask a question. Follow the [community conduct guidance](https://github.com/THD-AI/.github/blob/main/CODE_OF_CONDUCT.md), give people credit, and respect each project's licence and decisions.
25 changes: 25 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
MIT License

Copyright (c) 2026 Alexander Rudaev

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

This licence covers the website's original code and documentation. Images,
archived artwork, project banners, and the projects themselves retain their
separate licences and credits in site/credits/index.html.
7 changes: 7 additions & 0 deletions asset-sources.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"project-chexvision.png": "https://raw.githubusercontent.com/arudaev/chexvision/main/.github/banner.png",
"project-painfully-trivial.png": "https://raw.githubusercontent.com/arudaev/Painfully-Trivial/main/.github/banner.png",
"project-devablos.png": "https://raw.githubusercontent.com/THD-AI/Devablos-Project-V2/main/.github/banner.png",
"project-room.webp": "https://raw.githubusercontent.com/arudaev/THD-Room-Finder/main/website/assets/hero-day.webp",
"history-avatar-first-redesign.png": "https://github.com/THD-AI/.github/tree/main/archive/2026-10-17-first-redesign"
}
74 changes: 74 additions & 0 deletions scripts/check.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { readFile, access, readdir, stat } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import assert from 'node:assert/strict';

const root = fileURLToPath(new URL('../', import.meta.url));
const site = path.join(root, 'site');
const collection = JSON.parse(await readFile(path.join(site, 'projects.json'), 'utf8'));
const categories = new Set(['campus', 'ai', 'learning']);
const ids = new Set();
assert(Array.isArray(collection.projects) && collection.projects.length > 0, 'Provide a non-empty project collection');
function https(url, field) {
const parsed = new URL(url);
assert.equal(parsed.protocol, 'https:', `${field} must use HTTPS`);
assert(!parsed.username && !parsed.password, `${field} must not contain credentials`);
return parsed;
}
for (const project of collection.projects) {
assert(/^[a-z0-9-]+$/.test(project.id) && !ids.has(project.id), `Unique safe project ID: ${project.id}`);
ids.add(project.id);
for (const key of ['title', 'description', 'categoryLabel', 'status', 'collaboration']) {
assert(typeof project[key] === 'string' && project[key].trim(), `${project.id}: missing ${key}`);
}
assert(project.categories?.length && project.categories.every(c => categories.has(c)), `${project.id}: unknown category`);
const repository = https(project.repository, 'repository');
assert.equal(repository.hostname, 'github.com', 'Project repository must be a public GitHub link');
assert(/^\/[^/]+\/[^/]+\/?$/.test(repository.pathname), 'Use a repository URL, not a subpage');
if (project.community) assert.equal(https(project.community, 'community').hostname, 'github.com');
if (project.demo) https(project.demo, 'demo');
assert(Array.isArray(project.creators) && project.creators.length > 0, `${project.id}: credit the creators`);
for (const creator of project.creators) {
assert(creator.name?.trim(), 'Creator name required');
https(creator.url, 'creator');
}
assert(Array.isArray(project.tags) && project.tags.every(t => typeof t === 'string'), 'Project tags must be strings');
if (project.image) {
assert(project.image.startsWith('assets/') && !project.image.includes('..'), 'Project image must be a local asset');
await access(path.join(site, project.image));
assert(project.imageAlt?.trim(), 'Project image needs alt text');
}
}
async function walk(folder) {
const files = [];
for (const entry of await readdir(folder)) {
const full = path.join(folder, entry);
if ((await stat(full)).isDirectory()) files.push(...await walk(full));
else files.push(full);
}
return files;
}
const files = await walk(site);
for (const file of files.filter(f => f.endsWith('.html'))) {
const html = await readFile(file, 'utf8');
assert(/<html lang="en">/.test(html), 'Document language required');
assert(/<meta name="viewport"/.test(html), 'Responsive viewport required');
assert(/<title>[^<]+<\/title>/.test(html), 'Document title required');
for (const match of html.matchAll(/(?:src|href)="([^"]+)"/g)) {
const target = match[1];
if (/^(https:|mailto:)/.test(target)) continue;
if (target.startsWith('#')) {
assert(html.includes(`id="${target.slice(1)}"`), `${file}: missing section ${target}`);
continue;
}
const targetPath = target.split(/[?#]/)[0];
const destination = targetPath.startsWith('/') ? path.resolve(site, '.' + targetPath) : path.resolve(path.dirname(file), targetPath);
assert(destination === site || destination.startsWith(site + path.sep), `${file}: path escapes site`);
await access(destination);
if ((await stat(destination)).isDirectory()) await access(path.join(destination, 'index.html'));
}
for (const image of html.matchAll(/<img\b[^>]*>/g)) assert(/\balt="[^"]*"/.test(image[0]), 'Every image needs alt text');
}
assert(files.every(f => !/\.git|\.env|\.pem|\.key$/.test(path.relative(site, f))), 'Only public website files may be deployed');
assert(!files.some(f => /member-profile|\.github-private|first-redesign\/profile/.test(f)), 'Private archive content must not ship with the website');
console.log(`Validated ${ids.size} credited projects, ${files.filter(f => f.endsWith('.html')).length} pages, local links, image alternatives, HTTPS URLs, and deployment boundaries.`);
1 change: 1 addition & 0 deletions site/.nojekyll
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

6 changes: 6 additions & 0 deletions site/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="theme-color" content="#eeedf8"><title>Page not found – THD AI</title><link rel="stylesheet" href="/style.css?v=3"><link rel="icon" type="image/png" href="/assets/symbol.png"></head><body>
<a class="skip-link" href="#content">Skip to content</a>
<header class="site-header wrap"><a class="identity" href="/" aria-label="THD AI home"><img src="/assets/symbol.png" width="48" height="48" alt=""><span>THD <b>AI</b></span></a><nav aria-label="Main navigation"><a href="/#projects">The showroom</a><a href="/#about">The idea</a><a href="/history/">Earlier chapters</a></nav><a class="header-action" href="/#share">Share your project ↗</a></header>
<main id="content" class="history-main wrap"><p class="eyebrow">404 / A small detour</p><h1>This shelf<br>is empty.</h1><p class="lede">That page may have moved. The showroom and project collection are still here.</p><a class="button primary" href="https://thd-ai.github.io/">Back to the showroom ↗</a></main>
<footer class="site-footer wrap"><div><a class="identity" href="/"><img src="/assets/symbol.png" width="38" height="38" alt=""><span>THD <b>AI</b></span></a><p>An independent project showroom.<br>Made with care, around THD.</p></div><div class="footer-links"><a href="/#projects">The showroom</a><a href="/history/">Visual history</a><a href="/credits/">Credits & licences</a><a href="https://github.com/THD-AI">GitHub ↗</a></div><p class="footer-note">Started in 2023. Room for what comes next.<br>Independent of THD; no university endorsement.</p></footer></body></html>
Loading
Loading