Telecom
Budget Supermarket SIM Provider
2023 – 2025
Overview
Two parallel Nuxt 3 rebuilds of legacy customer portals for a pair of German budget mobile MVNOs. Both projects were built in tandem following the same composable and component conventions — tariff listings, prepaid top-up, help & service, network coverage, and a contract cancellation flow — with each brand maintaining its own independent codebase and deployment.
Architecture
Brand A (A-Mobil) Brand B (B-Mobil)
Nuxt 3 + brand theme A Nuxt 3 + brand theme B
useTealium · useDeviceType useTealium · useDeviceType
useAnchorScroll useAnchorScroll
useRedirectTracking useRedirectTracking
│ │
└──────────────┬────────────────┘
│
MVNO Backend API
(cancellation · tariffs · top-up)Key Contributions
- Worked on both Nuxt 3 rebuilds, applying the same composable and component conventions across both brand portals
- Contract cancellation form with multi-step conditional logic and schema-based validation (VeeValidate + Yup) — covering cancellation type, customer identification, phone number, and document upload
- jsPDF cancellation confirmation: dynamically assembles a branded PDF with company logo, customer data, and cancellation details, generated client-side on successful form submission
- Composable implementations for Tealium tag management, device type detection, anchor-scroll navigation, and redirect tracking — maintained in parallel across both brand codebases
- Tariff comparison matrix, prepaid top-up flow, help & service FAQ, and network coverage page — all developed with WCAG 2.2 accessibility as a baseline requirement
Code Examples
Conditional Yup Validation Schema
The cancellation form branches on two independent conditions: the termination date field is only required when the customer is not porting their number (mnp = 'no'), and the document upload is only required for extraordinary cancellations where the reason isn't 'other' — and when required it must be a PDF under 5 MB. Both conditions use Yup's .when() API so the schema stays declarative and VeeValidate re-evaluates the affected fields reactively as the user makes selections.
const SUPPORTED_FORMATS = ['application/pdf']
const schema = yup.object().shape({
firstName: yup.string().required('Please enter your first name'),
lastName: yup.string().required('Please enter your last name'),
identification: yup.string().required('Please select an option'),
identificationInput: yup.string().required('Please fill in this field'),
type: yup.string().required('Please select an option'),
cancellationType: yup.string().required('Please select an option'),
email: yup
.string()
.required('Please enter your email address')
.email('Please enter a valid email address'),
phoneNumber: yup
.number()
.typeError('Please enter a valid phone number')
.required('Please enter your mobile number'),
alternativePhoneNumber: yup
.number()
.typeError('Please enter a valid phone number'),
// Only required when NOT porting the number to another provider
date: yup.string().when('mnp', {
is: (value: string) => value === 'mnp-no',
then: schema => schema.required('Please select an option'),
otherwise: schema => schema.notRequired(),
}),
// Only required for extraordinary cancellations with a specific reason
upload: yup.mixed().when('cancellationType', {
is: (value: string) =>
value === 'Extraordinary cancellation' &&
formData.value.cancellationTypeValue &&
formData.value.cancellationTypeValue !== 'Other',
then: schema =>
schema
.required('Please upload supporting documentation')
.test('format', 'Please upload a PDF (max. 5 MB)', (value: unknown) => {
const fileList = value as FileList
if (!fileList || fileList.length === 0) return false
const file = fileList[0]
return SUPPORTED_FORMATS.includes(file.type) && file.size <= 5_242_880
}),
otherwise: schema => schema.notRequired(),
}),
})Tech Stack
- Nuxt 3
- Vue 3
- TypeScript
- SCSS / Sass
- VeeValidate
- Yup
- jsPDF
- VueUse
- Axios
- Vitest