Ouvrir le site dans un nouvel ongletShopyPhone
Boutique en ligne
Catalogue, variantes produits et parcours d’achat pour une boutique suisse de smartphones reconditionnés.
Étude de cas (nouvel onglet)Je développe des sites web
et des applications.
Je m’occupe du design, du code et de ce qui tourne derrière. Ici, vous trouverez les projets sur lesquels je travaille.
Je dessine les écrans et je les développe. Du premier clic aux petits détails qui rendent un site agréable à utiliser.
Vue / Nuxt / Astro / GSAPCe qui se passe après le clic : les règles métier, les droits d’accès et les données à retrouver au bon endroit.
TypeScript / SQL / CloudflareDes applications qui tournent sur l’ordinateur, avec une interface web et du Rust pour travailler avec le système.
Tauri / Rust / SQLitefunction click(event: MouseEvent) {
if (suppressClick && event.detail !== 0) {
suppressClick = false;
return;
}
emit("activate");
}La carte transmet le clic à l’application, en évitant un déclenchement en double après un appui prolongé.
Lire dans le code 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();L’interface transmet les demandes de lancement, de reprise et de fermeture à Tauri.
Lire dans le code 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)?
};Le code natif retrouve un processus déjà ouvert ou lance l’application demandée.
Lire dans le code sourceindex: 0 · view: carouselCode source ↗Démonstration de navigation, sans lancement d’applications. Flèches pour naviguer, Échap pour sortir.
<UButton
type="button"
:label="`Encaisser le solde · ${formatCurrency(balanceDue)}`"
icon="i-lucide-wallet"
:loading="creatingMethod === quickPaymentMethod"
:disabled="!canCreatePayment || mutationPending"
block
@click="createQuickPayment(quickPaymentMethod)"
/>Le bouton affiche le solde à encaisser et se désactive pendant l’enregistrement.
Lire dans le code 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))
})Le serveur vérifie les droits, valide la saisie et exige une clé pour reconnaître une demande répétée.
Lire dans le code 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(),Chaque paiement conserve son montant, son statut et le lien vers son document ; un remboursement peut référencer l’encaissement d’origine.
Lire dans le code 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),
),
),
}));La vue Vue additionne les salaires enregistrés et suit séparément les montants payés.
Lire dans le code 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,Le calcul additionne les retenues, puis les déduit du brut et ajoute les remboursements.
Lire dans le code 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)?;La couche Rust enregistre une révision de fiche et son PDF dans une transaction SQLite.
Lire dans le code sourceCliquez, explorez le code, essayez.
Sites clients
Avec Peaks, je conçois et développe des sites pour les petites entreprises. Voici trois projets que j’ai réalisés.
Ouvrir le site dans un nouvel ongletBoutique en ligne
Catalogue, variantes produits et parcours d’achat pour une boutique suisse de smartphones reconditionnés.
Étude de cas (nouvel onglet)
Ouvrir le site dans un nouvel ongletSite et réservation
Présentation des prestations et réservation en ligne d’un service de collecte, entretien et retour du matériel.
Étude de cas (nouvel onglet)
Ouvrir le site dans un nouvel ongletCatalogue en ligne
Catalogue de montres, navigation par marque, langues, devises et prise de contact pour l’achat ou la revente.
Étude de cas (nouvel onglet)Pour le site web de votre entreprise, retrouvez-moi chez Peaks.
Contacter Peaks (nouvel onglet)
Je programme depuis l’enfance. Aujourd’hui, je partage mon temps entre les projets de mes clients chez Peaks et mes propres logiciels. J’aime autant soigner une interface que comprendre pourquoi un bout de code ne fait pas ce qu’il devrait.
Un peu plus sur moi