# Job board with active/inactive postings

A careers page for a company. Each open role is a sub-page with a
description and an apply form. The main page lists all currently open
roles. When a role is filled, you flip a metadata flag and it
disappears from the public list — no deletion, the URL still works.

This is mechanically the same as `property-listings.md`. The
differences are cosmetic (different fields, different layout) and the
apply form (which uses the email action and therefore requires Google).

## When to use this

- Companies with a careers page
- Recruitment agencies
- Open call / submission lists
- Anything where items have an "active / closed" lifecycle

## The script

```python
import requests
import time

API = "https://api.uat-beam.page"
TOKEN = "<your-google-token>"  # MUST be Google for the apply form
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}

slug = "acme-careers"

# 1. Create the project
requests.post(f"{API}/projects", headers=H, json={
    "slug": slug,
    "context": "Careers page for Acme Corp. Lists open positions; each role is a sub-page with description and apply form.",
})
time.sleep(2)

# 2. Brand it
requests.put(f"{API}/projects/{slug}/assets/tailwind-config.js", headers=H, json={
    "content": (
        'tailwind.config = { theme: { extend: { '
        'colors: { brand: "#1e40af", accent: "#10b981" }, '
        'fontFamily: { sans: ["Inter", "sans-serif"] } '
        '} } }'
    ),
})

requests.put(f"{API}/projects/{slug}/assets/styles.css", headers=H, json={
    "content": "@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');",
})

# 3. The roles
roles = [
    {
        "slug": "senior-frontend-engineer",
        "title": "Senior Frontend Engineer",
        "department": "Engineering",
        "location": "Remote (UK)",
        "type": "Full-time",
        "salary": "£75,000 – £95,000",
        "summary": "Lead the frontend team. React, TypeScript, design system experience essential.",
    },
    {
        "slug": "product-designer",
        "title": "Product Designer",
        "department": "Design",
        "location": "London",
        "type": "Full-time",
        "salary": "£60,000 – £80,000",
        "summary": "Design new product surfaces from research through to ship. Figma fluency required.",
    },
    {
        "slug": "customer-success-manager",
        "title": "Customer Success Manager",
        "department": "Customer Success",
        "location": "Remote (Europe)",
        "type": "Full-time",
        "salary": "£50,000 – £65,000",
        "summary": "Own a portfolio of enterprise accounts and drive expansion.",
    },
]

for role in roles:
    requests.post(f"{API}/projects/{slug}/pages", headers=H, json={
        "slug": role["slug"],
        "notes": role["title"],
    })
    time.sleep(0.3)

    requests.put(
        f"{API}/projects/{slug}/pages/{role['slug']}/metadata",
        headers=H,
        json={
            "title": role["title"],
            "department": role["department"],
            "location": role["location"],
            "type": role["type"],
            "salary": role["salary"],
            "summary": role["summary"],
            "status": "active",
            "posted": "2026-04-01",
        },
    )
    time.sleep(0.3)

# 4. The main page lists all active roles, grouped by department
main_html = """<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Careers at Acme — Join us</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="/assets/tailwind-config.js"></script>
  <link rel="stylesheet" href="/assets/styles.css">
</head>
<body class="font-sans bg-stone-50 text-stone-900"
      x-data="{ roles: [], get byDept() { const g = {}; this.roles.forEach(r => { (g[r.metadata.department] = g[r.metadata.department] || []).push(r); }); return g; } }"
      x-init="
        fetch('/cross_page_metadata.json').then(r => r.json())
          .then(d => roles = d.pages.filter(p => p.metadata.status === 'active'))
      ">

  <header class="bg-brand text-white py-20 px-6 text-center">
    <h1 class="text-5xl font-bold mb-3">Build with us at Acme</h1>
    <p class="text-blue-200 text-xl max-w-2xl mx-auto">
      We're a small team building the tools we wish existed. If that
      sounds good, we'd love to hear from you.
    </p>
  </header>

  <main class="max-w-3xl mx-auto px-6 py-16">
    <p class="text-stone-500 text-sm uppercase tracking-widest mb-2">Open positions</p>
    <h2 class="text-3xl font-bold text-brand mb-12" x-text="'(' + roles.length + ' roles)'"></h2>

    <template x-for="(deptRoles, dept) in byDept" :key="dept">
      <section class="mb-12">
        <h3 class="text-sm uppercase tracking-widest text-stone-400 mb-4" x-text="dept"></h3>
        <div class="space-y-3">
          <template x-for="role in deptRoles" :key="role.slug">
            <a :href="role.url"
               class="block bg-white rounded-lg shadow-sm hover:shadow-md transition-all p-6 border-l-4 border-accent">
              <div class="flex justify-between items-start mb-2">
                <h4 class="text-xl font-bold text-brand" x-text="role.metadata.title"></h4>
                <span class="text-stone-400 text-sm" x-text="role.metadata.type"></span>
              </div>
              <p class="text-stone-600 text-sm mb-3" x-text="role.metadata.summary"></p>
              <div class="flex gap-4 text-xs text-stone-500">
                <span x-text="'📍 ' + role.metadata.location"></span>
                <span x-text="'💰 ' + role.metadata.salary"></span>
              </div>
            </a>
          </template>
        </div>
      </section>
    </template>

    <template x-if="roles.length === 0">
      <p class="text-center text-stone-500 py-16">
        No open positions right now. Check back soon, or send us an
        email anyway — we keep great CVs on file.
      </p>
    </template>
  </main>

  <footer class="text-center py-8 text-stone-400 text-sm">Acme Corp &middot; Careers</footer>

  <script src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js" defer></script>
</body>
</html>
"""

requests.put(
    f"{API}/projects/{slug}/pages/_root/files/index.html",
    headers=H,
    json={"content": main_html},
)

# 5. Each role's detail page reads its own metadata and includes
# an apply form using the email action.
# The __PAGE_SLUG__ placeholder is replaced per-role in the upload loop.
role_html = """<!DOCTYPE html>
<html lang="en">
<head>
  <base href="/__PAGE_SLUG__/">
  <meta charset="UTF-8">
  <title>Role at Acme</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="/assets/tailwind-config.js"></script>
  <link rel="stylesheet" href="/assets/styles.css">
</head>
<body class="font-sans bg-stone-50 text-stone-900"
      x-data="{ p: null, sending: false, sent: false, name: '', email: '', cv: '' }"
      x-init="
        const slug = window.location.pathname.replace(/^\\/|\\/$/g, '');
        fetch('/cross_page_metadata.json').then(r => r.json())
          .then(d => p = d.pages.find(pg => pg.slug === slug))
      ">

  <header class="bg-brand text-white py-6 px-6">
    <a href="/" class="text-xl font-bold hover:text-blue-200">Acme Careers</a>
  </header>

  <template x-if="p && p.metadata">
    <main class="max-w-2xl mx-auto px-6 py-12">
      <a href="/" class="text-brand text-sm hover:underline mb-6 block">&larr; All open roles</a>

      <p class="text-stone-400 uppercase tracking-widest text-xs mb-2" x-text="p.metadata.department"></p>
      <h1 class="text-4xl font-bold text-brand mb-3" x-text="p.metadata.title"></h1>
      <div class="flex gap-4 text-stone-500 text-sm mb-8">
        <span x-text="p.metadata.location"></span>
        <span>&middot;</span>
        <span x-text="p.metadata.type"></span>
        <span>&middot;</span>
        <span x-text="p.metadata.salary"></span>
      </div>

      <p class="text-lg text-stone-700 mb-12" x-text="p.metadata.summary"></p>

      <section class="bg-white rounded-lg shadow p-8">
        <h2 class="text-2xl font-bold text-brand mb-2">Apply for this role</h2>
        <p class="text-stone-500 text-sm mb-6">We'll be in touch within a week.</p>

        <form @submit.prevent="
                sending = true;
                fetch('https://api.uat-beam.page/actions/acme-careers/email', {
                  method: 'POST',
                  headers: { 'Content-Type': 'application/json' },
                  body: JSON.stringify({
                    subject: 'Application: ' + p.metadata.title + ' — ' + name,
                    name: name,
                    email: email,
                    role: p.metadata.title,
                    cv_link: cv
                  })
                })
                .then(() => { sent = true; sending = false })
                .catch(() => { sending = false })
              ">
          <template x-if="!sent">
            <div class="space-y-4">
              <input x-model="name" type="text" placeholder="Full name" required class="w-full border border-stone-300 rounded p-3">
              <input x-model="email" type="email" placeholder="Email" required class="w-full border border-stone-300 rounded p-3">
              <input x-model="cv" type="url" placeholder="Link to CV (LinkedIn, Dropbox, etc.)" required class="w-full border border-stone-300 rounded p-3">
              <button :disabled="sending" class="w-full bg-brand text-white py-3 rounded font-semibold hover:bg-blue-900 disabled:opacity-50">
                <span x-show="!sending">Submit application</span>
                <span x-show="sending">Sending...</span>
              </button>
            </div>
          </template>
          <template x-if="sent">
            <div class="text-center py-6">
              <h3 class="text-xl font-bold text-brand">Application received</h3>
              <p class="text-stone-600 mt-1">Thanks for applying. We'll be in touch soon.</p>
            </div>
          </template>
        </form>
      </section>
    </main>
  </template>

  <script src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js" defer></script>
</body>
</html>
"""

for role in roles:
    requests.put(
        f"{API}/projects/{slug}/pages/{role['slug']}/files/index.html",
        headers=H,
        json={"content": role_html.replace("__PAGE_SLUG__", role["slug"])},
    )
    time.sleep(0.2)

print(f"Done. Live at https://{slug}.uat-beam.page")
```

## Closing a role

Same pattern as marking a property as sold — flip the `status` flag.

```python
import requests

API = "https://api.uat-beam.page"
TOKEN = "<your-token>"
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}

# Read, modify, write back
page = requests.get(
    f"{API}/projects/acme-careers/pages/senior-frontend-engineer",
    headers=H,
).json()
metadata = page["metadata"]
metadata["status"] = "filled"
metadata["filled_date"] = "2026-04-20"

requests.put(
    f"{API}/projects/acme-careers/pages/senior-frontend-engineer/metadata",
    headers=H,
    json=metadata,
)
```

The role disappears from the main page (which filters by
`status === 'active'`) but the URL still works for anyone with the
link. You could also extend the role page HTML to show "This position
has been filled" when status is `filled`.
