Open website in a new tabShopyPhone
Online store
Product catalogue, variants and shopping journey for a Swiss refurbished smartphone store.
Case study (new tab)I build websites
and applications.
I handle the design, the code and the systems behind it. Here are some of the projects I’m working on.
I design the screens and build them. From the first click to the small details that make a site feel good to use.
Vue / Nuxt / Astro / GSAPWhat happens after the click: business rules, access permissions and keeping data in the right place.
TypeScript / SQL / CloudflareApplications that run on your computer, with a web interface and Rust to work with the operating system.
Tauri / Rust / SQLitefunction click(event: MouseEvent) {
if (suppressClick && event.detail !== 0) {
suppressClick = false;
return;
}
emit("activate");
}The card forwards a click while preventing a duplicate action after a long press.
Read the original sourceexport const launch = (id: string): Promise<Session> =>
native ? invoke("launch_app", { id }) : unavailable();
export const resume = (id: string): Promise<void> =>
native ? invoke("resume_app", { id }) : unavailable();
export const closeApp = (id: string, force = false): Promise<void> =>
native ? invoke("close_app", { id, force }) : unavailable();The interface sends launch, resume and close requests to Tauri.
Read the original sourcelet existing = path.as_ref().and_then(|p| {
before.iter().find(|process| {
process.path.eq_ignore_ascii_case(p) && windows.iter().any(|w| w.process == process.key)
})
});
let key = if let Some(p) = existing {
Some(p.key)
} else {
platform::launch(&item)?
};The native code finds an existing process or launches the requested application.
Read the original sourceindex: 0 · view: carouselSource code ↗Navigation demonstration. No applications are launched. Arrow keys to navigate; Escape to leave.
<UButton
type="button"
:label="`Encaisser le solde · ${formatCurrency(balanceDue)}`"
icon="i-lucide-wallet"
:loading="creatingMethod === quickPaymentMethod"
:disabled="!canCreatePayment || mutationPending"
block
@click="createQuickPayment(quickPaymentMethod)"
/>The button shows the outstanding amount and is disabled while a payment is being recorded.
Read the original sourceexport default eventHandler(async (event) => {
await requireCapability(event, 'financial:record')
const idempotencyKey = requireIdempotencyKey(event)
const params = await getValidatedRouterParams(event, numericIdParamsSchema.parse)
const body = await readValidatedBody(event, markDocumentPaidSchema.parse)
return markDocumentAsPaid(params.id, body, idempotencyKey, readDossierWriteContext(event))
})The server checks permissions, validates input and requires a key to recognize a repeated request.
Read the original sourceexport const payments = sqliteTable('payments', {
id: integer('id').primaryKey({ autoIncrement: true }),
customerId: integer('customer_id').references(() => customers.id, { onDelete: 'set null' }),
documentId: integer('document_id').notNull().references(() => documents.id, { onDelete: 'cascade' }),
kind: text('kind', { enum: ['receipt', 'refund'] }).notNull().default('receipt'),
originalPaymentId: integer('original_payment_id').references((): AnySQLiteColumn => payments.id, { onDelete: 'restrict' }),
recordedBy: text('recorded_by'),
voidedAt: text('voided_at'),
voidReason: text('void_reason'),
method: text('method', { enum: ['cash', 'card_twint', 'bank_transfer', 'stripe', 'shopify'] }).notNull(),
status: text('status', { enum: ['pending', 'paid', 'refunded', 'cancelled'] }).notNull().default('pending'),
amount: integer('amount').notNull(),Each payment stores its amount, status and document link; a refund can reference the original receipt.
Read the original sourceconst totals = computed(() => ({
net: sum(saved.value.map((p) => p.result.net)),
gross: sum(saved.value.map((p) => p.result.gross)),
deductions: sum(saved.value.map((p) => p.result.deductions)),
paid: sum(
rows.value.map(
(r) => r.p?.paidAmount ?? (r.p?.paidDate ? r.p.result.net : 0),
),
),
}));The Vue view totals recorded payroll and tracks paid amounts separately.
Read the original sourceconst deductions = sum(
lines.filter((l) => l.category === "contribution").map((l) => l.amount),
),
employerCost = sum(lines.map((l) => l.employer));
if (base + absence < 0) errors.push("absenceTooHigh");
return {
...(smallSalary ? { smallSalary } : {}),
lines,
gross,
deductions,
reimbursements,
net: gross - deductions + reimbursements,The calculation totals deductions, subtracts them from gross pay and adds reimbursements.
Read the original sourcetx.execute(
"INSERT INTO revisions VALUES(?1,?2,?3,?4,?5)",
params![
id,
row["payrollId"].as_str().ok_or("invalidState")?,
row["number"].as_i64().ok_or("invalidState")?,
row.to_string(),
pdf
],
)
.map_err(err)?;The Rust layer saves a payslip revision and its PDF within a SQLite transaction.
Read the original sourceClick, explore the code, try it out.
Client websites
Through Peaks, I design and build websites for small businesses. Here are three projects I’ve worked on.
Open website in a new tabOnline store
Product catalogue, variants and shopping journey for a Swiss refurbished smartphone store.
Case study (new tab)
Open website in a new tabWebsite and bookings
Service information and online booking for ski equipment collection, maintenance and return.
Case study (new tab)
Open website in a new tabOnline catalogue
Watch catalogue with brand navigation, language and currency selection, and buying or selling enquiries.
Case study (new tab)For your business website, get in touch through Peaks.
Contact Peaks (new tab)
I’ve been programming since I was a kid. These days, I split my time between client projects at Peaks and my own software. I enjoy getting an interface right as much as figuring out why a piece of code isn’t doing what it should.
A bit more about me