artist-premium.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785
  1. <template>
  2. <div class="theme-artist">
  3. <CommonMeta
  4. title="Essai gratuit Opentalent Artist Premium - 30 jours sans engagement"
  5. description="Essayez gratuitement Opentalent Artist Premium pendant 30 jours. Solution complète pour orchestres, chorales, compagnies de théâtre, de danse ou de cirque."
  6. />
  7. <div class="background-container">
  8. <LayoutContainer class="trial-container">
  9. <div id="anchor" />
  10. <v-card class="form-card">
  11. <v-card-text>
  12. <h1 class="text-center mb-6">
  13. Essayez gratuitement Opentalent Artist Premium pendant 30 jours !
  14. </h1>
  15. <div class="description mb-8">
  16. <p>
  17. Opentalent Artist Premium est une solution en ligne complète,
  18. pensée pour les orchestres, chorales, compagnies de théâtre, de
  19. danse ou de cirque. Elle vous aide à gagner du temps dans
  20. l'organisation de vos activités, à mieux collaborer avec vos
  21. équipes et à renforcer votre visibilité auprès de votre public.
  22. </p>
  23. <p>
  24. Pendant 30 jours, profitez de toutes les fonctionnalités
  25. d'Opentalent Artist Premium, gratuitement et sans engagement :
  26. </p>
  27. <ul class="benefits-list">
  28. <li>
  29. <span class="mr-1">✔️</span> Gestion intuitive des membres et
  30. des événements
  31. </li>
  32. <li>
  33. <span class="mr-1">✔️</span> Planification avancée des
  34. répétitions, spectacles et tournées
  35. </li>
  36. <li>
  37. <span class="mr-1">✔️</span> Outils de communication intégrés
  38. (emails, publipostage, etc.)
  39. </li>
  40. <li>
  41. <span class="mr-1">✔️</span> Site web personnalisable pour
  42. présenter vos projets et votre structure
  43. </li>
  44. <li>
  45. <span class="mr-1">✔️</span> Accès collaboratif pour vos
  46. équipes, en temps réel
  47. </li>
  48. </ul>
  49. <p>
  50. Il vous suffit de remplir le formulaire ci-dessous pour activer
  51. votre essai gratuit.
  52. </p>
  53. <p>
  54. Lancez-vous dès aujourd'hui et découvrez comment Opentalent peut
  55. transformer votre organisation artistique !
  56. </p>
  57. </div>
  58. <v-form
  59. v-if="!trialRequestSent"
  60. ref="form"
  61. validate-on="submit lazy"
  62. @submit.prevent="submit"
  63. >
  64. <v-container>
  65. <div v-if="isDevelopment" class="dev-tools-container">
  66. <v-btn
  67. color="info"
  68. size="small"
  69. prepend-icon="fa fa-magic"
  70. @click="fillWithDummyData"
  71. >
  72. Remplir avec des données de test
  73. </v-btn>
  74. </div>
  75. <i
  76. >Les champs dont le nom est suivi d'un astérisque (*) sont
  77. obligatoires.</i
  78. >
  79. <h2 class="section-title">Coordonnées de la structure</h2>
  80. <!-- Structure name -->
  81. <v-row>
  82. <v-col cols="12">
  83. <v-text-field
  84. v-model="trialRequest.structureName"
  85. :rules="[validateRequired]"
  86. label="Nom de la structure*"
  87. required
  88. @input="onStructureNameUpdated"
  89. />
  90. </v-col>
  91. </v-row>
  92. <!-- Structure address -->
  93. <v-row>
  94. <v-col cols="12" md="6">
  95. <v-text-field
  96. v-model="trialRequest.address"
  97. :rules="[validateRequired]"
  98. label="Adresse siège social de la structure*"
  99. required
  100. />
  101. </v-col>
  102. <v-col cols="12" md="6">
  103. <v-text-field
  104. v-model="trialRequest.addressComplement"
  105. label="Adresse (suite)"
  106. />
  107. </v-col>
  108. </v-row>
  109. <v-row>
  110. <v-col cols="12" md="6">
  111. <v-text-field
  112. v-model="trialRequest.postalCode"
  113. :rules="[validateRequired, validatePostalCode]"
  114. label="Code postal*"
  115. required
  116. />
  117. </v-col>
  118. <v-col cols="12" md="6">
  119. <v-text-field
  120. v-model="trialRequest.city"
  121. :rules="[validateRequired]"
  122. label="Ville*"
  123. required
  124. />
  125. </v-col>
  126. </v-row>
  127. <!-- Structure email and SIREN -->
  128. <v-row>
  129. <v-col cols="12" md="6">
  130. <v-text-field
  131. v-model="trialRequest.structureEmail"
  132. :rules="[validateRequired, validateEmail]"
  133. label="Adresse mail de la structure*"
  134. required
  135. type="email"
  136. />
  137. </v-col>
  138. <v-col cols="12" md="6">
  139. <v-text-field
  140. v-model="trialRequest.siren"
  141. :rules="[validateSiren]"
  142. label="SIREN (optionnel)"
  143. hint="Numéro à 9 chiffres"
  144. />
  145. </v-col>
  146. </v-row>
  147. <!-- Structure type and legal status -->
  148. <v-row>
  149. <v-col cols="12" md="6">
  150. <v-select
  151. v-model="trialRequest.structureType"
  152. :rules="[validateRequired]"
  153. label="Type de la structure*"
  154. :items="structureTypes"
  155. item-value="value"
  156. item-title="title"
  157. required
  158. />
  159. </v-col>
  160. <v-col cols="12" md="6">
  161. <v-select
  162. v-model="trialRequest.legalStatus"
  163. :rules="[validateRequired]"
  164. label="Statut juridique*"
  165. :items="legalStatuses"
  166. item-value="value"
  167. item-title="title"
  168. required
  169. />
  170. </v-col>
  171. </v-row>
  172. <!-- Structure identifier -->
  173. <v-row>
  174. <v-col cols="12" md="6" class="mx-auto">
  175. <v-text-field
  176. v-model="trialRequest.structureIdentifier"
  177. :rules="[
  178. validateRequired,
  179. validateSubdomain,
  180. validateSubdomainAvailability,
  181. ]"
  182. label="Identifiant de la structure*"
  183. required
  184. class="text-center"
  185. @input="onStructureIdentifierUpdated"
  186. />
  187. <div class="validationMessage">
  188. <span v-if="validationPending">
  189. <v-progress-circular size="16" indeterminate />
  190. <i class="ml-2">Vérification en cours</i>
  191. </span>
  192. <span
  193. v-else-if="subdomainAvailable === true"
  194. class="text-success"
  195. >
  196. <v-icon>fa fa-check</v-icon>
  197. <i class="ml-2"> Cet identifiant est disponible</i>
  198. </span>
  199. <span
  200. v-else-if="subdomainAvailable === false"
  201. class="text-error"
  202. >
  203. <v-icon>fa fa-x</v-icon>
  204. <i class="ml-2">Cet identifiant n'est pas disponible</i>
  205. </span>
  206. </div>
  207. </v-col>
  208. </v-row>
  209. <h2 class="section-title">Représentée par</h2>
  210. <!-- Representative function -->
  211. <v-row>
  212. <v-col cols="12">
  213. <v-text-field
  214. v-model="trialRequest.representativeFunction"
  215. :rules="[validateRequired]"
  216. label="Fonction*"
  217. required
  218. />
  219. </v-col>
  220. </v-row>
  221. <!-- Representative name -->
  222. <v-row>
  223. <v-col cols="12" md="6">
  224. <v-text-field
  225. v-model="trialRequest.representativeFirstName"
  226. :rules="[validateRequired]"
  227. label="Prénom*"
  228. required
  229. />
  230. </v-col>
  231. <v-col cols="12" md="6">
  232. <v-text-field
  233. v-model="trialRequest.representativeLastName"
  234. :rules="[validateRequired]"
  235. label="Nom*"
  236. required
  237. />
  238. </v-col>
  239. </v-row>
  240. <!-- Representative contact -->
  241. <v-row>
  242. <v-col cols="12" md="6">
  243. <v-text-field
  244. v-model="trialRequest.representativeEmail"
  245. :rules="[validateRequired, validateEmail]"
  246. label="Adresse mail*"
  247. required
  248. type="email"
  249. />
  250. </v-col>
  251. <v-col cols="12" md="6">
  252. <v-text-field
  253. v-model="trialRequest.representativePhone"
  254. :rules="[validateRequired, validatePhone]"
  255. label="Téléphone*"
  256. required
  257. type="tel"
  258. />
  259. </v-col>
  260. </v-row>
  261. <h2 class="section-title">Accord de termes et conditions</h2>
  262. <!-- Terms checkboxes -->
  263. <v-checkbox
  264. v-model="trialRequest.termsAccepted"
  265. :rules="[validateCheckbox]"
  266. required
  267. >
  268. <template #label>
  269. Mon organisme accepte les &nbsp;
  270. <a
  271. href="https://maestro.opentalent.fr/uploads/share/Documents_juridique/CGU.pdf"
  272. target="_blank"
  273. >
  274. conditions générales d'utilisation </a
  275. >.*
  276. </template>
  277. </v-checkbox>
  278. <v-checkbox
  279. v-model="trialRequest.legalRepresentative"
  280. :rules="[validateCheckbox]"
  281. label="J'agis en tant que représentant légal de l'association ou de la structure.*"
  282. required
  283. />
  284. <v-checkbox
  285. v-model="trialRequest.newsletterSubscription"
  286. label="J'accepte de recevoir la lettre d'information culturelle afin de découvrir des idées de sorties adaptées à ma région."
  287. />
  288. <div class="d-flex flex-row justify-center">
  289. <LayoutCaptcha />
  290. </div>
  291. <!-- Submit Button -->
  292. <div class="d-flex flex-row justify-center my-10">
  293. <v-btn
  294. type="submit"
  295. color="secondary"
  296. size="large"
  297. class="submit-btn"
  298. >
  299. COMMENCER MON ESSAI DE 30 JOURS
  300. </v-btn>
  301. </div>
  302. <p class="text-center no-credit-card">
  303. Aucune carte de crédit requise. En cliquant sur "Commencer Mon
  304. essai de 30 jours", vous acceptez de démarrer votre période
  305. d'essai gratuit.
  306. </p>
  307. <div v-if="errorMsg" class="error">
  308. {{ errorMsg }}
  309. </div>
  310. </v-container>
  311. <div class="legal">
  312. Les données recueillies par Opentalent sont utilisées pour le
  313. traitement de votre demande et pour vous informer sur nos
  314. offres. Elles sont destinées aux services Opentalent et à ses
  315. sous-traitants pour l'exécution des contrats. Conformément à la
  316. loi "Informatique et Libertés du 6 Janvier 1978", vous disposez
  317. d'un droit d'accès, de modifications, de rectification et de
  318. suppression des données vous concernant. Pour toute demande,
  319. adressez-vous à : OPENTALENT, 265 rue de la Grange 74950
  320. SCIONZIER - FRANCE, opentalent.fr s'engage à la confidentialité
  321. et à la protection de vos données."
  322. </div>
  323. </v-form>
  324. <div
  325. v-else
  326. class="confirmation-message d-flex flex-row justify-center"
  327. >
  328. <v-card>
  329. <v-card-title class="text-center">
  330. <v-icon
  331. icon="fas fa-check mr-2"
  332. color="success"
  333. max-height="48"
  334. />
  335. Félicitations !
  336. </v-card-title>
  337. <v-card-text class="text-center">
  338. <p>
  339. Votre essai gratuit de 30 jours d'Opentalent Artist Premium
  340. a bien été activé.
  341. </p>
  342. <p>
  343. Vous allez recevoir un email avec vos identifiants de
  344. connexion et toutes les informations nécessaires pour
  345. commencer à utiliser la plateforme.
  346. </p>
  347. <p>
  348. Notre équipe reste à votre disposition pour vous accompagner
  349. durant cette période d'essai.
  350. </p>
  351. </v-card-text>
  352. <v-card-actions class="justify-center">
  353. <v-btn color="primary" to="/opentalent-artist">
  354. Retour à la page Opentalent Artist
  355. </v-btn>
  356. </v-card-actions>
  357. </v-card>
  358. </div>
  359. </v-card-text>
  360. </v-card>
  361. </LayoutContainer>
  362. </div>
  363. </div>
  364. </template>
  365. <script setup lang="ts">
  366. import { useRouter } from 'vue-router'
  367. import type { Ref } from 'vue'
  368. import { reactive } from 'vue'
  369. import _ from 'lodash'
  370. import { parsePhoneNumber, isValidPhoneNumber } from 'libphonenumber-js'
  371. import { useRuntimeConfig, useAsyncData, useNuxtApp } from '#app'
  372. import type { TrialRequest } from '~/types/interface'
  373. import { STRUCTURE_TYPE, LEGAL_STATUS } from '~/types/types'
  374. import { useAp2iRequestService } from '~/composables/data/useAp2iRequestService'
  375. import { slugify } from '~/services/utils/stringUtils'
  376. const router = useRouter()
  377. const form: Ref<HTMLElement | null> = ref(null)
  378. // Structure types and legal statuses
  379. const structureTypes = Object.values(STRUCTURE_TYPE)
  380. .map((item) => ({
  381. value: item.key,
  382. title: item.label,
  383. }))
  384. .sort((a, b) => (a.title > b.title ? 1 : -1))
  385. const legalStatuses = Object.values(LEGAL_STATUS)
  386. .map((item) => ({
  387. value: item.key,
  388. title: item.label,
  389. }))
  390. .sort((a, b) => (a.title > b.title ? 1 : -1))
  391. // Get apiRequestService for subdomain availability check
  392. const { ap2iRequestService } = useAp2iRequestService()
  393. // Check if we're in a development environment
  394. const config = useRuntimeConfig()
  395. const isDevelopment = computed(() => config.public.env === 'dev')
  396. // Trial request data
  397. const trialRequest = reactive<TrialRequest>({
  398. structureName: '',
  399. address: '',
  400. addressComplement: '',
  401. postalCode: '',
  402. city: '',
  403. structureEmail: '',
  404. structureType: 'ARTISTIC_PRACTICE_ONLY',
  405. legalStatus: 'ASSOCIATION_LAW_1901',
  406. structureIdentifier: '',
  407. siren: '',
  408. representativeFirstName: '',
  409. representativeLastName: '',
  410. representativeFunction: '',
  411. representativeEmail: '',
  412. representativePhone: '',
  413. termsAccepted: false,
  414. legalRepresentative: false,
  415. newsletterSubscription: false,
  416. })
  417. // Function to fill the form with dummy data
  418. const fillWithDummyData = () => {
  419. trialRequest.structureName = 'Compagnie Artistique Test'
  420. trialRequest.address = '123 Rue des Arts'
  421. trialRequest.addressComplement = 'Bâtiment B'
  422. trialRequest.postalCode = '75001'
  423. trialRequest.city = 'Paris'
  424. trialRequest.structureEmail = 'contact@compagnie-test.fr'
  425. trialRequest.structureType = 'ARTISTIC_PRACTICE_ONLY'
  426. trialRequest.legalStatus = 'ASSOCIATION_LAW_1901'
  427. trialRequest.structureIdentifier =
  428. 'compagnie-test-' + Math.floor(Math.random() * 1000)
  429. trialRequest.siren = '123456789'
  430. trialRequest.representativeFirstName = 'Jean'
  431. trialRequest.representativeLastName = 'Dupont'
  432. trialRequest.representativeFunction = 'Directeur Artistique'
  433. trialRequest.representativeEmail = 'jean.dupont@compagnie-test.fr'
  434. trialRequest.representativePhone = '0612345678'
  435. trialRequest.termsAccepted = true
  436. trialRequest.legalRepresentative = true
  437. trialRequest.newsletterSubscription = true
  438. // Trigger subdomain availability check
  439. checkSubdomainAvailabilityDebounced()
  440. }
  441. // Track if structure identifier has been manually modified
  442. const structureIdentifierModified = ref(false)
  443. // Variables for subdomain validation
  444. const validationPending = ref(false)
  445. const subdomainAvailable = ref<boolean | null>(null)
  446. const checkSubdomainAvailability = async (subdomain: string) => {
  447. if (!subdomain || validateSubdomain(subdomain) !== true) {
  448. subdomainAvailable.value = null
  449. return false
  450. }
  451. validationPending.value = true
  452. try {
  453. const subdomainAvailability = await ap2iRequestService.get(
  454. '/api/public/subdomains/is_available',
  455. { subdomain }
  456. )
  457. subdomainAvailable.value =
  458. subdomainAvailability && subdomainAvailability.available === true
  459. validationPending.value = false
  460. return subdomainAvailable.value
  461. } catch (error) {
  462. console.error('Error checking subdomain availability:', error)
  463. subdomainAvailable.value = false
  464. validationPending.value = false
  465. return false
  466. }
  467. }
  468. /**
  469. * Version debounced de la fonction checkAvailability
  470. * @see https://docs-lodash.com/v4/debounce/
  471. */
  472. const checkSubdomainAvailabilityDebounced: _.DebouncedFunc<() => void> =
  473. _.debounce(
  474. async () =>
  475. await checkSubdomainAvailability(trialRequest.structureIdentifier),
  476. 600
  477. )
  478. // Validation rules
  479. const validateRequired = (value: string) =>
  480. !!value || 'Ce champ est obligatoire'
  481. const validateEmail = (email: string) =>
  482. /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || 'Adresse email invalide'
  483. const validatePostalCode = (postalCode: string) =>
  484. /^\d{5}$/.test(postalCode) || 'Code postal invalide (5 chiffres)'
  485. const validatePhone = (phone: string) => {
  486. try {
  487. // Assume French phone number if no country code is provided
  488. return isValidPhoneNumber(phone, 'FR') || 'Numéro de téléphone invalide'
  489. } catch (error) {
  490. return 'Numéro de téléphone invalide'
  491. }
  492. }
  493. const validateSiren = (siren: string) =>
  494. !siren || /^\d{9}$/.test(siren) || 'SIREN invalide (9 chiffres)'
  495. const validateCheckbox = (value: boolean) =>
  496. value || 'Vous devez accepter cette condition'
  497. const validateSubdomain = (value: string) => {
  498. if (!value) return 'Ce champ est obligatoire'
  499. const regex = /^[a-z0-9][a-z0-9-]{0,28}[a-z0-9]$/
  500. return (
  501. regex.test(value) ||
  502. '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.'
  503. )
  504. }
  505. const validateSubdomainAvailability = (value: string) => {
  506. if (!value) return ''
  507. return (
  508. subdomainAvailable.value === true || "Cet identifiant n'est pas disponible"
  509. )
  510. }
  511. // Form state
  512. const trialRequestSent: Ref<boolean> = ref(false)
  513. const errorMsg: Ref<string | null> = ref(null)
  514. // Function to convert phone number to international format
  515. const convertToInternationalFormat = (phone: string): string => {
  516. try {
  517. // Assume French phone number if no country code is provided
  518. const phoneNumber = parsePhoneNumber(phone, 'FR')
  519. if (phoneNumber && phoneNumber.isValid()) {
  520. return phoneNumber.format('E.164') // E.164 format: +33123456789
  521. }
  522. return phone
  523. } catch (error) {
  524. console.error('Error converting phone number:', error)
  525. return phone
  526. }
  527. }
  528. // Submit function
  529. const submit = async (): Promise<void> => {
  530. const { valid } = await form.value!.validate()
  531. if (!valid) {
  532. return
  533. }
  534. // Convert phone number to international format before submission
  535. trialRequest.representativePhone = convertToInternationalFormat(
  536. trialRequest.representativePhone
  537. )
  538. try {
  539. const { data, error } = await useAsyncData('submit-trial-request', () =>
  540. ap2iRequestService.post(
  541. '/api/public/shop/new-structure-artist-premium-trial-request',
  542. trialRequest
  543. )
  544. )
  545. if (error.value) {
  546. throw error.value
  547. }
  548. console.log('Trial request submitted successfully:', data.value)
  549. trialRequestSent.value = true
  550. errorMsg.value = null
  551. // Scroll to top to show confirmation message
  552. setTimeout(() => router.push({ path: '', hash: '#anchor' }), 30)
  553. } catch (e) {
  554. console.error('Error submitting trial request:', e)
  555. errorMsg.value =
  556. "Une erreur s'est produite lors de l'activation de votre essai. Veuillez réessayer plus tard ou nous contacter directement."
  557. // Try to extract the specific error message from the API response
  558. if (
  559. e &&
  560. typeof e === 'object' &&
  561. 'data' in e &&
  562. e.data &&
  563. typeof e.data === 'object'
  564. ) {
  565. const errorData = e.data as { detail?: string }
  566. if (errorData.detail) {
  567. const { $i18n } = useNuxtApp()
  568. // Use translation if available, otherwise use the original message
  569. errorMsg.value += '\n' + ($i18n.t(errorData.detail) || errorData.detail)
  570. }
  571. }
  572. }
  573. }
  574. // Event handler for structureName updates
  575. const onStructureNameUpdated = (newName: string) => {
  576. if (!structureIdentifierModified.value && newName) {
  577. trialRequest.structureIdentifier = slugify(trialRequest.structureName)
  578. checkSubdomainAvailabilityDebounced()
  579. }
  580. }
  581. // Event handler for structureIdentifier updates
  582. const onStructureIdentifierUpdated = () => {
  583. structureIdentifierModified.value = true
  584. checkSubdomainAvailabilityDebounced()
  585. }
  586. </script>
  587. <style scoped lang="scss">
  588. .background-container {
  589. background-image: url('/images/logos/opentalent/Logo_Opentalent_Griffe.png');
  590. background-size: 700px;
  591. min-height: 100vh;
  592. }
  593. .trial-container {
  594. max-width: 1200px;
  595. margin: 0 auto;
  596. padding: 2rem;
  597. }
  598. .form-card {
  599. box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
  600. max-width: 90%;
  601. margin: 0 auto;
  602. padding: 2rem;
  603. }
  604. h1 {
  605. font-size: 2.5rem;
  606. font-weight: 700;
  607. color: var(--primary-color);
  608. text-decoration: underline var(--artist-color) 3px solid;
  609. margin-bottom: 2rem;
  610. @media (max-width: 768px) {
  611. font-size: 1.8rem;
  612. }
  613. }
  614. .description {
  615. font-size: 1.1rem;
  616. line-height: 1.6;
  617. margin-bottom: 2rem;
  618. p {
  619. margin-bottom: 1rem;
  620. }
  621. }
  622. .benefits-list {
  623. list-style: none;
  624. padding-left: 1rem;
  625. margin: 1.5rem 0;
  626. li {
  627. margin-bottom: 0.5rem;
  628. }
  629. }
  630. .section-title {
  631. margin-top: 2rem;
  632. font-size: 1.5rem;
  633. font-weight: 600;
  634. color: var(--primary-color);
  635. text-decoration: underline var(--artist-color) 3px solid;
  636. margin-bottom: 1rem;
  637. text-transform: uppercase;
  638. letter-spacing: 0.05em;
  639. }
  640. .v-form {
  641. max-width: 1200px;
  642. margin: 0 auto;
  643. .submit-btn {
  644. font-weight: 600;
  645. letter-spacing: 0.05em;
  646. padding: 0 2rem;
  647. }
  648. .error {
  649. color: var(--warning-color);
  650. width: 80%;
  651. margin: 0 auto 2em;
  652. text-align: center;
  653. }
  654. .legal {
  655. opacity: 0.7;
  656. font-size: 14px;
  657. font-style: italic;
  658. margin: 2rem auto;
  659. max-width: 80%;
  660. }
  661. }
  662. .no-credit-card {
  663. font-size: 0.9rem;
  664. opacity: 0.8;
  665. margin-top: 1rem;
  666. }
  667. .confirmation-message {
  668. .v-card {
  669. max-width: 800px;
  670. padding: 2rem;
  671. margin: 4rem 0;
  672. .v-card-title {
  673. font-size: 1.8rem;
  674. font-weight: 700;
  675. color: var(--primary-color);
  676. }
  677. .v-card-text {
  678. font-size: 1.1rem;
  679. line-height: 1.6;
  680. p {
  681. margin-bottom: 1rem;
  682. }
  683. }
  684. }
  685. }
  686. .dev-tools-container {
  687. position: absolute;
  688. bottom: 10px;
  689. right: 10px;
  690. z-index: 10;
  691. }
  692. </style>