| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907 |
- <template>
- <div class="theme-artist">
- <CommonMeta
- title="Essai gratuit Opentalent Artist Premium - 30 jours sans engagement"
- description="Essayez gratuitement Opentalent Artist Premium pendant 30 jours. Solution complète pour orchestres, chorales, compagnies de théâtre, de danse ou de cirque."
- />
- <div class="background-container">
- <LayoutContainer class="trial-container">
- <div id="anchor" />
- <v-card class="form-card">
- <v-card-text>
- <h1 class="text-center mb-6">
- Essayez gratuitement Opentalent Artist Premium pendant 30 jours !
- </h1>
- <div v-if="!trialRequestSent">
- <div class="description mb-8">
- <p>
- Opentalent Artist Premium est une solution en ligne complète,
- pensée pour les orchestres, chorales, compagnies de théâtre,
- de danse ou de cirque. Elle vous aide à gagner du temps dans
- l'organisation de vos activités, à mieux collaborer avec vos
- équipes et à renforcer votre visibilité auprès de votre
- public.
- </p>
- <p>
- Pendant 30 jours, profitez de toutes les fonctionnalités
- d'Opentalent Artist Premium, gratuitement et sans engagement :
- </p>
- <ul class="benefits-list">
- <li>
- <span class="mr-1">✔️</span> Gestion intuitive des membres
- et des événements
- </li>
- <li>
- <span class="mr-1">✔️</span> Planification avancée des
- répétitions, spectacles et tournées
- </li>
- <li>
- <span class="mr-1">✔️</span> Outils de communication
- intégrés (emails, publipostage, etc.)
- </li>
- <li>
- <span class="mr-1">✔️</span> Site web personnalisable pour
- présenter vos projets et votre structure
- </li>
- <li>
- <span class="mr-1">✔️</span> Accès collaboratif pour vos
- équipes, en temps réel
- </li>
- </ul>
- <p>
- Il vous suffit de remplir le formulaire ci-dessous pour
- activer votre essai gratuit.
- </p>
- <p>
- Lancez-vous dès aujourd'hui et découvrez comment Opentalent
- peut transformer votre organisation artistique !
- </p>
- </div>
- <v-alert
- type="info"
- variant="tonal"
- border="start"
- class="mb-4"
- density="comfortable"
- >
- <template #title>
- Vous êtes adhérents à la Confédération Musicale de France ?
- <br />Et si on vous disait que vous l'aviez déjà...
- </template>
- Dans le cadre de votre adhésion, vous bénéficiez de la version
- Opentalent Artist Standard, et de conditions privilégiées pour
- la version Artist Premium. Contactez votre fédération pour
- obtenir vos codes d'accès.
- <div class="mt-2">
- <a href="https://www.cmf-musique.org/contact/" target="_blank"
- >Je souhaite obtenir mon code d'accès</a
- >
- </div>
- </v-alert>
- <v-form
- ref="form"
- validate-on="submit lazy"
- @submit.prevent="submit"
- >
- <v-container>
- <div v-if="isDevelopmentOrTest" class="dev-tools-container">
- <v-btn
- color="info"
- size="small"
- prepend-icon="fa fa-magic"
- @click="fillWithDummyData"
- >
- Remplir avec des données de test
- </v-btn>
- </div>
- <i
- >Les champs dont le nom est suivi d'un astérisque (*) sont
- obligatoires.</i
- >
- <h2 class="section-title">Coordonnées de la structure</h2>
- <!-- Structure name -->
- <v-row>
- <v-col cols="12">
- <v-text-field
- v-model="trialRequest.structureName"
- :rules="[validateRequired]"
- label="Nom de la structure*"
- required
- @input="onStructureNameUpdated"
- />
- </v-col>
- </v-row>
- <!-- Structure address -->
- <v-row>
- <v-col cols="12" md="6">
- <v-text-field
- v-model="trialRequest.address"
- :rules="[validateRequired]"
- label="Adresse du siège social de la structure*"
- required
- />
- </v-col>
- <v-col cols="12" md="6">
- <v-text-field
- v-model="trialRequest.addressComplement"
- label="Adresse (suite)"
- />
- </v-col>
- </v-row>
- <v-row>
- <v-col cols="12" md="6">
- <v-text-field
- v-model="trialRequest.postalCode"
- :rules="[validateRequired, validatePostalCode]"
- label="Code postal*"
- required
- />
- </v-col>
- <v-col cols="12" md="6">
- <v-text-field
- v-model="trialRequest.city"
- :rules="[validateRequired]"
- label="Ville*"
- required
- />
- </v-col>
- </v-row>
- <!-- Structure email and SIREN -->
- <v-row>
- <v-col cols="12" md="6">
- <v-text-field
- v-model="trialRequest.structureEmail"
- :rules="[validateRequired, validateEmail]"
- label="Adresse email de la structure*"
- required
- type="email"
- />
- </v-col>
- <v-col cols="12" md="6">
- <v-text-field
- v-model="trialRequest.siren"
- :rules="[validateSiren]"
- label="SIREN (optionnel)"
- hint="Numéro à 9 chiffres"
- />
- </v-col>
- </v-row>
- <!-- Structure type and legal status -->
- <v-row>
- <v-col cols="12" md="6">
- <v-select
- v-model="trialRequest.structureType"
- :rules="[validateRequired]"
- label="Type de la structure*"
- :items="structureTypes"
- item-value="value"
- item-title="title"
- required
- />
- </v-col>
- <v-col cols="12" md="6">
- <v-select
- v-model="trialRequest.legalStatus"
- :rules="[validateRequired]"
- label="Statut juridique*"
- :items="legalStatuses"
- item-value="value"
- item-title="title"
- required
- />
- </v-col>
- </v-row>
- <h2 class="section-title">Représentée par</h2>
- <!-- Representative function -->
- <v-row>
- <v-col cols="12">
- <v-text-field
- v-model="trialRequest.representativeFunction"
- :rules="[validateRequired]"
- label="Fonction*"
- required
- />
- </v-col>
- </v-row>
- <!-- Representative name -->
- <v-row>
- <v-col cols="12" md="6">
- <v-text-field
- v-model="trialRequest.representativeFirstName"
- :rules="[validateRequired]"
- label="Prénom*"
- required
- />
- </v-col>
- <v-col cols="12" md="6">
- <v-text-field
- v-model="trialRequest.representativeLastName"
- :rules="[validateRequired]"
- label="Nom*"
- required
- />
- </v-col>
- </v-row>
- <!-- Representative contact -->
- <v-row>
- <v-col cols="12" md="6">
- <v-text-field
- v-model="trialRequest.representativeEmail"
- :rules="[validateRequired, validateEmail]"
- label="Adresse email*"
- required
- type="email"
- />
- </v-col>
- <v-col cols="12" md="6">
- <CommonPhoneInput
- ref="phoneInput"
- v-model="trialRequest.representativePhone"
- label="Téléphone*"
- required
- />
- </v-col>
- </v-row>
- <h2 class="section-title mb-6">Informations de connexion</h2>
- <!-- Structure identifier -->
- <v-row>
- <v-col cols="12" md="6" class="mx-auto">
- <v-text-field
- v-model="trialRequest.structureIdentifier"
- :rules="[
- validateRequired,
- validateSubdomain,
- validateSubdomainAvailability,
- ]"
- label="Identifiant de la structure*"
- required
- class="text-center"
- @input="onStructureIdentifierUpdated"
- />
- <div class="validationMessage">
- <span v-if="validationPending">
- <v-progress-circular size="16" indeterminate />
- <i class="ml-2">Vérification en cours</i>
- </span>
- <span
- v-else-if="subdomainAvailable === true"
- class="text-success"
- >
- <v-icon>fa fa-check</v-icon>
- <i class="ml-2"> Cet identifiant est disponible</i>
- </span>
- <span
- v-else-if="subdomainAvailable === false"
- class="text-error"
- >
- <v-icon>fa fa-x</v-icon>
- <i class="ml-2"
- >Cet identifiant n'est pas disponible</i
- >
- </span>
- </div>
- <div class="mt-2">
- <i v-if="trialRequest.structureIdentifier">
- Le compte administrateur de la structure sera
- <strong>
- admin{{ trialRequest.structureIdentifier }}
- </strong>
- </i>
- </div>
- <div>
- <i>
- Veuillez renseigner un mot de passe pour ce compte :
- </i>
- </div>
- </v-col>
- </v-row>
- <!-- Password field -->
- <v-row>
- <v-col cols="12" md="6" class="mx-auto">
- <v-text-field
- v-model="trialRequest.password"
- :rules="[validateRequired, validatePassword]"
- label="Mot de passe*"
- required
- :type="showPassword ? 'text' : 'password'"
- :append-inner-icon="
- showPassword ? 'fa fa-eye-slash' : 'fa fa-eye'
- "
- @click:append-inner="showPassword = !showPassword"
- />
- <div class="mt-1">
- <i>
- Le mot de passe doit contenir au moins 8 caractères,
- une minuscule, une majuscule, un chiffre et un
- caractère spécial.
- </i>
- </div>
- <v-text-field
- v-model="trialRequest.confirmPassword"
- :rules="[
- showPassword ? () => true : validateRequired,
- showPassword ? () => true : validatePasswordMatch,
- ]"
- label="Confirmer le mot de passe*"
- :required="!showPassword"
- :disabled="showPassword"
- :type="showPassword ? 'text' : 'password'"
- />
- </v-col>
- </v-row>
- <h2 class="section-title">Accord de termes et conditions</h2>
- <!-- Terms checkboxes -->
- <v-checkbox
- v-model="trialRequest.termsAccepted"
- :rules="[validateCheckbox]"
- required
- >
- <template #label>
- Mon organisme accepte les
- <a
- href="https://maestro.opentalent.fr/uploads/share/Documents_juridique/CGU.pdf"
- target="_blank"
- >
- conditions générales d'utilisation </a
- >.*
- </template>
- </v-checkbox>
- <v-checkbox
- v-model="trialRequest.legalRepresentative"
- :rules="[validateCheckbox]"
- label="J'agis en tant que représentant légal de l'association ou de la structure.*"
- required
- />
- <v-checkbox
- v-model="trialRequest.newsletterSubscription"
- label="J'accepte de recevoir la lettre d'information culturelle afin de découvrir des idées de sorties adaptées à ma région."
- />
- <div class="d-flex flex-row justify-center">
- <LayoutCaptcha />
- </div>
- <!-- Submit Button -->
- <div class="d-flex flex-row justify-center my-10">
- <v-btn
- type="submit"
- color="secondary"
- size="large"
- class="submit-btn"
- >
- COMMENCER MON ESSAI DE 30 JOURS
- </v-btn>
- </div>
- <p class="text-center no-credit-card">
- Aucune carte de crédit requise. En cliquant sur "Commencer
- mon essai de 30 jours", vous acceptez de démarrer votre
- période d'essai gratuit.
- </p>
- <div v-if="validationError" class="error">
- Des champs du formulaire sont invalides ou manquants.
- </div>
- <div v-if="errorMsg" class="error">
- {{ errorMsg }}
- </div>
- </v-container>
- <div class="legal">
- Les données recueillies par Opentalent sont utilisées pour le
- traitement de votre demande et pour vous informer sur nos
- offres. Elles sont destinées aux services Opentalent et à ses
- sous-traitants pour l'exécution des contrats. Conformément à
- la loi "Informatique et Libertés du 6 Janvier 1978", vous
- disposez d'un droit d'accès, de modifications, de
- rectification et de suppression des données vous concernant.
- Pour toute demande, adressez-vous à : OPENTALENT, 265 rue de
- la Grange 74950 SCIONZIER - FRANCE, opentalent.fr s'engage à
- la confidentialité et à la protection de vos données."
- </div>
- </v-form>
- </div>
- <div
- v-else
- class="confirmation-message d-flex flex-row justify-center"
- >
- <v-card>
- <v-card-title class="text-center">
- <v-icon
- icon="fas fa-check mr-2"
- color="success"
- max-height="48"
- />
- Félicitations !
- </v-card-title>
- <v-card-text class="text-center">
- <p>
- Votre demande d'essai gratuit de 30 jours d'Opentalent
- Artist Premium a bien été enregistrée, mais nécessite une
- validation de votre part.
- </p>
- <p>
- Vous allez recevoir un email permettant de valider cette
- demande, à la suite de quoi votre compte sera créé.
- Attention, la durée de validité du lien d'activation est de
- 60 minutes.
- </p>
- <p>
- Notre équipe reste à votre disposition pour vous accompagner
- durant cette période d'essai.
- </p>
- </v-card-text>
- <v-card-actions class="justify-center">
- <v-btn
- variant="elevated"
- prepend-icon="fas fa-arrow-left"
- color="secondary"
- to="/opentalent-artist"
- >
- Retour à la page Opentalent Artist
- </v-btn>
- </v-card-actions>
- </v-card>
- </div>
- </v-card-text>
- </v-card>
- </LayoutContainer>
- </div>
- </div>
- </template>
- <script setup lang="ts">
- import { useRouter } from 'vue-router'
- import type { Ref } from 'vue'
- import { reactive } from 'vue'
- import _ from 'lodash'
- import { useRuntimeConfig, useAsyncData } from '#app'
- import type { TrialRequest } from '~/types/interface'
- import { STRUCTURE_TYPE, LEGAL_STATUS } from '~/types/types'
- import { useAp2iRequestService } from '~/composables/data/useAp2iRequestService'
- import { useAp2iErrorHandler } from '~/composables/utils/useAp2iErrorHandler'
- import {
- convertPhoneNumberToInternationalFormat,
- slugify,
- } from '~/services/utils/stringUtils'
- const router = useRouter()
- const form: Ref<HTMLElement | null> = ref(null)
- // Structure types and legal statuses
- const structureTypes = Object.values(STRUCTURE_TYPE)
- .map((item) => ({
- value: item.key,
- title: item.label,
- }))
- .sort((a, b) => (a.title > b.title ? 1 : -1))
- const legalStatuses = Object.values(LEGAL_STATUS)
- .map((item) => ({
- value: item.key,
- title: item.label,
- }))
- .sort((a, b) => (a.title > b.title ? 1 : -1))
- // Get apiRequestService for subdomain availability check
- const { ap2iRequestService } = useAp2iRequestService()
- const { processApiError } = useAp2iErrorHandler()
- // Check if we're in a development environment
- const config = useRuntimeConfig()
- const isDevelopmentOrTest = computed(
- () => config.public.env === 'dev' || config.public.env === 'test'
- )
- // Trial request data
- const trialRequest = reactive<TrialRequest>({
- structureName: '',
- address: '',
- addressComplement: '',
- postalCode: '',
- city: '',
- structureEmail: '',
- structureType: 'ARTISTIC_PRACTICE_ONLY',
- legalStatus: 'ASSOCIATION_LAW_1901',
- structureIdentifier: '',
- siren: '',
- representativeFirstName: '',
- representativeLastName: '',
- representativeFunction: '',
- representativeEmail: '',
- representativePhone: '',
- password: '',
- confirmPassword: '',
- termsAccepted: false,
- legalRepresentative: false,
- newsletterSubscription: false,
- createWebsite: false,
- })
- // Function to fill the form with dummy data
- const fillWithDummyData = () => {
- // Generate a short timestamp (unix timestamp in seconds)
- const shortTimestamp = Math.floor(Date.now() / 1000).toString()
- trialRequest.structureName = `Compagnie Artistique Test ${shortTimestamp}`
- trialRequest.address = '123 Rue des Arts'
- trialRequest.addressComplement = 'Bâtiment B'
- trialRequest.postalCode = '75001'
- trialRequest.city = 'Paris'
- trialRequest.structureEmail = 'contact@compagnie-test.fr'
- trialRequest.structureType = 'ARTISTIC_PRACTICE_ONLY'
- trialRequest.legalStatus = 'ASSOCIATION_LAW_1901'
- trialRequest.structureIdentifier = `compagnie-test-${shortTimestamp}`
- trialRequest.siren = '123456789'
- trialRequest.representativeFirstName = 'Jean'
- trialRequest.representativeLastName = 'Dupont'
- trialRequest.representativeFunction = 'Directeur Artistique'
- trialRequest.representativeEmail = 'jean.dupont@compagnie-test.fr'
- trialRequest.representativePhone = '0612345678'
- trialRequest.password = 'Test1234!'
- trialRequest.confirmPassword = 'Test1234!'
- trialRequest.termsAccepted = true
- trialRequest.legalRepresentative = true
- trialRequest.newsletterSubscription = true
- // Trigger subdomain availability check
- checkSubdomainAvailabilityDebounced()
- }
- // Track if structure identifier has been manually modified
- const structureIdentifierModified = ref(false)
- // Variables for subdomain validation
- const validationPending = ref(false)
- const subdomainAvailable = ref<boolean | null>(null)
- const checkSubdomainAvailability = async (subdomain: string) => {
- if (!subdomain || validateSubdomain(subdomain) !== true) {
- subdomainAvailable.value = null
- return false
- }
- validationPending.value = true
- try {
- const subdomainAvailability = await ap2iRequestService.get(
- '/api/public/subdomains/is_available',
- { subdomain }
- )
- subdomainAvailable.value =
- subdomainAvailability && subdomainAvailability.available === true
- validationPending.value = false
- return subdomainAvailable.value
- } catch (error) {
- console.error('Error checking subdomain availability:', error)
- subdomainAvailable.value = false
- validationPending.value = false
- return false
- }
- }
- /**
- * Version debounced de la fonction checkAvailability
- * @see https://docs-lodash.com/v4/debounce/
- */
- const checkSubdomainAvailabilityDebounced: _.DebouncedFunc<() => void> =
- _.debounce(
- async () =>
- await checkSubdomainAvailability(trialRequest.structureIdentifier),
- 600
- )
- // Validation rules
- const validateRequired = (value: string) =>
- !!value || 'Ce champ est obligatoire'
- const validateEmail = (email: string) =>
- /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || 'Adresse email invalide'
- const validatePostalCode = (postalCode: string) =>
- /^\d{5}$/.test(postalCode) || 'Code postal invalide (5 chiffres)'
- const validateSiren = (siren: string) =>
- !siren || /^\d{9}$/.test(siren) || 'SIREN invalide (9 chiffres)'
- const validateCheckbox = (value: boolean) =>
- value || 'Vous devez accepter cette condition'
- const validatePassword = (password: string) => {
- if (!password) return 'Ce champ est obligatoire'
- const minLength = password.length >= 8
- const hasLowercase = /[a-z]/.test(password)
- const hasUppercase = /[A-Z]/.test(password)
- const hasNumber = /[0-9]/.test(password)
- const hasSpecial = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password)
- if (!minLength) {
- return 'Le mot de passe doit contenir au moins 8 caractères'
- }
- if (!hasLowercase) {
- return 'Le mot de passe doit contenir au moins une lettre minuscule'
- }
- if (!hasUppercase) {
- return 'Le mot de passe doit contenir au moins une lettre majuscule'
- }
- if (!hasNumber) {
- return 'Le mot de passe doit contenir au moins un chiffre'
- }
- if (!hasSpecial) {
- return 'Le mot de passe doit contenir au moins un caractère spécial'
- }
- return true
- }
- const validatePasswordMatch = () => {
- if (!trialRequest.confirmPassword) return 'Ce champ est obligatoire'
- return (
- trialRequest.password === trialRequest.confirmPassword ||
- 'Les mots de passe ne correspondent pas'
- )
- }
- const validateSubdomain = (value: string) => {
- if (!value) return 'Ce champ est obligatoire'
- const regex = /^[a-z0-9][a-z0-9-]{0,28}[a-z0-9]$/
- return (
- regex.test(value) ||
- 'Format invalide. Utilisez uniquement des lettres minuscules, des chiffres et des tirets. Doit commencer et finir par une lettre ou un chiffre. Maximum 30 caractères.'
- )
- }
- const validateSubdomainAvailability = (value: string) => {
- if (!value) return ''
- return (
- subdomainAvailable.value === true || "Cet identifiant n'est pas disponible"
- )
- }
- // Password visibility toggle
- const showPassword = ref(false)
- // Form state
- const trialRequestSent: Ref<boolean> = ref(false)
- const errorMsg: Ref<string | null> = ref(null)
- const validationError: Ref<boolean> = ref(false)
- // Reference to the phone input component
- const phoneInput = ref(null)
- // Submit function
- const submit = async (): Promise<void> => {
- const { valid } = await form.value!.validate()
- if (!valid) {
- validationError.value = true
- return
- }
- validationError.value = false
- // Convert phone number to international format before submission
- if (phoneInput.value) {
- trialRequest.representativePhone = convertPhoneNumberToInternationalFormat(
- trialRequest.representativePhone
- )
- }
- try {
- const { data, error } = await useAsyncData('submit-trial-request', () =>
- ap2iRequestService.post(
- '/api/public/shop/new-structure-artist-premium-trial-request',
- trialRequest
- )
- )
- if (error.value) {
- throw error.value
- }
- console.log('Trial request submitted successfully:', data.value)
- trialRequestSent.value = true
- errorMsg.value = null
- // Scroll to top to show confirmation message
- setTimeout(() => router.push({ path: '', hash: '#anchor' }), 30)
- } catch (e) {
- console.error('Error submitting trial request:', e)
- // Process the error message using the common error handler
- const processedError = processApiError(e)
- // Set the error message directly from the processed error
- // The processApiError function now returns either a translated message
- // or a generic error message as appropriate
- errorMsg.value = processedError
- }
- }
- // Event handler for structureName updates
- const onStructureNameUpdated = (newName: string) => {
- if (!structureIdentifierModified.value && newName) {
- trialRequest.structureIdentifier = slugify(
- trialRequest.structureName
- ).replace(/[-_]/g, '')
- checkSubdomainAvailabilityDebounced()
- }
- }
- // Event handler for structureIdentifier updates
- const onStructureIdentifierUpdated = () => {
- structureIdentifierModified.value = true
- checkSubdomainAvailabilityDebounced()
- }
- </script>
- <style scoped lang="scss">
- .background-container {
- background-image: url('/images/logos/opentalent/Logo_Opentalent_Griffe.png');
- background-size: 700px;
- min-height: 100vh;
- }
- .trial-container {
- max-width: 1200px;
- margin: 0 auto;
- padding: 2rem;
- }
- .form-card {
- box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
- max-width: 90%;
- margin: 0 auto;
- padding: 2rem;
- }
- h1 {
- font-size: 2.5rem;
- font-weight: 700;
- color: var(--primary-color);
- text-decoration: underline var(--artist-color) 3px solid;
- margin-bottom: 2rem;
- @media (max-width: 768px) {
- font-size: 1.8rem;
- }
- }
- .description {
- font-size: 1.1rem;
- line-height: 1.6;
- margin-bottom: 2rem;
- p {
- margin-bottom: 1rem;
- }
- }
- .benefits-list {
- list-style: none;
- padding-left: 1rem;
- margin: 1.5rem 0;
- li {
- margin-bottom: 0.5rem;
- }
- }
- .section-title {
- margin-top: 2rem;
- font-size: 1.5rem;
- font-weight: 600;
- color: var(--primary-color);
- text-decoration: underline var(--artist-color) 3px solid;
- margin-bottom: 1rem;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- }
- .v-form {
- max-width: 1200px;
- margin: 0 auto;
- .submit-btn {
- font-weight: 600;
- letter-spacing: 0.05em;
- padding: 0 2rem;
- }
- .error {
- color: var(--warning-color);
- width: 80%;
- margin: 0.7em auto 2em;
- text-align: center;
- font-size: 1.05rem;
- font-weight: 600;
- border: 2px solid var(--warning-color);
- border-radius: 4px;
- padding: 0.5rem;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .legal {
- opacity: 0.7;
- font-size: 14px;
- font-style: italic;
- margin: 2rem auto;
- max-width: 80%;
- }
- }
- .no-credit-card {
- font-size: 0.9rem;
- opacity: 0.8;
- margin-top: 1rem;
- }
- .confirmation-message {
- .v-card {
- max-width: 800px;
- padding: 2rem;
- margin: 4rem 0;
- .v-card-title {
- font-size: 1.8rem;
- font-weight: 700;
- color: var(--primary-color);
- }
- .v-card-text {
- font-size: 1.1rem;
- line-height: 1.6;
- p {
- margin-bottom: 1rem;
- }
- }
- }
- }
- .dev-tools-container {
- position: absolute;
- bottom: 10px;
- right: 10px;
- z-index: 10;
- }
- </style>
|