@energysage/es-ds-components 3.0.0-alpha.6 → 3.0.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/.eslintignore +9 -0
  2. package/.eslintrc.json +3 -0
  3. package/.prettierrc.json +10 -0
  4. package/components/es-accordion-list.vue +112 -0
  5. package/components/es-accordion.vue +7 -0
  6. package/components/es-badge.vue +20 -0
  7. package/components/es-breadcrumbs.vue +43 -32
  8. package/components/es-button.vue +112 -0
  9. package/components/es-card.vue +64 -0
  10. package/components/es-carousel.vue +160 -0
  11. package/components/{b-col.vue → es-col.vue} +11 -11
  12. package/components/es-collapse.vue +38 -36
  13. package/components/es-cta-banner.vue +95 -0
  14. package/components/es-cta-card.vue +60 -0
  15. package/components/es-data-table-simple.vue +73 -0
  16. package/components/es-data-table.vue +126 -0
  17. package/components/es-error-page.vue +120 -0
  18. package/components/es-file-input.vue +331 -0
  19. package/components/es-file-preview-modal.vue +61 -0
  20. package/components/es-footer.vue +163 -0
  21. package/components/es-form-checkbox.vue +40 -0
  22. package/components/es-form-input.vue +130 -0
  23. package/components/es-form-msg.vue +81 -0
  24. package/components/es-form-radio-card.vue +57 -0
  25. package/components/es-form-radio-cards.vue +78 -0
  26. package/components/es-form-textarea.vue +124 -0
  27. package/components/es-form.vue +16 -0
  28. package/components/es-modal.vue +103 -0
  29. package/components/es-nav-bar-account-menu.vue +67 -0
  30. package/components/es-nav-bar-featured-article.vue +60 -0
  31. package/components/es-nav-bar-link.vue +27 -0
  32. package/components/es-nav-bar-product-menu.vue +215 -0
  33. package/components/es-nav-bar-top-level-menu.vue +193 -0
  34. package/components/es-nav-bar-topic-menu.vue +194 -0
  35. package/components/es-nav-bar.vue +396 -0
  36. package/components/es-pagination.vue +66 -0
  37. package/components/es-popover.vue +113 -0
  38. package/components/es-progress-circle.vue +81 -0
  39. package/components/es-progress.vue +36 -0
  40. package/components/es-radio-button-group.vue +53 -0
  41. package/components/es-radio-button.vue +50 -57
  42. package/components/es-rating.vue +120 -0
  43. package/components/es-reviews-io-card-carousel.vue +275 -0
  44. package/components/es-slider.vue +147 -0
  45. package/components/es-spinner.vue +22 -0
  46. package/components/es-support-card.vue +150 -0
  47. package/components/es-support.vue +66 -0
  48. package/components/es-tab.vue +7 -0
  49. package/components/es-tabs.vue +67 -0
  50. package/components/es-verification-code.vue +94 -0
  51. package/components/es-view-more.vue +63 -0
  52. package/components/es-zip-code-form.vue +204 -0
  53. package/components/icon/arrow-clockwise.vue +51 -0
  54. package/components/icon/arrow-counter-clockwise.vue +37 -0
  55. package/components/icon/arrow-cycle.vue +43 -0
  56. package/components/icon/arrow-left.vue +39 -0
  57. package/components/icon/arrow-right.vue +39 -0
  58. package/components/icon/arrow-up.vue +39 -0
  59. package/components/icon/arrow.vue +68 -0
  60. package/components/icon/bank.vue +38 -0
  61. package/components/icon/battery.vue +40 -0
  62. package/components/icon/bell.vue +42 -0
  63. package/components/icon/block.vue +46 -0
  64. package/components/icon/bolt.vue +38 -0
  65. package/components/icon/business.vue +42 -0
  66. package/components/icon/calculator.vue +37 -0
  67. package/components/icon/calendar.vue +46 -0
  68. package/components/icon/cart.vue +46 -0
  69. package/components/icon/chat-add.vue +44 -0
  70. package/components/icon/chat-approve.vue +42 -0
  71. package/components/icon/chat-bubble.vue +40 -0
  72. package/components/icon/chat-dots.vue +42 -0
  73. package/components/icon/check.vue +39 -0
  74. package/components/{icon-chevron-down.vue → icon/chevron-down.vue} +16 -16
  75. package/components/icon/chevron-horizontal.vue +39 -0
  76. package/components/icon/chevron-left.vue +39 -0
  77. package/components/{icon-chevron-right.vue → icon/chevron-right.vue} +16 -16
  78. package/components/icon/chevron-up.vue +39 -0
  79. package/components/icon/circle-alert.vue +46 -0
  80. package/components/icon/circle-check.vue +46 -0
  81. package/components/icon/circle-chevron-down.vue +47 -0
  82. package/components/icon/circle-chevron-left.vue +46 -0
  83. package/components/icon/circle-chevron-right.vue +46 -0
  84. package/components/icon/circle-chevron-up.vue +46 -0
  85. package/components/icon/circle-minus.vue +46 -0
  86. package/components/icon/circle-plus.vue +46 -0
  87. package/components/icon/circle-x.vue +46 -0
  88. package/components/icon/circle.vue +42 -0
  89. package/components/icon/clock.vue +46 -0
  90. package/components/icon/community-solar.vue +37 -0
  91. package/components/icon/companies.vue +43 -0
  92. package/components/icon/contact-us.vue +37 -0
  93. package/components/icon/copy.vue +45 -0
  94. package/components/icon/credit-card.vue +37 -0
  95. package/components/icon/document.vue +45 -0
  96. package/components/icon/electric-vehicle.vue +37 -0
  97. package/components/icon/electricity-circle.vue +39 -0
  98. package/components/icon/envelope.vue +45 -0
  99. package/components/icon/es-leaf.vue +39 -0
  100. package/components/icon/eye.vue +41 -0
  101. package/components/icon/facebook.vue +41 -0
  102. package/components/icon/file-doc.vue +72 -0
  103. package/components/icon/file-docx.vue +72 -0
  104. package/components/icon/file-pdf.vue +72 -0
  105. package/components/icon/flag.vue +37 -0
  106. package/components/icon/folder.vue +41 -0
  107. package/components/icon/gear.vue +37 -0
  108. package/components/icon/gift.vue +37 -0
  109. package/components/icon/hamburger.vue +38 -0
  110. package/components/icon/heart.vue +39 -0
  111. package/components/icon/heat-pump.vue +41 -0
  112. package/components/icon/image.vue +45 -0
  113. package/components/icon/info-blog-post.vue +47 -0
  114. package/components/icon/info.vue +45 -0
  115. package/components/icon/instagram.vue +49 -0
  116. package/components/icon/light-bulb.vue +36 -0
  117. package/components/icon/link-out.vue +38 -0
  118. package/components/icon/linkedin.vue +41 -0
  119. package/components/icon/location.vue +45 -0
  120. package/components/icon/lock-off.vue +45 -0
  121. package/components/icon/lock-on.vue +48 -0
  122. package/components/icon/marketplace.vue +43 -0
  123. package/components/icon/minus.vue +38 -0
  124. package/components/icon/money-bag.vue +37 -0
  125. package/components/icon/money.vue +35 -0
  126. package/components/icon/open-envelope.vue +45 -0
  127. package/components/icon/paper.vue +45 -0
  128. package/components/icon/payback.vue +37 -0
  129. package/components/icon/pencil.vue +45 -0
  130. package/components/icon/person-add.vue +41 -0
  131. package/components/icon/person-check.vue +41 -0
  132. package/components/icon/person-cross.vue +41 -0
  133. package/components/icon/person.vue +41 -0
  134. package/components/icon/phone.vue +41 -0
  135. package/components/icon/play.vue +41 -0
  136. package/components/icon/plus.vue +38 -0
  137. package/components/icon/roof.vue +37 -0
  138. package/components/icon/rooftop-solar.vue +37 -0
  139. package/components/icon/saving.vue +37 -0
  140. package/components/icon/search.vue +43 -0
  141. package/components/icon/settings-horizontal.vue +57 -0
  142. package/components/icon/settings-vertical.vue +57 -0
  143. package/components/icon/shield-with-check.vue +38 -0
  144. package/components/icon/shield.vue +41 -0
  145. package/components/icon/shopping-bag.vue +39 -0
  146. package/components/icon/solar.vue +37 -0
  147. package/components/icon/sound-down.vue +45 -0
  148. package/components/icon/sound-off.vue +41 -0
  149. package/components/icon/sound-on.vue +41 -0
  150. package/components/icon/sound-up.vue +41 -0
  151. package/components/icon/star-empty.vue +38 -0
  152. package/components/icon/star-full.vue +36 -0
  153. package/components/icon/star-half.vue +55 -0
  154. package/components/icon/state-ak.vue +53 -0
  155. package/components/icon/state-al.vue +38 -0
  156. package/components/icon/state-ar.vue +38 -0
  157. package/components/icon/state-az.vue +38 -0
  158. package/components/icon/state-ca.vue +38 -0
  159. package/components/icon/state-co.vue +38 -0
  160. package/components/icon/state-ct.vue +38 -0
  161. package/components/icon/state-dc.vue +52 -0
  162. package/components/icon/state-de.vue +38 -0
  163. package/components/icon/state-fl.vue +38 -0
  164. package/components/icon/state-ga.vue +38 -0
  165. package/components/icon/state-hi.vue +59 -0
  166. package/components/icon/state-ia.vue +38 -0
  167. package/components/icon/state-id.vue +38 -0
  168. package/components/icon/state-il.vue +38 -0
  169. package/components/icon/state-in.vue +38 -0
  170. package/components/icon/state-ks.vue +38 -0
  171. package/components/icon/state-ky.vue +38 -0
  172. package/components/icon/state-la.vue +38 -0
  173. package/components/icon/state-ma.vue +38 -0
  174. package/components/icon/state-md.vue +39 -0
  175. package/components/icon/state-me.vue +38 -0
  176. package/components/icon/state-mi.vue +38 -0
  177. package/components/icon/state-mn.vue +38 -0
  178. package/components/icon/state-mo.vue +38 -0
  179. package/components/icon/state-ms.vue +38 -0
  180. package/components/icon/state-mt.vue +38 -0
  181. package/components/icon/state-nc.vue +38 -0
  182. package/components/icon/state-nd.vue +38 -0
  183. package/components/icon/state-ne.vue +38 -0
  184. package/components/icon/state-nh.vue +38 -0
  185. package/components/icon/state-nj.vue +37 -0
  186. package/components/icon/state-nm.vue +38 -0
  187. package/components/icon/state-nv.vue +38 -0
  188. package/components/icon/state-ny.vue +41 -0
  189. package/components/icon/state-oh.vue +38 -0
  190. package/components/icon/state-ok.vue +38 -0
  191. package/components/icon/state-or.vue +38 -0
  192. package/components/icon/state-pa.vue +38 -0
  193. package/components/icon/state-ri.vue +38 -0
  194. package/components/icon/state-sc.vue +38 -0
  195. package/components/icon/state-sd.vue +38 -0
  196. package/components/icon/state-tn.vue +38 -0
  197. package/components/icon/state-tx.vue +38 -0
  198. package/components/icon/state-ut.vue +38 -0
  199. package/components/icon/state-va.vue +38 -0
  200. package/components/icon/state-vt.vue +38 -0
  201. package/components/icon/state-wa.vue +38 -0
  202. package/components/icon/state-wi.vue +38 -0
  203. package/components/icon/state-wv.vue +38 -0
  204. package/components/icon/state-wy.vue +38 -0
  205. package/components/icon/thumbs-up.vue +39 -0
  206. package/components/icon/trash-can.vue +41 -0
  207. package/components/icon/tree.vue +37 -0
  208. package/components/icon/twitter.vue +43 -0
  209. package/components/icon/upload.vue +41 -0
  210. package/components/icon/verified.vue +40 -0
  211. package/components/icon/video-play.vue +41 -0
  212. package/components/icon/wifi.vue +38 -0
  213. package/components/icon/x.vue +38 -0
  214. package/components/icon/youtube.vue +45 -0
  215. package/composables/forms.ts +124 -0
  216. package/lib-utils/mime-type-finder.js +48 -0
  217. package/nuxt.config.ts +40 -9
  218. package/package.json +29 -5
  219. package/tsconfig.json +2 -2
  220. package/utils/get-es-footer-content.js +90 -0
  221. package/utils/get-es-nav-bar-account-content.js +55 -0
  222. package/utils/get-es-nav-bar-global-content.js +474 -0
  223. /package/components/{b-container.vue → es-container.vue} +0 -0
  224. /package/components/{b-row.vue → es-row.vue} +0 -0
