HTML
xxxxxxxxxx
1
<!-- External assets (add in MDB snippet editor under "Add external resources")
2
CSS:
3
https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/6.4.0/mdb.min.css
4
JS:
5
https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/6.4.0/mdb.min.js
6
-->
7
8
<!-- Sticky Header -->
9
<header class="position-fixed top-0 start-0 w-100 bg-white border-bottom py-2 px-3 d-flex justify-content-between align-items-center" style="z-index: 1050;">
10
<div class="d-flex gap-3 align-items-center">
11
<a href="#" title="Personvern" class="text-muted"><i class="fas fa-shield-alt"></i></a>
12
<a href="#" title="Tilgjengelighet" class="text-muted"><i class="fas fa-universal-access"></i></a>
13
<select class="form-select form-select-sm" aria-label="Språkvalg" style="width: auto;">
14
<option selected>NO</option>
15
<option value="en">EN</option>
16
</select>
17
</div>
18
<div>
19
<span id="cart-summary" class="badge bg-primary px-3 py-2" aria-live="polite">
20
🛒 0 valg – NOK 0
21
</span>
22
</div>
23
</header>
24
25
<div class="container py-5 mt-5">
26
<!-- Header -->
27
<div class="text-center mb-5">
28
<img src="https://cdn.arkon.no/arkon/accounts/1766/uploaded/css/4e472842-08f6-11f0-9d2d-02da4459bb77.png" alt="Logo" class="img-fluid mb-4" style="max-height: 80px;">
29
<h1 class="h3 fw-bold">Lorem ipsum dolor sit amet</h1>
30
<p class="lead text-muted">Consectetur adipiscing elit. Morbi ut bibendum nulla, ac fermentum purus. Vennligst fyll ut skjemaet for å registrere deg.</p>
31
</div>
32
33
<form id="registration-form" class="needs-validation" novalidate>
34
<div class="card border shadow-sm p-4 mb-4">
35
<h5>Personlig informasjon</h5>
36
37
<div class="mb-3">
38
<label class="form-label" for="first-name">Fornavn</label>
39
<input type="text" id="first-name" class="form-control" required />
40
<div class="form-text">Skriv inn ditt fornavn.</div>
41
<div class="invalid-feedback">Vennligst fyll ut fornavn.</div>
42
</div>
43
44
<div class="mb-3">
45
<label class="form-label" for="last-name">Etternavn</label>
46
<input type="text" id="last-name" class="form-control" required />
47
<div class="form-text">Skriv inn ditt etternavn.</div>
48
<div class="invalid-feedback">Vennligst fyll ut etternavn.</div>
49
</div>
50
51
<div class="mb-3">
52
<label class="form-label" for="email">E-post</label>
53
<input type="email" id="email" class="form-control" required />
54
<div class="form-text">Vi sender bekreftelse til denne adressen.</div>
55
<div class="invalid-feedback">Oppgi en gyldig e-postadresse.</div>
56
</div>
57
58
59
<div class="mb-3">
60
<label class="form-label d-block">Telefonnummer</label>
61
<div class="input-group">
62
<select class="form-select" id="prefix" name="prefix" style="max-width: 150px;" required>
63
<option value="+47" selected>🇳🇴 NOR +47</option>
64
<option value="+46">🇸🇪 SWE +46</option>
65
<option value="+45">🇩🇰 DEN +45</option>
66
<option value="+358">🇫🇮 FIN +358</option>
67
</select>
68
<input type="tel" id="phone" name="phone" class="form-control" placeholder="12345678" required>
69
</div>
70
<div class="invalid-feedback">Vennligst oppgi et telefonnummer.</div>
71
</div>
72
73
74
75
<div class="mb-3">
76
<label class="form-label" for="datepicker">Velg dato</label>
77
<input type="text" class="form-control" id="datepicker" />
78
<div class="form-text">Velg en dato for deltakelse.</div>
79
<div class="invalid-feedback">Vennligst velg en dato.</div>
80
<div id="selected-date-display" class="form-text text-muted mt-1"></div>
81
</div>
82
83
<div class="mb-3">
84
<label class="form-label">Datointervall</label>
85
<div class="row g-2">
86
<div class="col-sm-6">
87
<input type="text" class="form-control" id="start-date" placeholder="Startdato" />
88
</div>
89
<div class="col-sm-6">
90
<input type="text" class="form-control" id="end-date" placeholder="Sluttdato" />
91
</div>
92
</div>
93
<div id="date-range-display" class="form-text text-muted mt-1"></div>
94
</div>
95
96
<div class="mb-3">
97
<label class="form-label" for="gender">Kjønn</label>
98
<select class="form-select" id="gender" required>
99
<option value="" disabled selected>Kjønn</option>
100
<option value="male">Mann</option>
101
<option value="female">Kvinne</option>
102
<option value="other">Annet</option>
103
</select>
104
<div class="invalid-feedback">Vennligst velg kjønn.</div>
105
</div>
106
</div>
107
108
<!-- Tilvalg -->
109
<div class="card border shadow-sm p-4 mb-4">
110
<fieldset id="extra-options" data-min="1" data-max="2">
111
<legend class="mb-3 fs-5">Valgfrie tillegg (1–2 valg tillatt)</legend>
112
<div class="form-check mb-2">
113
<input class="form-check-input price-item" type="checkbox" value="200" id="addon1">
114
<label class="form-check-label" for="addon1">Ekstra frokost – NOK 200</label>
115
</div>
116
<div class="form-check mb-2">
117
<input class="form-check-input price-item" type="checkbox" value="150" id="addon2">
118
<label class="form-check-label" for="addon2">Sen utsjekk – NOK 150</label>
119
</div>
120
<div class="form-check mb-2">
121
<input class="form-check-input price-item" type="checkbox" value="100" id="addon3">
122
<label class="form-check-label" for="addon3">Transport til flyplass – NOK 100</label>
123
</div>
124
<div id="group-feedback" class="form-text mt-2 text-muted"></div>
125
</fieldset>
126
</div>
127
128
<!-- Bildeopplasting med forhåndsvisning og crop -->
129
<div class="mb-4">
130
<label for="profile-image" class="form-label">Last opp profilbilde</label>
131
<input class="form-control" type="file" id="profile-image" name="profile-image" accept="image/*">
132
<div class="form-text">Velg et bilde i JPG eller PNG-format. Maks 5 MB.</div>
133
<div class="mt-3">
134
<img id="preview-image" src="#" alt="Forhåndsvisning" class="img-thumbnail d-none" style="max-height: 200px;">
135
</div>
136
<div class="mt-2">
137
<canvas id="cropped-canvas" class="d-none"></canvas>
138
</div>
139
</div>
140
141
<!-- Filopplasting (generelt) -->
142
<div class="mb-4">
143
<label for="attachment" class="form-label">Last opp vedlegg</label>
144
<input class="form-control" type="file" id="attachment" name="attachment">
145
<div class="form-text">Last opp dokumenter som PDF eller DOCX (maks 10 MB).</div>
146
</div>
147
</div>
148
149
</div>
150
151
<div class="text-end">
152
<button type="submit" class="btn btn-success">Send inn</button>
153
</div>
154
</form>
155
</div>
156
157
<script>
158
159
</script>
160
161
CSS
xxxxxxxxxx
1
<!-- External assets (add in MDB snippet editor under "Add external resources")
2
3
https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/6.4.0/mdb.min.css
4
https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css
5
https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/css/tom-select.css
6
JS:
7
https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/6.4.0/mdb.min.js
8
https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js
9
https://cdnjs.cloudflare.com/ajax/libs/libphonenumber-js/1.10.25/libphonenumber-js.min.js
10
https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js
11
https://ipapi.co/json/
12
-->
13
14
15
.form-text {
16
margin-top: .05rem;
17
font-size: .8em;
18
color: #757575;
19
}
20
21
.form-text {
22
display: none;
23
}
24
25
input:focus + .form-text,
26
select:focus + .form-text,
27
textarea:focus + .form-text {
28
display: block;
29
}
JS
xxxxxxxxxx
1
const dateInput = document.getElementById('datepicker');
2
const dateOutput = document.getElementById('selected-date-display');
3
dateInput.addEventListener('change', function () {
4
if (this.value) {
5
const date = new Date(this.value);
6
const options = { day: '2-digit', month: 'long', year: 'numeric' };
7
dateOutput.textContent = `Valgt dato: ${date.toLocaleDateString('no-NO', options)}`;
8
} else {
9
dateOutput.textContent = '';
10
}
11
});
12
13
// Prisberegning
14
function updateCartSummary() {
15
const checked = document.querySelectorAll('.price-item:checked');
16
const total = [...checked].reduce((sum, el) => sum + parseInt(el.value), 0);
17
const count = checked.length;
18
const cart = document.getElementById('cart-summary');
19
cart.innerText = `🛒 ${count} valg – NOK ${total}`;
20
}
21
22
document.querySelectorAll('.price-item').forEach(item => {
23
item.addEventListener('change', () => {// Tom Select init
24
new TomSelect("#phone-prefix", {
25
allowEmptyOption: false,
26
placeholder: "Velg land...",
27
render: {
28
option: function(data, escape) {
29
return data.html || `<div>${escape(data.text)}</div>`;
30
},
31
item: function(data, escape) {
32
return data.html || `<div>${escape(data.text)}</div>`;
33
}
34
}
35
});
36
37
const countryOptions = [
38
{ code: "NO", value: "+47", name: "Norway", flag: "no" },
39
{ code: "SE", value: "+46", name: "Sweden", flag: "se" },
40
{ code: "DK", value: "+45", name: "Denmark", flag: "dk" },
41
{ code: "FI", value: "+358", name: "Finland", flag: "fi" },
42
{ code: "GB", value: "+44", name: "UK", flag: "gb" },
43
{ code: "DE", value: "+49", name: "Germany", flag: "de" },
44
];
45
46
const prefixSelect = document.getElementById("phone-prefix");
47
countryOptions.forEach(({ code, value, name, flag }) => {
48
const option = document.createElement("option");
49
option.value = code;
50
option.innerHTML = `<img src='https://flagcdn.com/${flag}.svg' width='20' class='me-2'> ${value} ${name}`;
51
option.dataset.html = option.innerHTML;
52
prefixSelect.appendChild(option);
53
});
54
55
const ts = new TomSelect("#phone-prefix", {
56
allowEmptyOption: false,
57
render: {
58
option: function(data, escape) {
59
return data.html || `<div>${escape(data.text)}</div>`;
60
},
61
item: function(data, escape) {
62
return data.html || `<div>${escape(data.text)}</div>`;
63
}
64
}
65
});
66
67
fetch('https://ipapi.co/json/')
68
.then(res => res.json())
69
.then(data => {
70
const cc = data.country_code;
71
const found = countryOptions.find(opt => opt.code === cc);
72
if (found) ts.setValue(found.code);
73
});
74
75
const phoneInput = document.getElementById('phone-number');
76
const phoneError = document.getElementById('phone-error');
77
78
phoneInput.addEventListener('blur', () => {
79
const number = phoneInput.value;
80
const region = ts.getValue();
81
try {
82
const parsed = libphonenumber.parsePhoneNumber(number, region);
83
if (parsed && parsed.isValid()) {
84
phoneInput.classList.remove('is-invalid');
85
phoneError.style.display = 'none';
86
} else {
87
phoneInput.classList.add('is-invalid');
88
phoneError.style.display = 'block';
89
}
90
} catch {
91
phoneInput.classList.add('is-invalid');
92
phoneError.style.display = 'block';
93
}
94
});
95
96
document.getElementById('reset-phone').addEventListener('click', () => {
97
phoneInput.value = '';
98
ts.clear();
99
});
100
101
// Bilde crop
102
let cropper;
103
const profileImageInput = document.getElementById('profile-image');
104
const previewImage = document.getElementById('preview-image');
105
const croppedCanvas = document.getElementById('cropped-canvas');
106
const croppedInput = document.getElementById('croppedImage');
107
108
profileImageInput.addEventListener('change', function (e) {
109
const file = e.target.files[0];
110
if (file && file.type.startsWith('image/')) {
111
const reader = new FileReader();
112
reader.onload = function (event) {
113
previewImage.src = event.target.result;
114
previewImage.classList.remove('d-none');
115
if (cropper) cropper.destroy();
116
cropper = new Cropper(previewImage, {
117
aspectRatio: 1,
118
viewMode: 1,
119
autoCropArea: 1,
120
cropend() {
121
const canvas = cropper.getCroppedCanvas({ width: 200, height: 200 });
122
croppedCanvas.classList.remove('d-none');
123
croppedCanvas.width = canvas.width;
124
croppedCanvas.height = canvas.height;
125
const ctx = croppedCanvas.getContext('2d');
126
ctx.clearRect(0, 0, canvas.width, canvas.height);
127
ctx.drawImage(canvas, 0, 0);
128
croppedInput.value = canvas.toDataURL('image/png');
129
}
130
});
131
};
132
reader.readAsDataURL(file);
133
}
134
});
135
136
// Nullstill-knapp
137
const resetBtn = document.getElementById('reset-phone');
138
resetBtn.addEventListener('click', () => {
139
document.getElementById('phone-number').value = '';
140
const instance = document.querySelector('#phone-prefix').tomselect;
141
if (instance) instance.clear();
142
});
143
144
// Bildeopplasting med crop
145
let cropper;
146
const profileImageInput = document.getElementById('profile-image');
147
const previewImage = document.getElementById('preview-image');
148
const croppedCanvas = document.getElementById('cropped-canvas');
149
const croppedInput = document.getElementById('croppedImage');
150
151
profileImageInput.addEventListener('change', function (e) {
152
const file = e.target.files[0];
153
if (file && file.type.startsWith('image/')) {
154
const reader = new FileReader();
155
reader.onload = function (event) {
156
previewImage.src = event.target.result;
157
previewImage.classList.remove('d-none');
158
if (cropper) cropper.destroy();
159
cropper = new Cropper(previewImage, {
160
aspectRatio: 1,
161
viewMode: 1,
162
autoCropArea: 1,
163
cropend() {
164
const canvas = cropper.getCroppedCanvas({ width: 200, height: 200 });
165
croppedCanvas.classList.remove('d-none');
166
croppedCanvas.width = canvas.width;
167
croppedCanvas.height = canvas.height;
168
const ctx = croppedCanvas.getContext('2d');
169
ctx.clearRect(0, 0, canvas.width, canvas.height);
170
ctx.drawImage(canvas, 0, 0);
171
croppedInput.value = canvas.toDataURL('image/png');
172
}
173
});
174
};
175
reader.readAsDataURL(file);
176
}
177
});
178
updateCartSummary();
179
validateCheckboxGroup();
180
});
181
});
182
183
// Validering av min/max checkbokser
184
function validateCheckboxGroup() {
185
const group = document.getElementById('extra-options');
186
const min = parseInt(group.dataset.min);
187
const max = parseInt(group.dataset.max);
188
const selected = group.querySelectorAll('input[type="checkbox"]:checked').length;
189
const feedback = document.getElementById('group-feedback');
190
191
if (selected < min || selected > max) {
192
feedback.classList.remove('text-success');
193
feedback.classList.add('text-danger');
194
feedback.innerText = `Vennligst velg mellom ${min} og ${max} tillegg.`;
195
return false;
196
} else {
197
feedback.classList.remove('text-danger');
198
feedback.classList.add('text-success');
199
feedback.innerText = `Du har valgt ${selected} tillegg.`;
200
return true;
201
}
202
}
203
204
// Formvalidering
205
const form = document.getElementById('registration-form');
206
form.addEventListener('submit', function (e) {
207
let valid = form.checkValidity();
208
if (!validateCheckboxGroup()) valid = false;
209
210
if (!valid) {
211
e.preventDefault();
212
e.stopPropagation();
213
}
214
form.classList.add('was-validated');
215
});
216
217
// Initialiser MDB Datepicker
218
const datepickerEl = document.getElementById('datepicker');
219
if (datepickerEl) {
220
new mdb.Datepicker(datepickerEl, {
221
format: 'dd.mm.yyyy',
222
weekStart: 1,
223
disablePast: true,
224
inline: false
225
});
226
227
// Forhåndsvis valgt dato
228
datepickerEl.addEventListener('change', function () {
229
const selected = this.value;
230
const output = document.getElementById('selected-date-display');
231
output.textContent = selected ? `Valgt dato: ${selected}` : '';
232
});
233
}
Console errors: 0