@energysage/es-ds-components 0.0.1 → 3.0.0-alpha.10

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 (234) hide show
  1. package/.eslintignore +9 -0
  2. package/.eslintrc.json +3 -0
  3. package/.prettierrc.json +10 -0
  4. package/README.md +0 -3
  5. package/components/es-accordion-list.vue +117 -0
  6. package/components/es-accordion.vue +7 -0
  7. package/components/es-badge.vue +20 -0
  8. package/components/es-breadcrumbs.vue +73 -0
  9. package/components/es-button.vue +122 -0
  10. package/components/es-card.vue +64 -0
  11. package/components/es-carousel.vue +316 -0
  12. package/components/es-col.vue +27 -0
  13. package/components/es-collapse.vue +83 -0
  14. package/components/es-container.vue +14 -0
  15. package/components/es-cta-banner.vue +95 -0
  16. package/components/es-cta-card.vue +60 -0
  17. package/components/es-data-table-simple.vue +73 -0
  18. package/components/es-data-table.vue +128 -0
  19. package/components/es-error-page.vue +120 -0
  20. package/components/es-file-input.vue +334 -0
  21. package/components/es-file-preview-modal.vue +61 -0
  22. package/components/es-footer.vue +163 -0
  23. package/components/es-form-checkbox.vue +40 -0
  24. package/components/es-form-input.vue +130 -0
  25. package/components/es-form-msg.vue +81 -0
  26. package/components/es-form-radio-card.vue +57 -0
  27. package/components/es-form-radio-cards.vue +78 -0
  28. package/components/es-form-textarea.vue +124 -0
  29. package/components/es-form.vue +16 -0
  30. package/components/es-modal.vue +103 -0
  31. package/components/es-nav-bar-account-menu.vue +70 -0
  32. package/components/es-nav-bar-featured-article.vue +60 -0
  33. package/components/es-nav-bar-link.vue +27 -0
  34. package/components/es-nav-bar-product-menu.vue +219 -0
  35. package/components/es-nav-bar-top-level-menu.vue +194 -0
  36. package/components/es-nav-bar-topic-menu.vue +197 -0
  37. package/components/es-nav-bar.vue +411 -0
  38. package/components/es-pagination.vue +67 -0
  39. package/components/es-popover.vue +113 -0
  40. package/components/es-progress-circle.vue +81 -0
  41. package/components/es-progress.vue +36 -0
  42. package/components/es-radio-button-group.vue +53 -0
  43. package/components/es-radio-button.vue +57 -0
  44. package/components/es-rating.vue +120 -0
  45. package/components/es-reviews-io-card-carousel.vue +279 -0
  46. package/components/es-row.vue +5 -0
  47. package/components/es-skeleton-img.vue +38 -0
  48. package/components/es-skeleton-wrapper.vue +17 -0
  49. package/components/es-skeleton.vue +97 -0
  50. package/components/es-slider.vue +218 -0
  51. package/components/es-spinner.vue +22 -0
  52. package/components/es-support-card.vue +150 -0
  53. package/components/es-support.vue +66 -0
  54. package/components/es-tab.vue +7 -0
  55. package/components/es-tabs.vue +70 -0
  56. package/components/es-verification-code.vue +94 -0
  57. package/components/es-video.vue +60 -0
  58. package/components/es-view-more.vue +64 -0
  59. package/components/es-zip-code-form.vue +204 -0
  60. package/components/icon/arrow-clockwise.vue +51 -0
  61. package/components/icon/arrow-counter-clockwise.vue +37 -0
  62. package/components/icon/arrow-cycle.vue +43 -0
  63. package/components/icon/arrow-left.vue +39 -0
  64. package/components/icon/arrow-right.vue +39 -0
  65. package/components/icon/arrow-up.vue +39 -0
  66. package/components/icon/arrow.vue +68 -0
  67. package/components/icon/bank.vue +38 -0
  68. package/components/icon/battery.vue +40 -0
  69. package/components/icon/bell.vue +42 -0
  70. package/components/icon/block.vue +46 -0
  71. package/components/icon/bolt.vue +38 -0
  72. package/components/icon/business.vue +42 -0
  73. package/components/icon/calculator.vue +37 -0
  74. package/components/icon/calendar.vue +46 -0
  75. package/components/icon/cart.vue +46 -0
  76. package/components/icon/chat-add.vue +44 -0
  77. package/components/icon/chat-approve.vue +42 -0
  78. package/components/icon/chat-bubble.vue +40 -0
  79. package/components/icon/chat-dots.vue +42 -0
  80. package/components/icon/check.vue +39 -0
  81. package/components/icon/chevron-down.vue +39 -0
  82. package/components/icon/chevron-horizontal.vue +39 -0
  83. package/components/icon/chevron-left.vue +39 -0
  84. package/components/icon/chevron-right.vue +39 -0
  85. package/components/icon/chevron-up.vue +39 -0
  86. package/components/icon/circle-alert.vue +46 -0
  87. package/components/icon/circle-check.vue +46 -0
  88. package/components/icon/circle-chevron-down.vue +47 -0
  89. package/components/icon/circle-chevron-left.vue +46 -0
  90. package/components/icon/circle-chevron-right.vue +46 -0
  91. package/components/icon/circle-chevron-up.vue +46 -0
  92. package/components/icon/circle-minus.vue +46 -0
  93. package/components/icon/circle-plus.vue +46 -0
  94. package/components/icon/circle-x.vue +46 -0
  95. package/components/icon/circle.vue +42 -0
  96. package/components/icon/clock.vue +46 -0
  97. package/components/icon/community-solar.vue +37 -0
  98. package/components/icon/companies.vue +43 -0
  99. package/components/icon/contact-us.vue +37 -0
  100. package/components/icon/copy.vue +45 -0
  101. package/components/icon/credit-card.vue +37 -0
  102. package/components/icon/document.vue +45 -0
  103. package/components/icon/electric-vehicle.vue +37 -0
  104. package/components/icon/electricity-circle.vue +39 -0
  105. package/components/icon/envelope.vue +45 -0
  106. package/components/icon/es-leaf.vue +39 -0
  107. package/components/icon/eye.vue +41 -0
  108. package/components/icon/facebook.vue +41 -0
  109. package/components/icon/file-doc.vue +72 -0
  110. package/components/icon/file-docx.vue +72 -0
  111. package/components/icon/file-pdf.vue +72 -0
  112. package/components/icon/flag.vue +37 -0
  113. package/components/icon/folder.vue +41 -0
  114. package/components/icon/gear.vue +37 -0
  115. package/components/icon/gift.vue +37 -0
  116. package/components/icon/hamburger.vue +38 -0
  117. package/components/icon/heart.vue +39 -0
  118. package/components/icon/heat-pump.vue +41 -0
  119. package/components/icon/image.vue +45 -0
  120. package/components/icon/info-blog-post.vue +47 -0
  121. package/components/icon/info.vue +45 -0
  122. package/components/icon/instagram.vue +49 -0
  123. package/components/icon/light-bulb.vue +36 -0
  124. package/components/icon/link-out.vue +38 -0
  125. package/components/icon/linkedin.vue +41 -0
  126. package/components/icon/location.vue +45 -0
  127. package/components/icon/lock-off.vue +45 -0
  128. package/components/icon/lock-on.vue +48 -0
  129. package/components/icon/marketplace.vue +43 -0
  130. package/components/icon/minus.vue +38 -0
  131. package/components/icon/money-bag.vue +37 -0
  132. package/components/icon/money.vue +35 -0
  133. package/components/icon/open-envelope.vue +45 -0
  134. package/components/icon/paper.vue +45 -0
  135. package/components/icon/payback.vue +37 -0
  136. package/components/icon/pencil.vue +45 -0
  137. package/components/icon/person-add.vue +41 -0
  138. package/components/icon/person-check.vue +41 -0
  139. package/components/icon/person-cross.vue +41 -0
  140. package/components/icon/person.vue +41 -0
  141. package/components/icon/phone.vue +41 -0
  142. package/components/icon/play.vue +41 -0
  143. package/components/icon/plus.vue +38 -0
  144. package/components/icon/roof.vue +37 -0
  145. package/components/icon/rooftop-solar.vue +37 -0
  146. package/components/icon/saving.vue +37 -0
  147. package/components/icon/search.vue +43 -0
  148. package/components/icon/settings-horizontal.vue +57 -0
  149. package/components/icon/settings-vertical.vue +57 -0
  150. package/components/icon/shield-with-check.vue +38 -0
  151. package/components/icon/shield.vue +41 -0
  152. package/components/icon/shopping-bag.vue +39 -0
  153. package/components/icon/solar.vue +37 -0
  154. package/components/icon/sound-down.vue +45 -0
  155. package/components/icon/sound-off.vue +41 -0
  156. package/components/icon/sound-on.vue +41 -0
  157. package/components/icon/sound-up.vue +41 -0
  158. package/components/icon/star-empty.vue +38 -0
  159. package/components/icon/star-full.vue +36 -0
  160. package/components/icon/star-half.vue +55 -0
  161. package/components/icon/state-ak.vue +53 -0
  162. package/components/icon/state-al.vue +38 -0
  163. package/components/icon/state-ar.vue +38 -0
  164. package/components/icon/state-az.vue +38 -0
  165. package/components/icon/state-ca.vue +38 -0
  166. package/components/icon/state-co.vue +38 -0
  167. package/components/icon/state-ct.vue +38 -0
  168. package/components/icon/state-dc.vue +52 -0
  169. package/components/icon/state-de.vue +38 -0
  170. package/components/icon/state-fl.vue +38 -0
  171. package/components/icon/state-ga.vue +38 -0
  172. package/components/icon/state-hi.vue +59 -0
  173. package/components/icon/state-ia.vue +38 -0
  174. package/components/icon/state-id.vue +38 -0
  175. package/components/icon/state-il.vue +38 -0
  176. package/components/icon/state-in.vue +38 -0
  177. package/components/icon/state-ks.vue +38 -0
  178. package/components/icon/state-ky.vue +38 -0
  179. package/components/icon/state-la.vue +38 -0
  180. package/components/icon/state-ma.vue +38 -0
  181. package/components/icon/state-md.vue +39 -0
  182. package/components/icon/state-me.vue +38 -0
  183. package/components/icon/state-mi.vue +38 -0
  184. package/components/icon/state-mn.vue +38 -0
  185. package/components/icon/state-mo.vue +38 -0
  186. package/components/icon/state-ms.vue +38 -0
  187. package/components/icon/state-mt.vue +38 -0
  188. package/components/icon/state-nc.vue +38 -0
  189. package/components/icon/state-nd.vue +38 -0
  190. package/components/icon/state-ne.vue +38 -0
  191. package/components/icon/state-nh.vue +38 -0
  192. package/components/icon/state-nj.vue +37 -0
  193. package/components/icon/state-nm.vue +38 -0
  194. package/components/icon/state-nv.vue +38 -0
  195. package/components/icon/state-ny.vue +41 -0
  196. package/components/icon/state-oh.vue +38 -0
  197. package/components/icon/state-ok.vue +38 -0
  198. package/components/icon/state-or.vue +38 -0
  199. package/components/icon/state-pa.vue +38 -0
  200. package/components/icon/state-ri.vue +38 -0
  201. package/components/icon/state-sc.vue +38 -0
  202. package/components/icon/state-sd.vue +38 -0
  203. package/components/icon/state-tn.vue +38 -0
  204. package/components/icon/state-tx.vue +38 -0
  205. package/components/icon/state-ut.vue +38 -0
  206. package/components/icon/state-va.vue +38 -0
  207. package/components/icon/state-vt.vue +38 -0
  208. package/components/icon/state-wa.vue +38 -0
  209. package/components/icon/state-wi.vue +38 -0
  210. package/components/icon/state-wv.vue +38 -0
  211. package/components/icon/state-wy.vue +38 -0
  212. package/components/icon/thumbs-up.vue +39 -0
  213. package/components/icon/trash-can.vue +41 -0
  214. package/components/icon/tree.vue +37 -0
  215. package/components/icon/twitter.vue +43 -0
  216. package/components/icon/upload.vue +41 -0
  217. package/components/icon/verified.vue +40 -0
  218. package/components/icon/video-play.vue +41 -0
  219. package/components/icon/wifi.vue +38 -0
  220. package/components/icon/x.vue +38 -0
  221. package/components/icon/youtube.vue +45 -0
  222. package/composables/forms.ts +124 -0
  223. package/nuxt.config.ts +83 -1
  224. package/package.json +47 -13
  225. package/tsconfig.json +4 -0
  226. package/types/es-carousel.ts +12 -0
  227. package/types/index.ts +1 -0
  228. package/utils/forms.ts +120 -0
  229. package/utils/get-es-footer-content.ts +90 -0
  230. package/utils/get-es-nav-bar-account-content.ts +55 -0
  231. package/utils/get-es-nav-bar-global-content.ts +482 -0
  232. package/utils/mime-type-finder.ts +48 -0
  233. package/utils/vuelidate-keys.ts +39 -0
  234. package/components/TestComponent.vue +0 -5
