No updates detected yet. GitHub releases will appear here.
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗Knitting & crochet toolkit for Hong Kong · 織冷工具箱 (plain HTML/CSS/JS on Vite).
From the project folder:
cd "/Users/gracelee/Documents/GitHub/knitflix"
npm install
npm run dev
Open:
http://localhost:5173/app/ automaticallyLegacy redirect:
http://localhost:5173/knitflix.html → /app/Site: knitflix.gracelee.me (Vercel, deploys from main).
index.html: the landing page at / (for now a redirect to /app/)app/index.html: the toolkit at /app/ — six tools, one per URL hash: #home #calc #rows #shops #wiki #refknitflix.html: redirect for the legacy URLpublic/: served as-is at the domain root (no bundling — the scripts are classic, not modules)
assets/css/tokens.css: colours, type, spacing (shared by app and landing)assets/css/app.css: app styles (components)assets/js/app.js: core app logic (navigation + hash routing, lang toggle, counter, calc, toast)assets/js/data/fibers.js, reference.js, shops.js: datasetsassets/js/features/shops.js, wiki.js, reference.js: rendering per toolassets/js/features/pattern.js: AI pattern explainer (Ref → 解讀)assets/data/shops.json: local fallback shop datasetsite.webmanifest: PWA manifest (start_url is /app/)vite.config.js: three HTML entries (/, /app/, knitflix.html), base: '/'Open http://localhost:5173/app/ in dev.
This app can load shop data from a Google Sheet via a Google Apps Script web app that returns JSON.
window.SHOPS_ENDPOINT in index.html to your Apps Script URLassets/data/shops.json if the endpoint is blank/unreachableCreate a tab named shops with a header row containing these columns:
nameZh, nameEn, area, areaZh, areaEn, addr, hrsZh, hrsEn, tel, rating, web, ig, tagsZh, tagsEn, descZh, descEn, online, status
For tagsZh / tagsEn, use | separated text like 實體店|教班|老字號.
Create a new Apps Script project, then deploy as a Web app (Execute as: Me; Who has access: Anyone).
const SHEET_NAME = 'shops';
function doGet() {
const ss = SpreadsheetApp.getActive();
const sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) {
return jsonOut({ error: `Missing sheet tab: ${SHEET_NAME}` }, 404);
}
const values = sheet.getDataRange().getValues();
if (values.length < 2) return jsonOut([], 200);
const headers = values[0].map(h => String(h || '').trim());
const rows = values.slice(1);
const data = rows
.filter(r => r.some(cell => String(cell || '').trim() !== ''))
.map(r => rowToShop(headers, r));
return jsonOut(data, 200);
}
function rowToShop(headers, row) {
const o = {};
headers.forEach((h, i) => (o[h] = row[i]));
const toStr = (v) => (v === null || v === undefined ? '' : String(v)).trim();
const toNullStr = (v) => {
const s = toStr(v);
return s ? s : null;
};
const toBool = (v) => {
const s = toStr(v).toLowerCase();
return s === 'true' || s === 'yes' || s === 'y' || s === '1';
};
const toNumOrNull = (v) => {
const s = toStr(v);
if (!s) return null;
const n = Number(s);
return Number.isFinite(n) ? n : null;
};
const toTags = (v) => {
const s = toStr(v);
return s ? s.split('|').map(t => t.trim()).filter(Boolean) : [];
};
return {
nameZh: toStr(o.nameZh),
nameEn: toStr(o.nameEn),
area: toStr(o.area),
areaZh: toStr(o.areaZh),
areaEn: toStr(o.areaEn),
addr: toStr(o.addr),
hrsZh: toStr(o.hrsZh),
hrsEn: toStr(o.hrsEn),
tel: toNullStr(o.tel),
rating: toNumOrNull(o.rating),
web: toNullStr(o.web),
ig: toNullStr(o.ig),
tagsZh: toTags(o.tagsZh),
tagsEn: toTags(o.tagsEn),
descZh: toStr(o.descZh),
descEn: toStr(o.descEn),
online: toBool(o.online),
status: toStr(o.status) || 'verify'
};
}
function jsonOut(obj, status) {
return ContentService
.createTextOutput(JSON.stringify(obj))
.setMimeType(ContentService.MimeType.JSON);
}
main and Folder: / (root)Then the page will be available at:
.../ (recommended).../knitflix.htmlPaste an English knitting or crochet pattern; an AI model explains each abbreviation and translates the pattern line by line (Traditional Chinese, or English when the app is in EN). The answer streams in as it's written, can be stopped part-way, and keeps whatever arrived if the connection drops.
public/assets/js/features/pattern.jsapi/explain-pattern.js, a Vercel Function that calls an OpenAI-compatible chat API with fetch and streams plain text back. No SDK, no extra dependencies.Defaults to Groq's free tier (no credit card) with qwen/qwen3.8-27b.
LLM_API_KEY with that key.Free-tier limits apply (see https://console.groq.com/docs/rate-limits). When they're hit, the app shows a "try again later" message.
To use another OpenAI-compatible provider, also set LLM_BASE_URL and LLM_MODEL (and LLM_REASONING_EFFORT if the model needs it). ALLOWED_ORIGINS adds extra sites allowed to call the endpoint, comma-separated.
npm run dev (Vite) doesn't run the /api function. Use the Vercel CLI instead:
cp .env.example .env.local # then paste your key into .env.local
npx vercel dev