Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 13 additions & 0 deletions site/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
# environment
.env
.env.production
# macOS
.DS_Store
31 changes: 31 additions & 0 deletions site/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# PawWork site

Download landing page for PawWork. Built with [Astro](https://astro.build/) (plain CSS, no UI framework). Deploys as a static site to Cloudflare Pages, independent of the desktop app build.

## Develop

```sh
bun install
bun run dev # http://localhost:4321
bun run build # outputs to dist/
bun run preview # serve the production build locally
```

## Structure

```
src/
pages/index.astro page markup; English first paint + client-side CN/EN switch
layouts/Base.astro <head>, SEO tags, anti-flash theme script
styles/global.css all styling; light/dark via [data-theme], CN/EN via [data-lang]
i18n.ts EN/CN copy dictionary (single source of truth)
config.ts download links and repo URLs
public/
app-icon.svg favicon + brand mark
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

## Notes

- **Language**: first paint renders English for basic SEO; the client switches to Chinese based on browser language or the EN/中 toggle. Choice persists in `localStorage`. Per-language routes for SEO are deferred.
- **Download links**: `config.ts` currently points every button at the GitHub Releases page. Swap in China-hosted direct links (R2 / COS) once the updater fallback (issue #219) lands.
- **OG image**: `Base.astro` uses the app icon as a placeholder; replace with a dedicated 1200×630 share image.
8 changes: 8 additions & 0 deletions site/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from "astro/config";

// Landing page; deploys as a static site to Cloudflare Pages, decoupled from the
// desktop app build. `site` provides the absolute origin for canonical / og:url;
// the production domain may change once registration is sorted out.
export default defineConfig({
site: "https://pawwork.ai",
});
669 changes: 669 additions & 0 deletions site/bun.lock

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions site/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@pawwork/site",
"type": "module",
"version": "0.0.0",
"private": true,
"description": "PawWork download landing page",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"check": "astro check"
},
"dependencies": {
"astro": "6.4.2"
}
}
16 changes: 16 additions & 0 deletions site/public/app-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions site/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Site-level constants. Download links currently point at the GitHub Releases
// page, where users pick the installer themselves. Once China-hosted storage
// (R2 / COS) and the updater fallback (issue #219) land, swap mac / macIntel /
// win for the per-platform direct links — nothing else on the page changes.

export const REPO_URL = "https://github.com/Astro-Han/pawwork";
export const RELEASES_URL = `${REPO_URL}/releases/latest`;

export const DOWNLOAD = {
mac: RELEASES_URL,
macIntel: RELEASES_URL,
win: RELEASES_URL,
};
73 changes: 73 additions & 0 deletions site/src/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// EN/CN copy dictionary: single source of truth.
// The server renders EN on first paint (basic SEO); the client swaps to the
// chosen language. Values carry a few inline tags (<b> <span> <u> <br>) and are
// injected as HTML.

export type Lang = "en" | "cn";

export type Dict = Record<string, string>;

export const I18N: Record<Lang, Dict> = {
en: {
brand: "PawWork",
"nav.feat": "What it does",
tag: "Open-source · Free to use",
h1: 'Real work, done on your <span class="o">desktop</span>',
Comment thread
Astro-Han marked this conversation as resolved.
sub: "<b>No terminal. No API key. No paid plan.</b> Open PawWork, choose a folder, and ask in plain language.",
"dl.mac.t": "Download for macOS",
"dl.mac.s": "Apple Silicon",
"dl.intel.t": "macOS",
"dl.intel.s": "Intel",
"dl.win.t": "Windows",
"dl.win.s": "x64",
gh2: "On GitHub? <u>Grab the latest release →</u>",
wnote:
'<b>Windows:</b> If SmartScreen shows a warning on first launch, click "More info" → "Run anyway". The macOS build is signed and notarized — just open and go.',
shotnote: "Illustration, not a screenshot",
"mock.title": "PawWork — new task",
"mock.you": "Turn these 12 invoices into a spreadsheet I can review.",
"mock.ch": "Working on it…",
"mock.s1": "Read 12 PDFs",
"mock.s2": "Extracted vendor, date, total",
"mock.s3": "Building spreadsheet…",
"mock.rd": "ready to review",
"cap1.h": "Documents & data",
"cap1.p": "Extract invoice fields into a spreadsheet, generate a CSV summary, merge multiple PDFs.",
"cap2.h": "Research & writing",
"cap2.p": "Search the web, compare multiple pages and compile a memo, turn rough notes into a clean draft.",
"cap3.h": "Code & technical",
"cap3.p": "Understand a codebase, review a pull request, debug issues using logs and source code.",
foot: "Apache-2.0 · Built on OpenCode",
},
cn: {
brand: "爪印",
"nav.feat": "功能",
tag: "开源 · 下载即用",
h1: '<span class="o">真能干活</span>,<br>跑在你电脑上',
Comment thread
Astro-Han marked this conversation as resolved.
sub: "<b>不用终端,不用 API key,不用付费。</b>打开爪印,选个文件夹,直接告诉它你要什么。",
"dl.mac.t": "下载 macOS 版",
"dl.mac.s": "Apple 芯片",
"dl.intel.t": "macOS",
"dl.intel.s": "Intel",
"dl.win.t": "Windows",
"dl.win.s": "x64",
gh2: "有 GitHub?<u>去 Releases 下最新版 →</u>",
wnote:
"<b>Windows 用户</b>首次打开时如果弹出 SmartScreen 提示,点「更多信息」→「仍要运行」。macOS 版已签名公证,不会出现此提示。",
shotnote: "示意,非实拍",
"mock.title": "爪印 — 新任务",
"mock.you": "帮我把这 12 张发票整理成一张表格,方便逐笔核对。",
"mock.ch": "正在处理…",
"mock.s1": "读完 12 个 PDF",
"mock.s2": "抽出供应商、日期、金额",
"mock.s3": "正在生成表格…",
"mock.rd": "待核对",
"cap1.h": "文档与数据",
"cap1.p": "从发票提取信息填入表格、为 CSV 生成摘要、合并多个 PDF。",
"cap2.h": "研究与写作",
"cap2.p": "上网查资料、对比多篇网页整理成备忘、把零散笔记写成一篇稿子。",
"cap3.h": "代码与技术",
"cap3.p": "理解一个项目、review 他人的 PR、根据日志和源码定位错误。",
foot: "Apache-2.0 · 基于 OpenCode",
},
};
57 changes: 57 additions & 0 deletions site/src/layouts/Base.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
// Page shell: basic SEO tags in <head> plus the anti-flash theme script.
// Language switching happens on the client; first paint renders English.
import "../styles/global.css";

interface Props {
title: string;
description: string;
image?: string;
}

const { title, description, image = "/app-icon.svg" } = Astro.props;
const canonical = new URL(Astro.url.pathname, Astro.site).href;
const ogImage = new URL(image, Astro.site).href;
---

<!doctype html>
<html lang="en" data-theme="light" data-lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/app-icon.svg" />
<link rel="canonical" href={canonical} />
<title>{title}</title>
<meta name="description" content={description} />
<meta name="theme-color" content="#ff5910" />

<meta property="og:type" content="website" />
<meta property="og:site_name" content="PawWork" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
{/* TODO: replace with a dedicated 1200×630 share image; app icon is a placeholder */}
<meta property="og:image" content={ogImage} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImage} />

{/* Set the theme before first paint so dark-mode users don't see a light flash */}
<script is:inline>
(function () {
try {
var qs = new URLSearchParams(location.search);
var stored = localStorage.getItem("pw-theme");
var mq = window.matchMedia("(prefers-color-scheme: dark)");
var theme = qs.get("theme") || stored || (mq.matches ? "dark" : "light");
if (theme !== "dark" && theme !== "light") theme = "light";
document.documentElement.setAttribute("data-theme", theme);
} catch (e) {}
})();
</script>
</head>
<body>
<slot />
</body>
</html>
150 changes: 150 additions & 0 deletions site/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
---
import Base from "../layouts/Base.astro";
import { I18N } from "../i18n";
import { DOWNLOAD, REPO_URL, RELEASES_URL } from "../config";

// First paint renders English (basic SEO); the client then switches by browser
// language or the user's toggle.
const t = I18N.en;

const seoTitle = "PawWork — Real work, done on your desktop";
const seoDesc =
"Free, open-source AI desktop app for macOS and Windows. No terminal, no API key, no paid plan — open PawWork, choose a folder, and ask in plain language.";
Comment thread
Astro-Han marked this conversation as resolved.
---

<Base title={seoTitle} description={seoDesc}>
<svg width="0" height="0" style="position:absolute"
><defs>
<g id="gh"
><path
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0016 8c0-4.42-3.58-8-8-8z"
></path></g
>
<g id="apple"
><path
d="M16.36 12.78c-.02-2.05 1.67-3.03 1.75-3.08-.95-1.4-2.44-1.59-2.97-1.61-1.26-.13-2.46.74-3.1.74-.64 0-1.63-.72-2.68-.7-1.38.02-2.65.8-3.36 2.03-1.43 2.49-.37 6.17 1.03 8.19.68.99 1.49 2.1 2.55 2.06 1.02-.04 1.41-.66 2.65-.66 1.23 0 1.58.66 2.66.64 1.1-.02 1.79-1 2.47-1.99.78-1.14 1.1-2.25 1.12-2.31-.02-.01-2.15-.83-2.17-3.27zM14.3 6.84c.56-.68.94-1.62.84-2.56-.81.03-1.79.54-2.37 1.22-.52.6-.98 1.56-.86 2.48.9.07 1.83-.46 2.39-1.14z"
></path></g
>
<g id="win"
><path
d="M3 5.4 10.2 4.4v6.9H3V5.4zM11.1 4.27 21 3v8.3h-9.9V4.27zM3 12.2h7.2v6.9L3 18.1v-5.9zM11.1 12.2H21V21l-9.9-1.3v-7.5z"
></path></g
>
<g id="paw"
><circle cx="24.8" cy="22" r="4.4"></circle><circle cx="39.2" cy="22" r="4.4"></circle><circle
cx="18.3"
cy="30.75"
r="3.8"></circle><circle cx="45.75" cy="30.75" r="3.8"></circle><path
d="M32 29.2C24.2 29.2 19.8 37.6 19.8 42.6c0 3.8 3.5 5.3 8.5 3.5 1.8-.7 5.6-.7 7.5 0 5 1.8 8.4.3 8.4-3.5 0-5-4.4-13.4-12.2-13.4z"
></path></g
>
<g id="ck"
><path
d="M20 6 9 17l-5-5"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"></path></g
>
</defs></svg
>

<div class="in">
<div class="bar">
<span class="mark"><img src="/app-icon.svg" alt="" /><span data-i18n="brand" set:html={t["brand"]} /></span>
<nav class="nav">
<a class="lk" href="#feat" data-i18n="nav.feat" set:html={t["nav.feat"]}></a>
<a class="lk gh" href={REPO_URL}><svg viewBox="0 0 16 16" fill="currentColor"><use href="#gh"></use></svg>GitHub</a>
<button class="toggle" id="lg" aria-label="Switch language"><span class="ll-en">EN</span><span class="ll-cn">中</span></button>
<button class="toggle" id="tg" aria-label="Switch theme">
<svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"></path></svg>
<svg class="moon" viewBox="0 0 24 24" fill="currentColor"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"></path></svg>
</button>
</nav>
</div>

<section class="hero">
<div>
<span class="tag"><span class="dot"></span><span data-i18n="tag" set:html={t["tag"]} /></span>
<h1 data-i18n="h1" set:html={t["h1"]}></h1>
<p class="sub" data-i18n="sub" set:html={t["sub"]}></p>
<div class="dlblock">
<div class="dl">
<a class="dlbtn lead" href={DOWNLOAD.mac}><svg viewBox="0 0 24 24" fill="currentColor"><use href="#apple"></use></svg><span class="t"><b data-i18n="dl.mac.t" set:html={t["dl.mac.t"]}></b><span data-i18n="dl.mac.s" set:html={t["dl.mac.s"]}></span></span></a>
<a class="dlbtn" href={DOWNLOAD.macIntel}><svg viewBox="0 0 24 24" fill="currentColor"><use href="#apple"></use></svg><span class="t"><b data-i18n="dl.intel.t" set:html={t["dl.intel.t"]}></b><span data-i18n="dl.intel.s" set:html={t["dl.intel.s"]}></span></span></a>
<a class="dlbtn" href={DOWNLOAD.win}><svg viewBox="0 0 24 24" fill="currentColor"><use href="#win"></use></svg><span class="t"><b data-i18n="dl.win.t" set:html={t["dl.win.t"]}></b><span data-i18n="dl.win.s" set:html={t["dl.win.s"]}></span></span></a>
</div>
<div class="meta"><a class="gh2" href={RELEASES_URL}><svg viewBox="0 0 16 16" fill="currentColor"><use href="#gh"></use></svg> <span data-i18n="gh2" set:html={t["gh2"]} /></a></div>
<p class="wnote" data-i18n="wnote" set:html={t["wnote"]}></p>
</div>
</div>

<div class="stage">
<span class="shotnote" data-i18n="shotnote" set:html={t["shotnote"]}></span>
<div class="mock">
<div class="top"><span class="dots"><i></i><i></i><i></i></span><span class="ttl" data-i18n="mock.title" set:html={t["mock.title"]}></span></div>
<div class="body">
<span class="you" data-i18n="mock.you" set:html={t["mock.you"]}></span>
<div class="card">
<div class="ch"><span class="sp"></span> <span data-i18n="mock.ch" set:html={t["mock.ch"]}></span></div>
<div class="step"><svg class="ck" viewBox="0 0 24 24"><use href="#ck"></use></svg> <span data-i18n="mock.s1" set:html={t["mock.s1"]}></span></div>
<div class="step"><svg class="ck" viewBox="0 0 24 24"><use href="#ck"></use></svg> <span data-i18n="mock.s2" set:html={t["mock.s2"]}></span></div>
<div class="step run"><span class="rb"></span> <span data-i18n="mock.s3" set:html={t["mock.s3"]}></span></div>
</div>
<div class="out"><svg class="x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="3" width="16" height="18" rx="2"></rect><path d="M8 9h8M8 13h8M8 17h5"></path></svg> invoices.xlsx <span class="rd" data-i18n="mock.rd" set:html={t["mock.rd"]}></span></div>
</div>
</div>
<span class="pawchip"><svg viewBox="0 0 64 64" fill="currentColor"><use href="#paw"></use></svg></span>
</div>
</section>

<section class="caps" id="feat">
<div class="cap"><span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="3" width="16" height="18" rx="2"></rect><path d="M8 8h8M8 12h8M8 16h5"></path></svg></span><h3 data-i18n="cap1.h" set:html={t["cap1.h"]}></h3><p data-i18n="cap1.p" set:html={t["cap1.p"]}></p></div>
<div class="cap"><span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"></circle><path d="M21 21l-4.3-4.3"></path></svg></span><h3 data-i18n="cap2.h" set:html={t["cap2.h"]}></h3><p data-i18n="cap2.p" set:html={t["cap2.p"]}></p></div>
<div class="cap"><span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 6l-6 6 6 6M16 6l6 6-6 6"></path></svg></span><h3 data-i18n="cap3.h" set:html={t["cap3.h"]}></h3><p data-i18n="cap3.p" set:html={t["cap3.p"]}></p></div>
</section>
</div>

<footer><div class="foot">
<span class="mark"><img src="/app-icon.svg" alt="" /><span data-i18n="brand" set:html={t["brand"]} /></span>
<span data-i18n="foot" set:html={t["foot"]}></span>
</div></footer>

<script define:vars={{ I18N }}>
const root = document.documentElement;
const tg = document.getElementById("tg");
const lg = document.getElementById("lg");
const qs = new URLSearchParams(location.search);

function applyLang(l) {
root.setAttribute("data-lang", l);
root.setAttribute("lang", l === "cn" ? "zh" : "en");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const dict = I18N[l];
document.querySelectorAll("[data-i18n]").forEach((el) => {
const key = el.getAttribute("data-i18n");
if (dict[key] != null) el.innerHTML = dict[key];
});
try {
localStorage.setItem("pw-lang", l);
Comment thread
Astro-Han marked this conversation as resolved.
} catch (e) {}
}

function applyTheme(theme) {
root.setAttribute("data-theme", theme);
try {
localStorage.setItem("pw-theme", theme);
} catch (e) {}
}

// Initial language: URL param > localStorage > browser language, fall back to English
let lang = qs.get("lang") || localStorage.getItem("pw-lang");
if (lang !== "cn" && lang !== "en") {
lang = (navigator.language || "").toLowerCase().indexOf("zh") === 0 ? "cn" : "en";
}
applyLang(lang);
Comment thread
Astro-Han marked this conversation as resolved.
Outdated

tg.addEventListener("click", () => applyTheme(root.getAttribute("data-theme") === "dark" ? "light" : "dark"));
lg.addEventListener("click", () => applyLang(root.getAttribute("data-lang") === "cn" ? "en" : "cn"));
Comment thread
Astro-Han marked this conversation as resolved.
Outdated
</script>
</Base>
Loading
Loading