@@ -0,0 +1,128 @@
1
+ <script setup lang="ts">
2
+ import DataTable from 'primevue/datatable';
3
+ import Column from 'primevue/column';
4
+
5
+ const props = defineProps({
6
+ fields: {
7
+ type: Array,
8
+ default: null,
9
+ },
10
+ items: {
11
+ type: Array,
12
+ required: true,
13
+ },
14
+ stickyFirstCol: {
15
+ type: Boolean,
16
+ default: false,
17
+ },
18
+ stickyHeader: {
19
+ type: [Boolean, String],
20
+ default: false,
21
+ },
22
+ });
23
+
24
+ const checkFrozenRow = (index: any) => {
25
+ if (index === 0 && props.stickyFirstCol) {
26
+ return true;
27
+ }
28
+ return false;
29
+ };
30
+
31
+ // Code is taken from BootStrap Vue to implemnt <b-table> feature that automatically
32
+ // samples the first row to extract field name and "humanize" by converting kebab-case,
33
+ // snake_case, and camelCase to individual words and capitalizes each word.
34
+ // Doc: https://bootstrap-vue.org/docs/components/table
35
+ // Source Code: https://github.com/bootstrap-vue/bootstrap-vue/blob/5173dd19f6f46dc9d125cd7233fb59ccd2ef9296/src/utils/string.js#L30
36
+ const startCase = (str: string) =>
37
+ str
38
+ .replace(/_/g, ' ')
39
+ .replace(/([a-z])([A-Z])/g, (_, $1, $2) => `${$1} ${$2}`)
40
+ .replace(/(\s|^)(\w)/g, (_, $1, $2) => $1 + $2.toUpperCase());
41
+
42
+ const label = (col: any) => {
43
+ if (typeof col === 'string') {
44
+ return startCase(col);
45
+ }
46
+ return col?.label ? col.label : startCase(col?.key);
47
+ };
48
+
49
+ const columns = computed(() => {
50
+ if (props.fields) {
51
+ return props.fields;
52
+ }
53
+ // @ts-expect-error not sure
54
+ return Object.keys(props.items[0]);
55
+ });
56
+
57
+ const scrollHeight = computed(() => {
58
+ if (props.stickyHeader) {
59
+ if (typeof props.stickyHeader === 'string') {
60
+ return props.stickyHeader;
61
+ }
62
+ return '300px';
63
+ }
64
+ return '';
65
+ });
66
+ </script>
67
+ <template>
68
+ <data-table
69
+ scrollable
70
+ :pt="{
71
+ table: {
72
+ class: 'font-size-75 font-size-md-100',
73
+ style: 'border-collapse: separate; border-spacing: 0;',
74
+ },
75
+ wrapper: {
76
+ style: 'border-radius: inherit',
77
+ },
78
+ thead: {
79
+ style: 'top: 0px; z-index: 2',
80
+ },
81
+ }"
82
+ :value="props.items"
83
+ class="border table__data-table table-spacing table"
84
+ :scroll-height="scrollHeight">
85
+ <!-- @vue-expect-error -->
86
+ <column
87
+ v-for="(col, index) of columns"
88
+ :key="col?.key ? col.key : col"
89
+ :field="col?.key ? col.key : col"
90
+ :header="label(col)"
91
+ :sortable="col?.sortable"
92
+ :frozen="checkFrozenRow(index)"
93
+ align-frozen="left"
94
+ :pt="{
95
+ sortIcon: {
96
+ style: 'display: none',
97
+ },
98
+ headerCell: (options) => ({
99
+ class: [
100
+ {
101
+ sortable: options.props.sortable,
102
+ stickyFirstColHeader: options.props.frozen,
103
+ },
104
+ ],
105
+ }),
106
+ bodyCell: (options) => ({
107
+ class: [
108
+ {
109
+ stickyFirstCol: options.props.frozen,
110
+ },
111
+ ],
112
+ }),
113
+ }">
114
+ </column>
115
+ </data-table>
116
+ </template>
117
+ <style scoped lang="scss">
118
+ :deep(.stickyFirstColHeader) {
119
+ position: sticky !important;
120
+ }
121
+ :deep(.stickyFirstCol) {
122
+ position: sticky !important;
123
+ background: inherit;
124
+ }
125
+ :deep(.sortable) {
126
+ cursor: pointer !important;
127
+ }
128
+ </style>
@@ -0,0 +1,120 @@
1
+ <script setup lang="ts">
2
+ import IconContactUs from './icon/contact-us.vue';
3
+ import IconCommunitySolar from './icon/community-solar.vue';
4
+ import IconInfoBlogPost from './icon/info-blog-post.vue';
5
+ import IconMarketplace from './icon/marketplace.vue';
6
+
7
+ const ES_DOMAIN = process.env.ES_DOMAIN || 'https://www.energysage.com';
8
+ const linkItems = [
9
+ {
10
+ icon: IconContactUs,
11
+ link: 'mailto:hello@energysage.com',
12
+ label: 'Contact us',
13
+ },
14
+ {
15
+ icon: IconCommunitySolar,
16
+ link: `${ES_DOMAIN}/solar/`,
17
+ label: 'Learn about solar',
18
+ },
19
+ {
20
+ icon: IconInfoBlogPost,
21
+ link: `${ES_DOMAIN}/blog/`,
22
+ label: 'EnergySage blog',
23
+ },
24
+ {
25
+ icon: IconMarketplace,
26
+ link: `${ES_DOMAIN}/shop/home-solar/`,
27
+ label: 'EnergySage marketplace',
28
+ },
29
+ ];
30
+
31
+ const props = defineProps({
32
+ errorType: {
33
+ type: String,
34
+ required: true,
35
+ },
36
+ });
37
+
38
+ interface ErrorCode {
39
+ shortMessage?: string;
40
+ longMessage: string;
41
+ }
42
+
43
+ interface ErrorCodeMap {
44
+ [key: string]: ErrorCode;
45
+ }
46
+
47
+ const errorCodeToMessages: ErrorCodeMap = {
48
+ 403: {
49
+ shortMessage: 'Access denied',
50
+ longMessage:
51
+ "Oops! If you're seeing this message, there's a good chance you have cookies or " +
52
+ 'referrers turned off in your browser.',
53
+ },
54
+ 404: {
55
+ shortMessage: 'Page not found',
56
+ longMessage: 'Oops! We are terribly sorry, but there is nothing bright to see here.',
57
+ },
58
+ 500: {
59
+ shortMessage: 'Internal server error',
60
+ longMessage: 'Oops! We are terribly sorry, but our server is not so bright today. Please try again.',
61
+ },
62
+ 503: {
63
+ shortMessage: 'Service unavailable',
64
+ longMessage: 'The page you are requesting is temporarily unavailable.',
65
+ },
66
+ default: {
67
+ longMessage: 'Oops! Something went wrong!',
68
+ },
69
+ };
70
+
71
+ const messages: ErrorCode = errorCodeToMessages[props.errorType];
72
+
73
+ const errorShortMessage = messages?.shortMessage || props.errorType;
74
+
75
+ const errorLongMessage = messages?.longMessage || errorCodeToMessages.default.longMessage;
76
+ </script>
77
+
78
+ <template>
79
+ <div class="bg-gray-50">
80
+ <es-container class="py-500 py-lg-800">
81
+ <es-row>
82
+ <es-col
83
+ md="7"
84
+ class="my-500 mx-auto text-center">
85
+ <h1 class="post1 text-gray-600">
86
+ {{ errorShortMessage }}
87
+ </h1>
88
+ <p
89
+ id="msgError"
90
+ class="font-size-lg text-gray-600">
91
+ {{ errorLongMessage }}
92
+ </p>
93
+ </es-col>
94
+ </es-row>
95
+ <es-row>
96
+ <es-col
97
+ v-for="(item, index) in linkItems"
98
+ :key="index"
99
+ cols="6"
100
+ md="3">
101
+ <a
102
+ class="align-items-center d-flex flex-column justify-content-center text-center"
103
+ :href="item.link">
104
+ <div
105
+ class="bg-white d-flex justify-content-center mb-50 rounded-circle"
106
+ align-v="center">
107
+ <div class="m-100 text-orange">
108
+ <component
109
+ :is="item.icon"
110
+ height="53px"
111
+ width="53px" />
112
+ </div>
113
+ </div>
114
+ <p class="mb-200 mb-md-0">{{ item.label }}</p>
115
+ </a>
116
+ </es-col>
117
+ </es-row>
118
+ </es-container>
119
+ </div>
120
+ </template>
@@ -0,0 +1,334 @@
1
+ <script setup lang="ts">
2
+ type UploadInfo = {
3
+ name: string;
4
+ uploadUrl: string;
5
+ additionalFields?: object;
6
+ };
7
+
8
+ const props = defineProps({
9
+ /**
10
+ * An array of Objects with the following shape:
11
+ * {
12
+ * name: String,
13
+ * uploadUrl: String,
14
+ * additionalFields: Object,
15
+ * }
16
+ */
17
+ uploadUrls: {
18
+ type: Array<UploadInfo>,
19
+ default: () => [],
20
+ },
21
+ /**
22
+ * The file types that are allowed to be uploaded. This is a list of MIME types as strings. If the array is
23
+ * empty, all file types are allowed. A list of acceptable MIME types can be found here in the template column:
24
+ * https://www.iana.org/assignments/media-types/media-types.xhtml
25
+ */
26
+ fileTypes: {
27
+ type: Array,
28
+ default: () => [],
29
+ required: true,
30
+ validator(fileTypes: any[]) {
31
+ return fileTypes.every((fileType) => mimeTypes.includes(fileType));
32
+ },
33
+ },
34
+ /**
35
+ * The maximum file size in MB. This is per file, not the total size of all files.
36
+ */
37
+ maxFileSizeMb: {
38
+ type: Number,
39
+ default: 25,
40
+ required: false,
41
+ },
42
+ /**
43
+ * When true, the component will be shorter vertically. This is achieved by giving the parent div
44
+ * flex-direction: row instead of the default flex-direction: column.
45
+ */
46
+ collapsed: {
47
+ type: Boolean,
48
+ default: false,
49
+ required: false,
50
+ },
51
+ });
52
+
53
+ let currentFiles: Array<File> = [];
54
+ let active = false;
55
+ const fileInput = ref(null);
56
+ const emit = defineEmits([
57
+ 'fileSizeError',
58
+ 'fileTypeError',
59
+ 'fileIsAFolderError',
60
+ 'fileDataRead',
61
+ 'duplicateFileMessage',
62
+ 'readyToUpload',
63
+ 'uploadFailure',
64
+ 'uploadSuccess',
65
+ ]);
66
+
67
+ const filterLargeFiles = (files: Array<File>) => {
68
+ // Takes an array of files, and filters out any files that exceed the maxFileSizeMb prop. Emits a
69
+ // fileSizeError event for each file that exceeds the maxFileSizeMb prop. The File API uses bytes, so
70
+ // we have to convert the maxFileSizeMb prop to bytes.
71
+ const maxFileSizeBytes = props.maxFileSizeMb * 1000000;
72
+ return [...files].filter((file) => {
73
+ if (file.size > maxFileSizeBytes) {
74
+ emit('fileSizeError', file.name);
75
+ return false;
76
+ }
77
+ return true;
78
+ });
79
+ };
80
+
81
+ const removeSpaceFromFileNames = (files: Array<File>) => {
82
+ const newFiles = files.map(
83
+ (file) =>
84
+ new File(
85
+ // Return new File object with the modified name (without any space)
86
+ [file],
87
+ file.name.replace(/\s/g, ''),
88
+ { type: file.type, lastModified: file.lastModified },
89
+ ),
90
+ );
91
+ return newFiles;
92
+ };
93
+
94
+ async function verifyMimeType(file: File) {
95
+ // If an empty folder then trigger fileIsAFolderError
96
+ if (file.type === '') {
97
+ emit('fileIsAFolderError');
98
+ }
99
+ return new Promise((resolve, reject) => {
100
+ const fileReader = new FileReader();
101
+ fileReader.onload = (evt) => {
102
+ // @ts-expect-error not sure
103
+ const uint = new Uint8Array(evt.target.result);
104
+ const bytes: string[] = [];
105
+ uint.forEach((byte) => {
106
+ bytes.push(byte.toString(16));
107
+ });
108
+ const hex = bytes.join('').toUpperCase();
109
+ const mimeType = findMimeType(hex);
110
+ if (!mimeType || (props.fileTypes.length > 0 && !props.fileTypes.includes(mimeType))) {
111
+ emit('fileTypeError', file.name);
112
+ return reject();
113
+ }
114
+ return resolve(file);
115
+ };
116
+ fileReader.onerror = (error) => {
117
+ reject(error);
118
+ };
119
+ const blob = file.slice(0, 4);
120
+ fileReader.readAsArrayBuffer(blob);
121
+ });
122
+ }
123
+
124
+ async function uploadSingleFile(file: File) {
125
+ const uploadInfo = props.uploadUrls.find((uploadUrl) => uploadUrl.name === file.name);
126
+ if (!uploadInfo) {
127
+ emit('uploadFailure', {
128
+ name: file.name,
129
+ message: 'No upload URL found for this file.',
130
+ });
131
+ return;
132
+ }
133
+ const form = new FormData();
134
+ if (uploadInfo.additionalFields) {
135
+ Object.entries(uploadInfo.additionalFields).forEach(([key, value]) => {
136
+ form.append(key, value);
137
+ });
138
+ }
139
+ form.append('file', file);
140
+ fetch(uploadInfo.uploadUrl, {
141
+ method: 'POST',
142
+ body: form,
143
+ })
144
+ .then((response) => {
145
+ if (response.status >= 200 && response.status < 300) {
146
+ emit('uploadSuccess', file.name);
147
+ } else {
148
+ emit('uploadFailure', {
149
+ name: file.name,
150
+ message: `Received ${response.status} code from server.`,
151
+ });
152
+ }
153
+ })
154
+ .catch((error) => {
155
+ if (error.response) {
156
+ // The request was made and the server responded with a status code
157
+ // that falls out of the range of 2xx
158
+ emit('uploadFailure', {
159
+ name: file.name,
160
+ message: `Received ${error.response.status} code from server.`,
161
+ });
162
+ } else if (error.request) {
163
+ // The request was made but no response was received
164
+ // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
165
+ // http.ClientRequest in node.js
166
+ emit('uploadFailure', { name: file.name, message: 'The server did not respond.' });
167
+ } else {
168
+ // Something happened in setting up the request that triggered an Error
169
+ emit('uploadFailure', {
170
+ name: file.name,
171
+ message: 'There was an error sending your file to the server.',
172
+ });
173
+ }
174
+ });
175
+ }
176
+
177
+ const readFilesIntoUrl = (files: Array<File>) => {
178
+ files.forEach((file) => {
179
+ const fileReader = new FileReader();
180
+ fileReader.onload = () => {
181
+ emit('fileDataRead', {
182
+ name: file.name,
183
+ type: file.type,
184
+ size: file.size,
185
+ // For non-image files (pdf, docx, etc.)
186
+ data: file.type.includes('application')
187
+ ? URL.createObjectURL(new Blob([file], { type: file.type }))
188
+ : fileReader.result,
189
+ });
190
+ };
191
+ fileReader.readAsDataURL(file);
192
+ });
193
+ };
194
+
195
+ async function verifyFiles(files: Array<File>) {
196
+ const correctlySizedFiles = filterLargeFiles(files);
197
+ const correctlyNamedFiles = removeSpaceFromFileNames(correctlySizedFiles);
198
+
199
+ // Make sure the file is the correct mime type
200
+ let newValidFiles = (await Promise.allSettled(
201
+ correctlyNamedFiles.map(async (file) => verifyMimeType(file)),
202
+ )) as unknown as File[];
203
+ // Filter out undefined values and "rejected"
204
+ // @ts-expect-error basically we're filtering out the non-Files here
205
+ newValidFiles = newValidFiles.filter((file) => file && file.status === 'fulfilled').map((file) => file.value);
206
+
207
+ // If the new file with name already exists in the current files
208
+ // Don't upload the new file and display an error
209
+ const duplicateFiles = newValidFiles.filter(({ name }) => currentFiles.some((file) => file.name === name));
210
+ duplicateFiles.forEach((file) => emit('duplicateFileMessage', file.name));
211
+
212
+ // For new files with unused names, add them to files
213
+ newValidFiles = newValidFiles.filter(({ name }) => !currentFiles.some((file) => file.name === name));
214
+ currentFiles = [...currentFiles, ...newValidFiles];
215
+
216
+ if (newValidFiles.length > 0) {
217
+ emit('readyToUpload', newValidFiles);
218
+ readFilesIntoUrl(newValidFiles);
219
+ }
220
+ }
221
+
222
+ watch(
223
+ () => props.uploadUrls,
224
+ (newUrls, oldUrls) => {
225
+ // For every file that has an associated upload URL, we start the upload
226
+ newUrls.forEach((newUrlPair) => {
227
+ const oldUrl = oldUrls.find(({ name }) => name === newUrlPair.name);
228
+ const fileToUpload = currentFiles.find((file) => file.name === newUrlPair.name);
229
+ if ((!oldUrl || oldUrl.uploadUrl !== newUrlPair.uploadUrl) && fileToUpload) {
230
+ uploadSingleFile(fileToUpload);
231
+ }
232
+ });
233
+ },
234
+ );
235
+
236
+ async function onFileChanged($event: Event) {
237
+ const target = $event.target as HTMLInputElement;
238
+ // The user has selected files from the file picker. We have to filter out any files that exceed the
239
+ // maxFileSizeMb prop.
240
+ if (target && target.files) {
241
+ await verifyFiles([...target.files]);
242
+ }
243
+ }
244
+
245
+ const onDrop = (event: any) => {
246
+ // The user has dropped files onto the component. We have to apply the same logic as if they had
247
+ // selected the files from the file picker which limits the file types to the ones specified in the
248
+ // fileTypes prop.
249
+ active = false;
250
+
251
+ // Use DataTransferItemList interface to access the file(s)
252
+ const dataTransfersAsFiles = [...event.dataTransfer.items]
253
+ .filter((item) => {
254
+ if (item.kind !== 'file') {
255
+ emit('fileTypeError', item.name);
256
+ return false;
257
+ }
258
+ return true;
259
+ })
260
+ .map((item) => item.getAsFile());
261
+
262
+ verifyFiles(dataTransfersAsFiles);
263
+ };
264
+
265
+ const openFilePicker = () => {
266
+ if (fileInput.value) {
267
+ // @ts-expect-error not sure
268
+ fileInput.value.click();
269
+ }
270
+ };
271
+ </script>
272
+
273
+ <template>
274
+ <div
275
+ role="presentation"
276
+ class="es-file-upload align-items-center rounded-sm d-flex flex-column justify-content-center p-150"
277
+ :class="{ active: active }"
278
+ @dragenter.stop.prevent="active = true"
279
+ @dragover.stop.prevent="active = true"
280
+ @dragleave.stop.prevent="active = false"
281
+ @dragend.stop.prevent="onDrop"
282
+ @drop.stop.prevent="onDrop"
283
+ @click.self="openFilePicker"
284
+ @keydown.enter.prevent="openFilePicker"
285
+ @keydown.space.prevent="openFilePicker">
286
+ <div
287
+ class="d-flex justify-content-center flex-column"
288
+ :class="{ 'flex-md-row': collapsed }">
289
+ <div class="align-self-center">
290
+ <icon-upload
291
+ class="d-none d-md-inline-block"
292
+ :class="{ 'mb-200': collapsed }"
293
+ height="32px"
294
+ width="32px" />
295
+ <icon-upload
296
+ class="d-md-none"
297
+ height="24px"
298
+ width="24px" />
299
+ </div>
300
+ <div
301
+ class="align-self-center text-center"
302
+ :class="{ 'px-100': collapsed }">
303
+ <slot name="cta" />
304
+ </div>
305
+ <div class="align-self-center">
306
+ <es-button
307
+ class="d-none d-md-inline w-100 w-md-auto"
308
+ :class="{ 'mb-200': collapsed }"
309
+ @click="openFilePicker">
310
+ <slot name="buttonText">
311
+ <p class="m-0">Browse files</p>
312
+ </slot>
313
+ </es-button>
314
+ <es-button
315
+ class="d-md-none w-100"
316
+ @click="openFilePicker">
317
+ <slot name="buttonText">
318
+ <p class="m-0">Choose files</p>
319
+ </slot>
320
+ </es-button>
321
+ </div>
322
+ </div>
323
+ <div>
324
+ <slot name="helpText" />
325
+ </div>
326
+ <input
327
+ ref="fileInput"
328
+ type="file"
329
+ :accept="fileTypes.join(', ')"
330
+ style="display: none"
331
+ multiple
332
+ @change="onFileChanged($event)" />
333
+ </div>
334
+ </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>