← Back to projects

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

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.

Cancellation typerequired
Porting number?required
schema:upload·date
components/partial/CancellationForm.vue
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

Framework
  • Nuxt 3
  • Vue 3
  • TypeScript
Styling
  • SCSS / Sass
Forms & Validation
  • VeeValidate
  • Yup
Libraries
  • jsPDF
  • VueUse
  • Axios
Testing
  • Vitest