@@ -0,0 +1,331 @@
1
+ <script setup lang="ts">
2
+ import findMimeType, { mimeTypes } from '../lib-utils/mime-type-finder';
3
+
4
+ type UploadInfo = {
5
+ name: string;
6
+ uploadUrl: string;
7
+ additionalFields?: object;
8
+ };
9
+
10
+ const props = defineProps({
11
+ /**
12
+ * An array of Objects with the following shape:
13
+ * {
14
+ * name: String,
15
+ * uploadUrl: String,
16
+ * additionalFields: Object,
17
+ * }
18
+ */
19
+ uploadUrls: {
20
+ type: Array<UploadInfo>,
21
+ default: () => [],
22
+ },
23
+ /**
24
+ * The file types that are allowed to be uploaded. This is a list of MIME types as strings. If the array is
25
+ * empty, all file types are allowed. A list of acceptable MIME types can be found here in the template column:
26
+ * https://www.iana.org/assignments/media-types/media-types.xhtml
27
+ */
28
+ fileTypes: {
29
+ type: Array,
30
+ default: () => [],
31
+ required: true,
32
+ validator(fileTypes) {
33
+ return fileTypes.every((fileType) => mimeTypes.includes(fileType));
34
+ },
35
+ },
36
+ /**
37
+ * The maximum file size in MB. This is per file, not the total size of all files.
38
+ */
39
+ maxFileSizeMb: {
40
+ type: Number,
41
+ default: 25,
42
+ required: false,
43
+ },
44
+ /**
45
+ * When true, the component will be shorter vertically. This is achieved by giving the parent div
46
+ * flex-direction: row instead of the default flex-direction: column.
47
+ */
48
+ collapsed: {
49
+ type: Boolean,
50
+ default: false,
51
+ required: false,
52
+ },
53
+ });
54
+
55
+ let currentFiles: Array<File> = [];
56
+ let active = false;
57
+ const fileInput = ref(null);
58
+ const emit = defineEmits([
59
+ 'fileSizeError',
60
+ 'fileTypeError',
61
+ 'fileIsAFolderError',
62
+ 'fileDataRead',
63
+ 'duplicateFileMessage',
64
+ 'readyToUpload',
65
+ 'uploadFailure',
66
+ 'uploadSuccess',
67
+ ]);
68
+
69
+ const filterLargeFiles = (files: Array<File>) => {
70
+ // Takes an array of files, and filters out any files that exceed the maxFileSizeMb prop. Emits a
71
+ // fileSizeError event for each file that exceeds the maxFileSizeMb prop. The File API uses bytes, so
72
+ // we have to convert the maxFileSizeMb prop to bytes.
73
+ const maxFileSizeBytes = props.maxFileSizeMb * 1000000;
74
+ return [...files].filter((file) => {
75
+ if (file.size > maxFileSizeBytes) {
76
+ emit('fileSizeError', file.name);
77
+ return false;
78
+ }
79
+ return true;
80
+ });
81
+ };
82
+
83
+ const removeSpaceFromFileNames = (files: Array<File>) => {
84
+ const newFiles = files.map(
85
+ (file) =>
86
+ new File(
87
+ // Return new File object with the modified name (without any space)
88
+ [file],
89
+ file.name.replace(/\s/g, ''),
90
+ { type: file.type, lastModified: file.lastModified },
91
+ ),
92
+ );
93
+ return newFiles;
94
+ };
95
+
96
+ async function verifyMimeType(file: File) {
97
+ // If an empty folder then trigger fileIsAFolderError
98
+ if (file.type === '') {
99
+ emit('fileIsAFolderError');
100
+ }
101
+ return new Promise((resolve, reject) => {
102
+ const fileReader = new FileReader();
103
+ fileReader.onload = (evt) => {
104
+ const uint = new Uint8Array(evt.target.result);
105
+ const bytes: string[] = [];
106
+ uint.forEach((byte) => {
107
+ bytes.push(byte.toString(16));
108
+ });
109
+ const hex = bytes.join('').toUpperCase();
110
+ const mimeType = findMimeType(hex);
111
+ if (!mimeType || (props.fileTypes.length > 0 && !props.fileTypes.includes(mimeType))) {
112
+ emit('fileTypeError', file.name);
113
+ return reject();
114
+ }
115
+ return resolve(file);
116
+ };
117
+ fileReader.onerror = (error) => {
118
+ reject(error);
119
+ };
120
+ const blob = file.slice(0, 4);
121
+ fileReader.readAsArrayBuffer(blob);
122
+ });
123
+ }
124
+
125
+ async function uploadSingleFile(file: File) {
126
+ const uploadInfo = props.uploadUrls.find((uploadUrl) => uploadUrl.name === file.name);
127
+ if (!uploadInfo) {
128
+ emit('uploadFailure', {
129
+ name: file.name,
130
+ message: 'No upload URL found for this file.',
131
+ });
132
+ return;
133
+ }
134
+ const form = new FormData();
135
+ if (uploadInfo.additionalFields) {
136
+ Object.entries(uploadInfo.additionalFields).forEach(([key, value]) => {
137
+ form.append(key, value);
138
+ });
139
+ }
140
+ form.append('file', file);
141
+ fetch(uploadInfo.uploadUrl, {
142
+ method: 'POST',
143
+ body: form,
144
+ })
145
+ .then((response) => {
146
+ if (response.status >= 200 && response.status < 300) {
147
+ emit('uploadSuccess', file.name);
148
+ } else {
149
+ emit('uploadFailure', {
150
+ name: file.name,
151
+ message: `Received ${response.status} code from server.`,
152
+ });
153
+ }
154
+ })
155
+ .catch((error) => {
156
+ if (error.response) {
157
+ // The request was made and the server responded with a status code
158
+ // that falls out of the range of 2xx
159
+ emit('uploadFailure', {
160
+ name: file.name,
161
+ message: `Received ${error.response.status} code from server.`,
162
+ });
163
+ } else if (error.request) {
164
+ // The request was made but no response was received
165
+ // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
166
+ // http.ClientRequest in node.js
167
+ emit('uploadFailure', { name: file.name, message: 'The server did not respond.' });
168
+ } else {
169
+ // Something happened in setting up the request that triggered an Error
170
+ emit('uploadFailure', {
171
+ name: file.name,
172
+ message: 'There was an error sending your file to the server.',
173
+ });
174
+ }
175
+ });
176
+ }
177
+
178
+ const readFilesIntoUrl = (files: Array<File>) => {
179
+ files.forEach((file) => {
180
+ const fileReader = new FileReader();
181
+ fileReader.onload = () => {
182
+ emit('fileDataRead', {
183
+ name: file.name,
184
+ type: file.type,
185
+ size: file.size,
186
+ // For non-image files (pdf, docx, etc.)
187
+ data: file.type.includes('application')
188
+ ? URL.createObjectURL(new Blob([file], { type: file.type }))
189
+ : fileReader.result,
190
+ });
191
+ };
192
+ fileReader.readAsDataURL(file);
193
+ });
194
+ };
195
+
196
+ async function verifyFiles(files: Array<File>) {
197
+ const correctlySizedFiles = filterLargeFiles(files);
198
+ const correctlyNamedFiles = removeSpaceFromFileNames(correctlySizedFiles);
199
+
200
+ // Make sure the file is the correct mime type
201
+ let newValidFiles = await Promise.allSettled(correctlyNamedFiles.map(async (file) => verifyMimeType(file)));
202
+ // Filter out undefined values and "rejected"
203
+ newValidFiles = newValidFiles.filter((file) => file && file.status === 'fulfilled').map((file) => file.value);
204
+
205
+ // If the new file with name already exists in the current files
206
+ // Don't upload the new file and display an error
207
+ const duplicateFiles = newValidFiles.filter(({ name }) => currentFiles.some((file) => file.name === name));
208
+ duplicateFiles.forEach((file) => emit('duplicateFileMessage', file.name));
209
+
210
+ // For new files with unused names, add them to files
211
+ newValidFiles = newValidFiles.filter(({ name }) => !currentFiles.some((file) => file.name === name));
212
+ currentFiles = [...currentFiles, ...newValidFiles];
213
+
214
+ if (newValidFiles.length > 0) {
215
+ emit('readyToUpload', newValidFiles);
216
+ readFilesIntoUrl(newValidFiles);
217
+ }
218
+ }
219
+
220
+ watch(
221
+ () => props.uploadUrls,
222
+ (newUrls, oldUrls) => {
223
+ // For every file that has an associated upload URL, we start the upload
224
+ newUrls.forEach((newUrlPair) => {
225
+ const oldUrl = oldUrls.find(({ name }) => name === newUrlPair.name);
226
+ const fileToUpload = currentFiles.find((file) => file.name === newUrlPair.name);
227
+ if ((!oldUrl || oldUrl.uploadUrl !== newUrlPair.uploadUrl) && fileToUpload) {
228
+ uploadSingleFile(fileToUpload);
229
+ }
230
+ });
231
+ },
232
+ );
233
+
234
+ async function onFileChanged($event: Event) {
235
+ const target = $event.target as HTMLInputElement;
236
+ // The user has selected files from the file picker. We have to filter out any files that exceed the
237
+ // maxFileSizeMb prop.
238
+ if (target && target.files) {
239
+ await verifyFiles([...target.files]);
240
+ }
241
+ }
242
+
243
+ const onDrop = (event) => {
244
+ // The user has dropped files onto the component. We have to apply the same logic as if they had
245
+ // selected the files from the file picker which limits the file types to the ones specified in the
246
+ // fileTypes prop.
247
+ active = false;
248
+
249
+ // Use DataTransferItemList interface to access the file(s)
250
+ const dataTransfersAsFiles = [...event.dataTransfer.items]
251
+ .filter((item) => {
252
+ if (item.kind !== 'file') {
253
+ emit('fileTypeError', item.name);
254
+ return false;
255
+ }
256
+ return true;
257
+ })
258
+ .map((item) => item.getAsFile());
259
+
260
+ verifyFiles(dataTransfersAsFiles);
261
+ };
262
+
263
+ const openFilePicker = () => {
264
+ if (fileInput.value) {
265
+ fileInput.value.click();
266
+ }
267
+ };
268
+ </script>
269
+
270
+ <template>
271
+ <div
272
+ role="presentation"
273
+ class="es-file-upload align-items-center rounded-sm d-flex flex-column justify-content-center p-150"
274
+ :class="{ active: active }"
275
+ @dragenter.stop.prevent="active = true"
276
+ @dragover.stop.prevent="active = true"
277
+ @dragleave.stop.prevent="active = false"
278
+ @dragend.stop.prevent="onDrop"
279
+ @drop.stop.prevent="onDrop"
280
+ @click.self="openFilePicker"
281
+ @keydown.enter.prevent="openFilePicker"
282
+ @keydown.space.prevent="openFilePicker">
283
+ <div
284
+ class="d-flex justify-content-center flex-column"
285
+ :class="{ 'flex-md-row': collapsed }">
286
+ <div class="align-self-center">
287
+ <icon-upload
288
+ class="d-none d-md-inline-block"
289
+ :class="{ 'mb-200': collapsed }"
290
+ height="32px"
291
+ width="32px" />
292
+ <icon-upload
293
+ class="d-md-none"
294
+ height="24px"
295
+ width="24px" />
296
+ </div>
297
+ <div
298
+ class="align-self-center text-center"
299
+ :class="{ 'px-100': collapsed }">
300
+ <slot name="cta" />
301
+ </div>
302
+ <div class="align-self-center">
303
+ <es-button
304
+ class="d-none d-md-inline w-100 w-md-auto"
305
+ :class="{ 'mb-200': collapsed }"
306
+ @click="openFilePicker">
307
+ <slot name="buttonText">
308
+ <p class="m-0">Browse files</p>
309
+ </slot>
310
+ </es-button>
311
+ <es-button
312
+ class="d-md-none w-100"
313
+ @click="openFilePicker">
314
+ <slot name="buttonText">
315
+ <p class="m-0">Choose files</p>
316
+ </slot>
317
+ </es-button>
318
+ </div>
319
+ </div>
320
+ <div>
321
+ <slot name="helpText" />
322
+ </div>
323
+ <input
324
+ ref="fileInput"
325
+ type="file"
326
+ :accept="fileTypes.join(', ')"
327
+ style="display: none"
328
+ multiple
329
+ @change="onFileChanged($event)" />
330
+ </div>
331
+ </template>
@@ -0,0 +1,61 @@
1
+ <script setup lang="ts">
2
+ defineProps({
3
+ visible: {
4
+ type: Boolean,
5
+ default: false,
6
+ required: false,
7
+ },
8
+ fileName: {
9
+ type: String,
10
+ default: 'test.png',
11
+ required: false,
12
+ },
13
+ fileSource: {
14
+ type: String,
15
+ default: '',
16
+ required: false,
17
+ },
18
+ });
19
+
20
+ defineEmits(['hidden']);
21
+ </script>
22
+
23
+ <template>
24
+ <es-modal
25
+ :id="`preview-modal-${fileName}`"
26
+ hide-footer
27
+ :visible="visible"
28
+ size="xl"
29
+ body-class="border-top p-0"
30
+ @hidden="$emit('hidden')">
31
+ <template #modal-title>
32
+ {{ fileName }}
33
+ </template>
34
+ <div class="d-md-none d-block file-preview text-center">
35
+ <nuxt-img
36
+ loading="lazy"
37
+ class="img-fluid file-preview"
38
+ :src="fileSource"
39
+ :alt="fileName" />
40
+ </div>
41
+ <div class="d-none d-md-block">
42
+ <div class="px-500 py-200 bg-gray-200">
43
+ <div class="card bg-white file-preview">
44
+ <nuxt-img
45
+ loading="lazy"
46
+ class="img-fluid file-preview rounded-img"
47
+ :src="fileSource"
48
+ :alt="fileName" />
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </es-modal>
53
+ </template>
54
+
55
+ <style lang="scss" scoped>
56
+ @use '@energysage/es-ds-styles/scss/variables' as variables;
57
+
58
+ .rounded-img {
59
+ border-radius: variables.$card-inner-border-radius !important;
60
+ }
61
+ </style>
@@ -0,0 +1,163 @@
1
+ <script lang="js">
2
+ import IconFacebook from './icon/facebook.vue';
3
+ import IconInstagram from './icon/instagram.vue';
4
+ import IconLinkedin from './icon/linkedin.vue';
5
+ import IconTwitter from './icon/twitter.vue';
6
+ import IconYoutube from './icon/youtube.vue';
7
+
8
+ export default {
9
+ name: 'EsFooter',
10
+ components: {
11
+ IconFacebook,
12
+ IconInstagram,
13
+ IconLinkedin,
14
+ IconTwitter,
15
+ IconYoutube,
16
+ },
17
+ props: {
18
+ content: {
19
+ type: Object,
20
+ required: true,
21
+ },
22
+ },
23
+ computed: {
24
+ copyrightText() {
25
+ return this.content.copyrightText.replace('{currentYear}', new Date().getFullYear());
26
+ },
27
+ },
28
+ };
29
+ </script>
30
+
31
+ <template>
32
+ <div class="es-footer clearfix">
33
+ <!-- Make an impact -->
34
+ <div class="bg-blue-900 text-white py-200 py-lg-300 px-200">
35
+ <div class="container px-0">
36
+ <div class="row">
37
+ <div
38
+ class="col col-12 col-md-5 col-lg-4 font-size-200 font-size-md-400 font-weight-bold mb-150 mb-md-0">
39
+ {{ content.banner.headline }}
40
+ <br />
41
+ {{ content.banner.subHeadline }}
42
+ </div>
43
+ <div class="col col-12 col-md-7 col-lg-8">
44
+ {{ content.banner.body }}
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ <!-- Make an impact -->
50
+ <div class="bg-blue-800 py-200 pt-lg-200 pb-lg-150 px-200">
51
+ <div class="container px-0">
52
+ <div class="row">
53
+ <!-- Link columns -->
54
+ <div class="col col-12 col-lg-8 order-lg-1 text-white">
55
+ <div class="row">
56
+ <div
57
+ v-for="column in content.columns"
58
+ :key="column.title"
59
+ class="col col-6 col-md-3 mb-100 mb-lg-0">
60
+ <ul class="list-unstyled mb-lg-0">
61
+ <li class="font-weight-bold font-size-75 font-size-md-100 mb-100 mb-lg-50">
62
+ {{ column.title }}
63
+ </li>
64
+ <li
65
+ v-for="link in column.links"
66
+ :key="link.text"
67
+ class="mt-0 mt-lg-25 mb-100 mb-lg-50 font-size-75">
68
+ <es-nav-bar-link
69
+ :href="link.url"
70
+ :target="link.newTab ? '_blank' : null"
71
+ class="text-reset">
72
+ {{ link.text }}
73
+ </es-nav-bar-link>
74
+ </li>
75
+ </ul>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ <!-- Link columns -->
80
+ <!-- Logo and Social-->
81
+ <div class="col col-12 col-lg-4 order-lg-0">
82
+ <es-nav-bar-link
83
+ class="d-block mb-150 d-flex justify-content-center justify-content-lg-start"
84
+ :href="content.home.link">
85
+ <span class="sr-only">
86
+ {{ content.home.name }}
87
+ </span>
88
+ <div class="es-footer-logo">
89
+ <slot name="logo" />
90
+ </div>
91
+ </es-nav-bar-link>
92
+ <!-- Social icons -->
93
+ <div class="d-flex text-white justify-content-center justify-content-lg-start">
94
+ <es-nav-bar-link
95
+ v-for="(iconLink, index) in content.socialLinks"
96
+ :key="iconLink.text"
97
+ class="text-reset"
98
+ :class="{ 'mr-150': index < content.socialLinks.length - 1 }"
99
+ :href="iconLink.url"
100
+ target="_blank">
101
+ <span class="sr-only">{{ iconLink.text }}</span>
102
+ <component :is="iconLink.icon" />
103
+ </es-nav-bar-link>
104
+ </div>
105
+ <!-- Social icons -->
106
+ </div>
107
+ <!-- Logo and Social -->
108
+ </div>
109
+ </div>
110
+ </div>
111
+ <div class="bg-blue-700 text-blue-50 px-200 py-100">
112
+ <div class="container px-0">
113
+ <!-- Trademark Info -->
114
+ <p class="font-size-75">
115
+ {{ content.trademarkText }}
116
+ </p>
117
+ <p class="mb-200 font-size-75">
118
+ {{ copyrightText }}
119
+ </p>
120
+ <!-- Trademark Info -->
121
+ <!-- Legal -->
122
+ <hr class="border-top border-blue-500 m-0" />
123
+ <div class="row justify-content-center pt-100">
124
+ <div
125
+ v-for="link in content.legalLinks"
126
+ :key="link.text"
127
+ class="col col-6 col-lg-auto font-size-75 font-weight-semibold font-size-md-50 font-weight-md-normal mt-25 mb-50 px-lg-300">
128
+ <es-nav-bar-link
129
+ :href="link.url"
130
+ class="text-reset font-weight-normal">
131
+ {{ link.text }}
132
+ </es-nav-bar-link>
133
+ </div>
134
+ </div>
135
+ <!-- Legal -->
136
+ </div>
137
+ </div>
138
+ <div class="bg-blue-50 px-200 py-100">
139
+ <div class="container px-0">
140
+ <!-- DOE -->
141
+ <div class="d-lg-flex align-items-end">
142
+ <!-- eslint-disable-next-line -->
143
+ <img
144
+ :src="content.departmentOfEnergy.logoUrl"
145
+ alt="department of energy logo"
146
+ aria-hidden="true"
147
+ class="mr-100 mb-100 mb-lg-0"
148
+ width="99"
149
+ height="24" />
150
+ <p class="mb-0 font-size-75 font-weight-normal font-weight-lg-semibold">
151
+ <es-nav-bar-link
152
+ :href="content.departmentOfEnergy.learnMore.link"
153
+ target="_blank"
154
+ class="text-blue-900">
155
+ {{ content.departmentOfEnergy.learnMore.text }}
156
+ </es-nav-bar-link>
157
+ </p>
158
+ </div>
159
+ <!-- DOE -->
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </template>
@@ -0,0 +1,40 @@
1
+ <script setup lang="ts">
2
+ const model = defineModel<boolean>({
3
+ default: false,
4
+ });
5
+ defineOptions({
6
+ inheritAttrs: false,
7
+ });
8
+ defineProps({
9
+ id: {
10
+ type: String,
11
+ required: true,
12
+ },
13
+ disabled: {
14
+ type: Boolean,
15
+ default: false,
16
+ },
17
+ indeterminate: {
18
+ type: Boolean,
19
+ default: false,
20
+ },
21
+ });
22
+ </script>
23
+
24
+ <template>
25
+ <div class="custom-control custom-checkbox">
26
+ <input
27
+ v-bind="$attrs"
28
+ :id="id"
29
+ v-model="model"
30
+ type="checkbox"
31
+ class="custom-control-input"
32
+ :indeterminate="indeterminate"
33
+ :disabled="disabled" />
34
+ <label
35
+ :for="id"
36
+ class="custom-control-label">
37
+ <slot />
38
+ </label>
39
+ </div>
40
+ </template>