Easy-to-manage touch screens
무료 오픈소스 터치스크린으로 USB 설정 가이드, 드래그‑드롭 편집기, 로컬 제어를 제공하며 Home Assistant용으로 제작되었습니다
Cabana는 모바일 우선의 가벼운 호텔 투숙객 앱으로, 체크인·서비스·룸 요금·프런트 데스크를 하나의 화면에 제공합니다
Cabana is a light, mobile-first guest app for hotel stays: arrival, on-property services, room charges, and the front desk in a single calm surface.
라이프스타일TypeScript · CSSGitHub @kenanaiahjj온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Cabana is a mobile-first guest-stay app prototype: a light, neutral canvas with
white surfaces, hairline separators, and a single pink accent reserved for the
primary action. It covers arrival, stay access, hotel services, bookings, folio,
and front-desk support, and it is built with Asbir Sans.
The home route opens the guest app. The design-system gallery is available at
/components. See
docs/design-system.md for the component taxonomy, token
layers, and interaction contracts.README 전문 보기 ↗Cabana is a mobile-first guest-stay app prototype: a light, neutral canvas with white surfaces, hairline separators, and a single plum accent reserved for the primary action. It covers arrival, stay access, hotel services, bookings, folio, and front-desk support, and it is built with Asbir Sans.
The home route opens the guest app. The design-system gallery is available at
/components. See
docs/design-system.md for the component taxonomy, token
layers, and interaction contracts.
Prerequisites — Node.js >=20.9 (Next 16's floor; developed on 24.x) and npm.
No database and no external services at runtime: the app boots against a public
placeholder API. Installing, however, needs one credential. The icons come from
@hugeicons-pro, a licensed registry that .npmrc points the scope at, and npm
reads the token from the shell environment — .env.local is loaded by Next
long after npm install has already failed. Export it first:
git clone https://github.com/kenanaiahjj/pd-hospitality-user.git
cd pd-hospitality-user
export HUGEICONS_LICENSE_KEY="..." # from the Hugeicons account
npm install
cp .env.example .env.local
npm run dev
Without the key npm install stops on a 401 from npm.hugeicons.com for
@hugeicons-pro/core-stroke-rounded. On Vercel, set the same variable in the
project's environment so build-time installs can resolve the scope.
Open http://localhost:3000 for the guest app. Open
http://localhost:3000/components for the 36-pattern design-system gallery. The
dashboard at /dashboard continues to exercise the template's full data path
(route handler → typed API layer → upstream).
src/config/env.ts parses process.env with zod at module load, so a
missing or malformed value fails the boot with a readable error rather than
surfacing on the first request. Copy .env.example and adjust:
| Variable | Required | Default | Notes |
|---|---|---|---|
API_BASE_URL | yes | https://jsonplaceholder.typicode.com (from .env.example) | Upstream the server-side client calls. Must be a valid URL. |
API_TOKEN | no | — | Bearer token for the upstream. Server-only — never prefix it NEXT_PUBLIC_. |
NEXT_PUBLIC_APP_URL | no | http://localhost:3000 | Public origin, used for absolute internal fetches during SSR. |
HUGEICONS_LICENSE_KEY | install-time | — | Token for the private @hugeicons-pro registry (see .npmrc). Read by npm from the shell, not by src/config/env.ts — it is not in the zod schema and setting it in .env.local has no effect. |
Adding a variable means editing the schema in src/config/env.ts and
.env.example in the same commit — the parse is the only gate.
| Command | What it does |
|---|---|
npm run dev | Dev server on port 3000 (Turbopack) |
npm run build | Production build; runs typecheck and prerenders |
npm start | Serve a build produced by npm run build |
npm run typecheck | tsc --noEmit |
npm test | Vitest run, once (jsdom + Testing Library) |
npm run test:watch | Vitest in watch mode |
npm run lint | ESLint flat config — React Compiler rules are errors, not warnings |
npm run lint:fix | Same, with autofix |
Before calling a change done, run the full gate:
npm run typecheck && npm run lint && npm run build && npm test
Tests live beside what they test, as *.test.ts / *.test.tsx under src/.
They stub the network at the internalApi seam rather than at the transport,
so the "only one HTTP client" rule holds in tests too.
.next/types. rm -rf .next
and re-run.serverSchema is missing from .env.local — the message names it.serverEnv() was called in the browser. Server-only config reached a
Client Component; move the call into a Server Component or route handler.src/
app/ # routes only — thin, no business logic
(marketing)/ # route group: own layout, no URL segment
layout.tsx
page.tsx
api/ # route handlers (BFF layer)
users/route.ts # GET (list) + POST (create)
users/[id]/route.ts # GET one
dashboard/
layout.tsx page.tsx loading.tsx error.tsx
layout.tsx not-found.tsx globals.css
components/
ui/ # dumb, reusable primitives
button.tsx card.tsx input.tsx spinner.tsx
features/
users/ # feature-specific composed components
user-card.tsx user-list.tsx user-search.tsx
user-create-form.tsx user-detail.tsx
lib/
api/ # the fetch wrapper + configured clients
client.ts # axios wrapper (headers, timeout, zod, errors)
errors.ts # ApiError
instances.ts # internalApi — browser-safe, talks to this app's BFF
instances.server.ts # externalApi — server-only (`server-only` guarded)
users.ts # typed functions per resource
queries/ # BROWSER-side data access (TanStack Query)
client.ts # createQueryClient — cache and retry policy
provider.tsx # QueryProvider, mounted in the root layout
bff.ts # reads the { data } envelope from a route handler
users.ts # userQueries + userMutations
hooks/ # use-debounce
utils/ # cn, format, http (route-handler helpers)
validators/ # zod schemas
types/ # shared TS types (inferred from schemas)
config/ # env (zod-validated), constants
test/ # test helpers (render-with-query)
There is exactly one place that performs HTTP, and one place that owns each URL.
Server Component ──────────────────────────────────► usersApi.list() ─► externalApi ─► upstream
Client Component ─► useQuery(userQueries.list()) ─► /api/users ─► usersApi.list() ─► externalApi ─► upstream
lib/api/client.ts — the only HTTP client in the app, an axios instance
pinned to adapter: 'fetch' so Next's cache directives still apply. Adds base
URL, headers, query serialization, JSON body handling, a request timeout, and
turns any failure into an ApiError with a status and a stable code. Pass a zod
schema and you get a parsed, typed result; a mismatched upstream payload
fails as 502 invalid_response instead of leaking through your types.lib/api/users.ts — one typed function per operation. Components never
know the upstream path, auth or cache tags. Server-only.app/api/users/route.ts — the BFF. Validates input with zod, delegates to
usersApi, wraps the result in the app's envelope. The route() helper turns
every thrown error into consistent JSON, so no handler needs a try/catch.lib/queries/users.ts — the browser's door to the data. Key and fetcher
are declared together in one queryOptions() call, so a cache key can never
drift from the function that fills it; userMutations.create() takes the
QueryClient and invalidates ['users'] itself. Server twin of this file is
lib/api/users.ts — same resource, different side of the wire.lib/queries/bff.ts — the read-side mirror of ok(): one place unwraps
the { data } envelope and validates the payload against its zod schema.Responses use one envelope (src/types/api.ts):
{ "data": [ { "id": 1, "name": "…", "email": "…" } ] }
{ "error": { "message": "Invalid request body", "code": "validation_error",
"fields": { "email": ["Enter a valid email address"] } } }
Try it:
curl -s "localhost:3000/api/users?pageSize=2"
curl -s "localhost:3000/api/users?pageSize=999" # 422 with field messages
curl -s -X POST localhost:3000/api/users -H 'content-type: application/json' -d '{"name":"a","email":"nope"}'
page.tsx composes components and awaits a lib/api
function. Business logic never lives under app/.src/types/user.ts derives User
from userSchema, so the validator is the single source of truth.serverEnv() throws if called in the
browser, and API_TOKEN only ever reaches externalApi. lib/utils/index.ts
deliberately does not re-export http.ts (it imports next/server).src/config/env.ts parses process.env with zod at
module load, so a missing variable breaks the boot, not the first request.lib/validators/, a type in types/, a
server lib/api/<resource>.ts, a route handler, and a browser
lib/queries/<resource>.ts. Five small files, always the same shape.internalApi directly — they call useQuery(userQueries.list()). Keys are
hierarchical (['users'] → ['users','list'] → ['users','list',params]), so
invalidating the root reaches lists and details alike.API_BASE_URL defaults to jsonplaceholder.typicode.com so the boilerplate runs
with no setup — point it at your own API and delete the users example.
CLAUDE.md carries the architecture invariants and the local Superpowers
workflow (brainstorm → spec → plan → worktree → execute → review).
Specs live in docs/superpowers/specs/, plans in docs/superpowers/plans/.
Each has a template; start one with:
./scripts/new-spec.sh projects-resource
./scripts/new-plan.sh add-projects-resource
Both stamp today's date into the filename and the document's status header.
MIT — see LICENSE.