@usssa/component-library 1.0.0-alpha.226 → 1.0.0-alpha.228
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.
- package/README.md +1 -1
- package/package.json +2 -1
- package/src/components/core/UBracket.vue +351 -65
- package/src/components/core/UUploader.vue +146 -55
- package/src/utils/data.ts +3 -1
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usssa/component-library",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.228",
|
|
4
4
|
"description": "A Quasar component library project",
|
|
5
5
|
"productName": "Quasar component library App",
|
|
6
6
|
"author": "Engineering Team <engineering@usssa.com>",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"@usssa/core-client": "^0.0.19",
|
|
40
40
|
"algoliasearch": "4",
|
|
41
41
|
"flag-icons": "^7.2.3",
|
|
42
|
+
"heic2any": "^0.0.4",
|
|
42
43
|
"quasar": "^2.16.0",
|
|
43
44
|
"vue": "^3.4.18",
|
|
44
45
|
"vue-router": "^4.0.12",
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import * as d3 from 'd3'
|
|
3
|
+
import { useQuasar } from 'quasar'
|
|
3
4
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
4
|
-
import UBtnIcon from '
|
|
5
|
+
import { UBtnIcon, USheet, UMenuItem, UBtnStd } from '../../components'
|
|
6
|
+
import { useScreenType } from '../../composables/useScreenType'
|
|
5
7
|
|
|
6
8
|
const emit = defineEmits(['open-expand'])
|
|
7
9
|
|
|
@@ -9,22 +11,58 @@ defineOptions({
|
|
|
9
11
|
name: 'UBracket',
|
|
10
12
|
})
|
|
11
13
|
|
|
14
|
+
const $q = useQuasar()
|
|
15
|
+
const $screen = useScreenType()
|
|
16
|
+
|
|
12
17
|
const props = defineProps({
|
|
13
18
|
bracketData: {
|
|
14
19
|
type: Object,
|
|
15
20
|
required: true,
|
|
16
21
|
},
|
|
22
|
+
bracketSheetApplyLabel: { type: String, default: 'Select Bracket' },
|
|
23
|
+
bracketSheetCancelLabel: { type: String, default: 'Cancel' },
|
|
24
|
+
bracketSheetHeading: { type: String, default: 'Select Bracket Side' },
|
|
25
|
+
bracketTooltip: {
|
|
26
|
+
type: String,
|
|
27
|
+
default: 'Bracket',
|
|
28
|
+
},
|
|
29
|
+
expandTooltip: {
|
|
30
|
+
type: String,
|
|
31
|
+
default: 'Expand Bracket',
|
|
32
|
+
},
|
|
17
33
|
hGap: {
|
|
18
34
|
type: Number,
|
|
19
35
|
default: 650,
|
|
20
36
|
},
|
|
37
|
+
isExpandedView: {
|
|
38
|
+
type: Boolean,
|
|
39
|
+
default: false,
|
|
40
|
+
},
|
|
41
|
+
roundSheetApplyLabel: { type: String, default: 'Select Round' },
|
|
42
|
+
roundSheetCancelLabel: { type: String, default: 'Cancel' },
|
|
43
|
+
roundSheetHeading: { type: String, default: 'Select Round' },
|
|
44
|
+
roundTooltip: {
|
|
45
|
+
type: String,
|
|
46
|
+
default: 'Rounds',
|
|
47
|
+
},
|
|
48
|
+
teamSelectTooltip: {
|
|
49
|
+
type: String,
|
|
50
|
+
default: 'Select Team',
|
|
51
|
+
},
|
|
52
|
+
teamSheetApplyLabel: { type: String, default: 'Select Team' },
|
|
53
|
+
teamSheetCancelLabel: { type: String, default: 'Cancel' },
|
|
54
|
+
teamSheetHeading: { type: String, default: 'Select Team' },
|
|
21
55
|
vGap: {
|
|
22
56
|
type: Number,
|
|
23
57
|
default: 50,
|
|
24
58
|
},
|
|
25
|
-
|
|
26
|
-
type:
|
|
27
|
-
default:
|
|
59
|
+
zoomInTooltip: {
|
|
60
|
+
type: String,
|
|
61
|
+
default: 'Zoom In',
|
|
62
|
+
},
|
|
63
|
+
zoomOutTooltip: {
|
|
64
|
+
type: String,
|
|
65
|
+
default: 'Zoom Out',
|
|
28
66
|
},
|
|
29
67
|
})
|
|
30
68
|
|
|
@@ -45,14 +83,50 @@ const nodeGridPositions = ref({})
|
|
|
45
83
|
const openBracketMenu = ref(false)
|
|
46
84
|
const openMenu = ref(false)
|
|
47
85
|
const openRoundMenu = ref(false)
|
|
86
|
+
const roundSheet = ref([])
|
|
87
|
+
const bracketSheet = ref([])
|
|
88
|
+
const teamSheet = ref([])
|
|
89
|
+
const tempSelectedRoundId = ref('')
|
|
90
|
+
const tempSelectedBracketSide = ref('')
|
|
91
|
+
|
|
92
|
+
const applyRoundSheetSelection = () => {
|
|
93
|
+
const selectedRound = props.bracketData.round.find(
|
|
94
|
+
(r) => `${r.roundNo}-${r.columnNo}` === tempSelectedRoundId.value
|
|
95
|
+
)
|
|
96
|
+
if (selectedRound) scrollToRound(selectedRound)
|
|
97
|
+
roundSheet.value = []
|
|
98
|
+
}
|
|
99
|
+
const cancelRoundSheetSelection = () => {
|
|
100
|
+
roundSheet.value = []
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const toggleBracketSideInSheet = (side) => {
|
|
104
|
+
tempSelectedBracketSide.value = side
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const toggleRoundInSheet = (round) => {
|
|
108
|
+
tempSelectedRoundId.value = `${round.roundNo}-${round.columnNo}`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const applyBracketSheetSelection = () => {
|
|
112
|
+
scrollToBracketSide(tempSelectedBracketSide.value)
|
|
113
|
+
bracketSheet.value = []
|
|
114
|
+
}
|
|
115
|
+
const cancelBracketSheetSelection = () => {
|
|
116
|
+
bracketSheet.value = []
|
|
117
|
+
}
|
|
118
|
+
const openRoundSheet = () => {
|
|
119
|
+
tempSelectedRoundId.value = ''
|
|
120
|
+
roundSheet.value = [{ open: true, height: $q.screen.height - 150 }]
|
|
121
|
+
}
|
|
122
|
+
const openBracketSheet = () => {
|
|
123
|
+
bracketSheet.value = [{ open: true, height: 200 }]
|
|
124
|
+
}
|
|
48
125
|
const selectedTeamId = ref('')
|
|
49
126
|
const svgRef = ref(null)
|
|
127
|
+
const tempSelectedTeamId = ref('')
|
|
50
128
|
const zoomRef = ref(null)
|
|
51
129
|
|
|
52
|
-
const openExpandView = () => {
|
|
53
|
-
emit('open-expand')
|
|
54
|
-
}
|
|
55
|
-
|
|
56
130
|
// Flatten for rendering logic
|
|
57
131
|
const games = computed(() =>
|
|
58
132
|
props.bracketData.round.flatMap((r, i) =>
|
|
@@ -138,6 +212,17 @@ const toolbarTeams = computed(() => {
|
|
|
138
212
|
return seedTeams
|
|
139
213
|
})
|
|
140
214
|
|
|
215
|
+
const applyTeamSheetSelection = () => {
|
|
216
|
+
selectedTeamId.value = tempSelectedTeamId.value
|
|
217
|
+
teamSheet.value = []
|
|
218
|
+
renderBracket()
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const cancelTeamSheetSelection = () => {
|
|
222
|
+
teamSheet.value = []
|
|
223
|
+
tempSelectedTeamId.value = selectedTeamId.value
|
|
224
|
+
}
|
|
225
|
+
|
|
141
226
|
// Compute all node positions using a row/column approach
|
|
142
227
|
const computeBracketLayout = () => {
|
|
143
228
|
const newPositions = {}
|
|
@@ -274,7 +359,6 @@ const descriptiveRoundName = (round) => {
|
|
|
274
359
|
return `Winner ${round.roundName}`
|
|
275
360
|
}
|
|
276
361
|
|
|
277
|
-
// Convert row/col to x/y coordinates for rendering
|
|
278
362
|
const getNodePosition = (id) => {
|
|
279
363
|
const positions = nodeGridPositions.value
|
|
280
364
|
if (id && positions[id]) {
|
|
@@ -356,6 +440,62 @@ const getTeamPath = (teamId) => {
|
|
|
356
440
|
}
|
|
357
441
|
}
|
|
358
442
|
|
|
443
|
+
const openExpandView = () => {
|
|
444
|
+
emit('open-expand')
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const openTeamOptions = () => {
|
|
448
|
+
if ($screen.value.isMobile) {
|
|
449
|
+
tempSelectedTeamId.value = selectedTeamId.value
|
|
450
|
+
teamSheet.value = [{ open: true, height: $q.screen.height - 150 }]
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Placed this before onMounted and any watcher/event listener usage
|
|
456
|
+
const reinitZoomAndLayout = () => {
|
|
457
|
+
computeBracketLayout()
|
|
458
|
+
const viewportWidth = svgRef.value.clientWidth
|
|
459
|
+
const viewportHeight = svgRef.value.clientHeight
|
|
460
|
+
const uniqueColumns = [
|
|
461
|
+
...new Set(props.bracketData.round.map((r) => r.columnNo)),
|
|
462
|
+
]
|
|
463
|
+
const numColumns = uniqueColumns.length
|
|
464
|
+
const contentWidth = 50 + (numColumns - 1) * props.hGap + nodeWidth + 50
|
|
465
|
+
let maxRow = 0
|
|
466
|
+
Object.values(nodeGridPositions.value).forEach(({ row }) => {
|
|
467
|
+
if (row > maxRow) maxRow = row
|
|
468
|
+
})
|
|
469
|
+
const contentHeight = 50 + (maxRow - 1) * props.vGap + nodeHeight + 50
|
|
470
|
+
|
|
471
|
+
// Update zoom clamping
|
|
472
|
+
const zoomBehavior = zoomRef.value
|
|
473
|
+
if (zoomBehavior) {
|
|
474
|
+
zoomBehavior.on('zoom', (event) => {
|
|
475
|
+
let { transform } = event
|
|
476
|
+
const { k } = transform
|
|
477
|
+
|
|
478
|
+
const minTx = viewportWidth - contentWidth * k
|
|
479
|
+
const maxTx = 0
|
|
480
|
+
const minTy = viewportHeight - contentHeight * k
|
|
481
|
+
const maxTy = 0
|
|
482
|
+
|
|
483
|
+
const tx =
|
|
484
|
+
minTx > 0 ? minTx / 2 : Math.max(minTx, Math.min(maxTx, transform.x))
|
|
485
|
+
const ty =
|
|
486
|
+
minTy > 0 ? minTy / 2 : Math.max(minTy, Math.min(maxTy, transform.y))
|
|
487
|
+
|
|
488
|
+
const clampedTransform = d3.zoomIdentity.translate(tx, ty).scale(k)
|
|
489
|
+
|
|
490
|
+
gRef.value && d3.select(gRef.value).attr('transform', clampedTransform)
|
|
491
|
+
currentTransform.value = clampedTransform
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Re-render the bracket
|
|
496
|
+
renderBracket()
|
|
497
|
+
}
|
|
498
|
+
|
|
359
499
|
const renderBracket = () => {
|
|
360
500
|
computeBracketLayout()
|
|
361
501
|
const g = d3.select(gRef.value)
|
|
@@ -553,7 +693,7 @@ const renderBracket = () => {
|
|
|
553
693
|
.attr('height', 24)
|
|
554
694
|
.style('cursor', 'pointer')
|
|
555
695
|
.append('title')
|
|
556
|
-
.text('
|
|
696
|
+
.text('Dallas, TX')
|
|
557
697
|
}
|
|
558
698
|
})
|
|
559
699
|
|
|
@@ -696,14 +836,14 @@ const renderBracket = () => {
|
|
|
696
836
|
|
|
697
837
|
// After all edges are drawn, move highlighted edges to top
|
|
698
838
|
nextTick(() => {
|
|
699
|
-
const gEl = gRef.value
|
|
839
|
+
const gEl = gRef.value
|
|
700
840
|
if (gEl) {
|
|
701
|
-
const highlightedEdges = gEl.querySelectorAll('.edge-path.highlighted')
|
|
841
|
+
const highlightedEdges = gEl.querySelectorAll('.edge-path.highlighted')
|
|
702
842
|
highlightedEdges.forEach((el) => {
|
|
703
|
-
gEl.appendChild(el)
|
|
704
|
-
})
|
|
843
|
+
gEl.appendChild(el)
|
|
844
|
+
})
|
|
705
845
|
}
|
|
706
|
-
})
|
|
846
|
+
})
|
|
707
847
|
}
|
|
708
848
|
|
|
709
849
|
const scrollToBracketSide = (side) => {
|
|
@@ -786,6 +926,14 @@ const setSelectedTeamId = (teamId) => {
|
|
|
786
926
|
openMenu.value = false
|
|
787
927
|
}
|
|
788
928
|
|
|
929
|
+
const toggleTeamInSheet = (teamId) => {
|
|
930
|
+
if (teamId === tempSelectedTeamId.value) {
|
|
931
|
+
tempSelectedTeamId.value = ''
|
|
932
|
+
return
|
|
933
|
+
}
|
|
934
|
+
tempSelectedTeamId.value = teamId
|
|
935
|
+
}
|
|
936
|
+
|
|
789
937
|
const zoomIn = () => {
|
|
790
938
|
const svg = d3.select(svgRef.value)
|
|
791
939
|
if (zoomRef.value) {
|
|
@@ -816,50 +964,6 @@ const zoomOut = () => {
|
|
|
816
964
|
}
|
|
817
965
|
}
|
|
818
966
|
|
|
819
|
-
// Place this before onMounted and any watcher/event listener usage
|
|
820
|
-
const reinitZoomAndLayout = () => {
|
|
821
|
-
computeBracketLayout()
|
|
822
|
-
const viewportWidth = svgRef.value.clientWidth
|
|
823
|
-
const viewportHeight = svgRef.value.clientHeight
|
|
824
|
-
const uniqueColumns = [
|
|
825
|
-
...new Set(props.bracketData.round.map((r) => r.columnNo)),
|
|
826
|
-
]
|
|
827
|
-
const numColumns = uniqueColumns.length
|
|
828
|
-
const contentWidth = 50 + (numColumns - 1) * props.hGap + nodeWidth + 50
|
|
829
|
-
let maxRow = 0
|
|
830
|
-
Object.values(nodeGridPositions.value).forEach(({ row }) => {
|
|
831
|
-
if (row > maxRow) maxRow = row
|
|
832
|
-
})
|
|
833
|
-
const contentHeight = 50 + (maxRow - 1) * props.vGap + nodeHeight + 50
|
|
834
|
-
|
|
835
|
-
// Update zoom clamping
|
|
836
|
-
const zoomBehavior = zoomRef.value
|
|
837
|
-
if (zoomBehavior) {
|
|
838
|
-
zoomBehavior.on('zoom', (event) => {
|
|
839
|
-
let { transform } = event
|
|
840
|
-
const { k } = transform
|
|
841
|
-
|
|
842
|
-
const minTx = viewportWidth - contentWidth * k
|
|
843
|
-
const maxTx = 0
|
|
844
|
-
const minTy = viewportHeight - contentHeight * k
|
|
845
|
-
const maxTy = 0
|
|
846
|
-
|
|
847
|
-
const tx =
|
|
848
|
-
minTx > 0 ? minTx / 2 : Math.max(minTx, Math.min(maxTx, transform.x))
|
|
849
|
-
const ty =
|
|
850
|
-
minTy > 0 ? minTy / 2 : Math.max(minTy, Math.min(maxTy, transform.y))
|
|
851
|
-
|
|
852
|
-
const clampedTransform = d3.zoomIdentity.translate(tx, ty).scale(k)
|
|
853
|
-
|
|
854
|
-
gRef.value && d3.select(gRef.value).attr('transform', clampedTransform)
|
|
855
|
-
currentTransform.value = clampedTransform
|
|
856
|
-
})
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
// Re-render the bracket
|
|
860
|
-
renderBracket()
|
|
861
|
-
}
|
|
862
|
-
|
|
863
967
|
onMounted(() => {
|
|
864
968
|
// Initial layout computation to determine extents
|
|
865
969
|
computeBracketLayout()
|
|
@@ -1021,7 +1125,7 @@ onUnmounted(() => {
|
|
|
1021
1125
|
:anchor="anchor"
|
|
1022
1126
|
:self="self"
|
|
1023
1127
|
size="md"
|
|
1024
|
-
tooltip="
|
|
1128
|
+
:tooltip="zoomOutTooltip"
|
|
1025
1129
|
@click="zoomOut"
|
|
1026
1130
|
></UBtnIcon>
|
|
1027
1131
|
<UBtnIcon
|
|
@@ -1030,16 +1134,17 @@ onUnmounted(() => {
|
|
|
1030
1134
|
:anchor="anchor"
|
|
1031
1135
|
:self="self"
|
|
1032
1136
|
size="md"
|
|
1033
|
-
tooltip="
|
|
1137
|
+
:tooltip="zoomInTooltip"
|
|
1034
1138
|
@click="zoomIn"
|
|
1035
1139
|
></UBtnIcon>
|
|
1036
1140
|
<UBtnIcon
|
|
1141
|
+
v-if="!$screen.isMobile"
|
|
1037
1142
|
iconClass="fa-kit-duotone fa-bracket"
|
|
1038
1143
|
ariaLabel="Rounds"
|
|
1039
1144
|
:anchor="anchor"
|
|
1040
1145
|
:self="self"
|
|
1041
1146
|
size="md"
|
|
1042
|
-
tooltip="
|
|
1147
|
+
:tooltip="roundTooltip"
|
|
1043
1148
|
>
|
|
1044
1149
|
<template v-slot:menu>
|
|
1045
1150
|
<q-menu v-model="openRoundMenu">
|
|
@@ -1062,12 +1167,23 @@ onUnmounted(() => {
|
|
|
1062
1167
|
</template>
|
|
1063
1168
|
</UBtnIcon>
|
|
1064
1169
|
<UBtnIcon
|
|
1170
|
+
v-if="$screen.isMobile"
|
|
1171
|
+
iconClass="fa-kit-duotone fa-bracket"
|
|
1172
|
+
ariaLabel="Rounds"
|
|
1173
|
+
:anchor="anchor"
|
|
1174
|
+
:self="self"
|
|
1175
|
+
size="md"
|
|
1176
|
+
:tooltip="roundTooltip"
|
|
1177
|
+
@click="openRoundSheet"
|
|
1178
|
+
/>
|
|
1179
|
+
<UBtnIcon
|
|
1180
|
+
v-if="!$screen.isMobile"
|
|
1065
1181
|
iconClass="fa-kit-duotone fa-game-center"
|
|
1066
1182
|
ariaLabel="Bracket"
|
|
1067
1183
|
:anchor="anchor"
|
|
1068
1184
|
:self="self"
|
|
1069
1185
|
size="md"
|
|
1070
|
-
tooltip="
|
|
1186
|
+
:tooltip="bracketTooltip"
|
|
1071
1187
|
>
|
|
1072
1188
|
<template v-slot:menu>
|
|
1073
1189
|
<q-menu v-model="openBracketMenu">
|
|
@@ -1086,12 +1202,23 @@ onUnmounted(() => {
|
|
|
1086
1202
|
</template>
|
|
1087
1203
|
</UBtnIcon>
|
|
1088
1204
|
<UBtnIcon
|
|
1205
|
+
v-if="$screen.isMobile"
|
|
1206
|
+
iconClass="fa-kit-duotone fa-game-center"
|
|
1207
|
+
ariaLabel="Bracket"
|
|
1208
|
+
:anchor="anchor"
|
|
1209
|
+
:self="self"
|
|
1210
|
+
size="md"
|
|
1211
|
+
:tooltip="bracketTooltip"
|
|
1212
|
+
@click="openBracketSheet"
|
|
1213
|
+
/>
|
|
1214
|
+
<UBtnIcon
|
|
1215
|
+
v-if="!$screen.isMobile"
|
|
1089
1216
|
iconClass="fa-kit-duotone fa-jersey"
|
|
1090
1217
|
ariaLabel="Select Team"
|
|
1091
1218
|
:anchor="anchor"
|
|
1092
1219
|
:self="self"
|
|
1093
1220
|
size="md"
|
|
1094
|
-
tooltip="
|
|
1221
|
+
:tooltip="selectTeamTooltip"
|
|
1095
1222
|
>
|
|
1096
1223
|
<template v-slot:menu>
|
|
1097
1224
|
<q-menu v-model="openMenu">
|
|
@@ -1112,6 +1239,17 @@ onUnmounted(() => {
|
|
|
1112
1239
|
</q-menu>
|
|
1113
1240
|
</template>
|
|
1114
1241
|
</UBtnIcon>
|
|
1242
|
+
<UBtnIcon
|
|
1243
|
+
v-if="$screen.isMobile"
|
|
1244
|
+
iconClass="fa-kit-duotone fa-jersey"
|
|
1245
|
+
ariaLabel="Select Team"
|
|
1246
|
+
:anchor="anchor"
|
|
1247
|
+
:self="self"
|
|
1248
|
+
size="md"
|
|
1249
|
+
:tooltip="selectTeamTooltip"
|
|
1250
|
+
@click="openTeamOptions"
|
|
1251
|
+
>
|
|
1252
|
+
</UBtnIcon>
|
|
1115
1253
|
<UBtnIcon
|
|
1116
1254
|
v-if="!props.isExpandedView"
|
|
1117
1255
|
iconClass="fa-kit-duotone fa-expand"
|
|
@@ -1119,12 +1257,154 @@ onUnmounted(() => {
|
|
|
1119
1257
|
:anchor="anchor"
|
|
1120
1258
|
:self="self"
|
|
1121
1259
|
size="md"
|
|
1122
|
-
tooltip="
|
|
1260
|
+
:tooltip="expandTooltip"
|
|
1123
1261
|
@click="openExpandView"
|
|
1124
1262
|
></UBtnIcon>
|
|
1125
1263
|
</div>
|
|
1126
1264
|
</div>
|
|
1127
1265
|
</div>
|
|
1266
|
+
<USheet
|
|
1267
|
+
v-model:dialogs="teamSheet"
|
|
1268
|
+
data-test-id="team-selection-sheet"
|
|
1269
|
+
dialog-class="bracket-sheet"
|
|
1270
|
+
:heading="props.teamSheetHeading"
|
|
1271
|
+
show-action-buttons
|
|
1272
|
+
>
|
|
1273
|
+
<template #content>
|
|
1274
|
+
<div>
|
|
1275
|
+
<template v-for="team in toolbarTeams" :key="team.id">
|
|
1276
|
+
<UMenuItem
|
|
1277
|
+
class="season-menu-item"
|
|
1278
|
+
:data-test-id="`season-sheet-item-${team.value}`"
|
|
1279
|
+
in-sheet
|
|
1280
|
+
:label="team.label"
|
|
1281
|
+
:selected="tempSelectedTeamId === team.id"
|
|
1282
|
+
@onClick="toggleTeamInSheet(team.id)"
|
|
1283
|
+
>
|
|
1284
|
+
<template #trailing_slot>
|
|
1285
|
+
<q-icon
|
|
1286
|
+
v-if="tempSelectedTeamId === team.id"
|
|
1287
|
+
class="fa-kit-duotone fa-circle-check"
|
|
1288
|
+
color="primary"
|
|
1289
|
+
/>
|
|
1290
|
+
</template>
|
|
1291
|
+
</UMenuItem>
|
|
1292
|
+
</template>
|
|
1293
|
+
</div>
|
|
1294
|
+
</template>
|
|
1295
|
+
<template #action_primary_one>
|
|
1296
|
+
<UBtnStd
|
|
1297
|
+
color="primary"
|
|
1298
|
+
data-test-id="team-sheet-cancel-button"
|
|
1299
|
+
:label="props.teamSheetCancelLabel"
|
|
1300
|
+
outline
|
|
1301
|
+
@onClick="cancelTeamSheetSelection"
|
|
1302
|
+
/>
|
|
1303
|
+
</template>
|
|
1304
|
+
<template #action_primary_two>
|
|
1305
|
+
<UBtnStd
|
|
1306
|
+
color="primary"
|
|
1307
|
+
data-test-id="team-sheet-apply-button"
|
|
1308
|
+
:label="props.teamSheetApplyLabel"
|
|
1309
|
+
@onClick="applyTeamSheetSelection"
|
|
1310
|
+
/>
|
|
1311
|
+
</template>
|
|
1312
|
+
</USheet>
|
|
1313
|
+
<USheet
|
|
1314
|
+
v-model:dialogs="roundSheet"
|
|
1315
|
+
dialog-class="bracket-sheet"
|
|
1316
|
+
:heading="props.roundSheetHeading"
|
|
1317
|
+
show-action-buttons
|
|
1318
|
+
>
|
|
1319
|
+
<template #content>
|
|
1320
|
+
<div>
|
|
1321
|
+
<template
|
|
1322
|
+
v-for="round in bracketData.round.filter(
|
|
1323
|
+
(r) => r.roundName && r.roundName.trim() !== ''
|
|
1324
|
+
)"
|
|
1325
|
+
:key="`${round.roundNo}-${round.columnNo}`"
|
|
1326
|
+
>
|
|
1327
|
+
<UMenuItem
|
|
1328
|
+
class="season-menu-item"
|
|
1329
|
+
:label="descriptiveRoundName(round)"
|
|
1330
|
+
:selected="
|
|
1331
|
+
tempSelectedRoundId === `${round.roundNo}-${round.columnNo}`
|
|
1332
|
+
"
|
|
1333
|
+
in-sheet
|
|
1334
|
+
@onClick="toggleRoundInSheet(round)"
|
|
1335
|
+
>
|
|
1336
|
+
<template #trailing_slot>
|
|
1337
|
+
<q-icon
|
|
1338
|
+
v-if="
|
|
1339
|
+
tempSelectedRoundId === `${round.roundNo}-${round.columnNo}`
|
|
1340
|
+
"
|
|
1341
|
+
class="fa-kit-duotone fa-circle-check"
|
|
1342
|
+
color="primary"
|
|
1343
|
+
/>
|
|
1344
|
+
</template>
|
|
1345
|
+
</UMenuItem>
|
|
1346
|
+
</template>
|
|
1347
|
+
</div>
|
|
1348
|
+
</template>
|
|
1349
|
+
<template #action_primary_one>
|
|
1350
|
+
<UBtnStd
|
|
1351
|
+
color="primary"
|
|
1352
|
+
:label="props.roundSheetCancelLabel"
|
|
1353
|
+
outline
|
|
1354
|
+
@onClick="cancelRoundSheetSelection"
|
|
1355
|
+
/>
|
|
1356
|
+
</template>
|
|
1357
|
+
<template #action_primary_two>
|
|
1358
|
+
<UBtnStd
|
|
1359
|
+
color="primary"
|
|
1360
|
+
:label="props.roundSheetApplyLabel"
|
|
1361
|
+
@onClick="applyRoundSheetSelection"
|
|
1362
|
+
/>
|
|
1363
|
+
</template>
|
|
1364
|
+
</USheet>
|
|
1365
|
+
<USheet
|
|
1366
|
+
v-model:dialogs="bracketSheet"
|
|
1367
|
+
dialog-class="bracket-sheet"
|
|
1368
|
+
:heading="props.bracketSheetHeading"
|
|
1369
|
+
show-action-buttons
|
|
1370
|
+
>
|
|
1371
|
+
<template #content>
|
|
1372
|
+
<div>
|
|
1373
|
+
<template v-for="side in bracketSides" :key="side.value">
|
|
1374
|
+
<UMenuItem
|
|
1375
|
+
class="season-menu-item"
|
|
1376
|
+
in-sheet
|
|
1377
|
+
:label="side.label"
|
|
1378
|
+
:selected="tempSelectedBracketSide === side.value"
|
|
1379
|
+
@onClick="toggleBracketSideInSheet(side.value)"
|
|
1380
|
+
>
|
|
1381
|
+
<template #trailing_slot>
|
|
1382
|
+
<q-icon
|
|
1383
|
+
v-if="tempSelectedBracketSide === side.value"
|
|
1384
|
+
class="fa-kit-duotone fa-circle-check"
|
|
1385
|
+
color="primary"
|
|
1386
|
+
/>
|
|
1387
|
+
</template>
|
|
1388
|
+
</UMenuItem>
|
|
1389
|
+
</template>
|
|
1390
|
+
</div>
|
|
1391
|
+
</template>
|
|
1392
|
+
<template #action_primary_one>
|
|
1393
|
+
<UBtnStd
|
|
1394
|
+
color="primary"
|
|
1395
|
+
:label="props.bracketSheetCancelLabel"
|
|
1396
|
+
outline
|
|
1397
|
+
@onClick="cancelBracketSheetSelection"
|
|
1398
|
+
/>
|
|
1399
|
+
</template>
|
|
1400
|
+
<template #action_primary_two>
|
|
1401
|
+
<UBtnStd
|
|
1402
|
+
color="primary"
|
|
1403
|
+
:label="props.bracketSheetApplyLabel"
|
|
1404
|
+
@onClick="applyBracketSheetSelection"
|
|
1405
|
+
/>
|
|
1406
|
+
</template>
|
|
1407
|
+
</USheet>
|
|
1128
1408
|
</template>
|
|
1129
1409
|
|
|
1130
1410
|
<style scoped lang="sass">
|
|
@@ -1181,6 +1461,12 @@ svg
|
|
|
1181
1461
|
height: 100% /* Make SVG fill the container */
|
|
1182
1462
|
display: block
|
|
1183
1463
|
|
|
1464
|
+
.bracket-sheet
|
|
1465
|
+
.main-content-dialog
|
|
1466
|
+
padding-top: $ba !important
|
|
1467
|
+
.q-item
|
|
1468
|
+
margin-bottom: $xs
|
|
1469
|
+
|
|
1184
1470
|
.bracket-toolbar
|
|
1185
1471
|
bottom: 1rem /* Position from the bottom */
|
|
1186
1472
|
left: 50% /* Center horizontally */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { useQuasar } from 'quasar'
|
|
3
|
-
import { computed, ref } from 'vue'
|
|
3
|
+
import { computed, ref, watch } from 'vue'
|
|
4
|
+
import heic2any from 'heic2any'
|
|
4
5
|
import { fixStringLength, formatDate, getFileCategory } from '../../utils/data'
|
|
5
6
|
import UBtnIcon from './UBtnIcon.vue'
|
|
6
7
|
import UBtnStd from './UBtnStd.vue'
|
|
@@ -8,6 +9,7 @@ import UInputTextStd from './UInputTextStd.vue'
|
|
|
8
9
|
import UTooltip from './UTooltip.vue'
|
|
9
10
|
|
|
10
11
|
const emit = defineEmits([
|
|
12
|
+
'conversionStatus',
|
|
11
13
|
'onUploadFactory',
|
|
12
14
|
'onViewFile',
|
|
13
15
|
'getFilesOnAdded',
|
|
@@ -29,6 +31,10 @@ const props = defineProps({
|
|
|
29
31
|
type: String,
|
|
30
32
|
default: 'Cancel',
|
|
31
33
|
},
|
|
34
|
+
conversionText: {
|
|
35
|
+
type: String,
|
|
36
|
+
default: 'Converting into jpeg image',
|
|
37
|
+
},
|
|
32
38
|
dataTestId: {
|
|
33
39
|
type: String,
|
|
34
40
|
default: 'uploader',
|
|
@@ -116,9 +122,18 @@ const props = defineProps({
|
|
|
116
122
|
|
|
117
123
|
const $q = useQuasar()
|
|
118
124
|
|
|
125
|
+
const filesConverting = ref([]) // track converting state per file
|
|
119
126
|
const fileDisplayName = ref([])
|
|
120
127
|
const isEditing = ref([])
|
|
121
128
|
const uploaderRef = ref(null)
|
|
129
|
+
const autoUploadRef = computed({
|
|
130
|
+
get() {
|
|
131
|
+
return props.autoUpload
|
|
132
|
+
},
|
|
133
|
+
set(val) {
|
|
134
|
+
return val
|
|
135
|
+
},
|
|
136
|
+
})
|
|
122
137
|
const isSmallWidthDevices = computed(() => $q.screen.width < 350)
|
|
123
138
|
const uploadedFiles = computed({
|
|
124
139
|
get() {
|
|
@@ -143,13 +158,69 @@ const handleViewClick = (file) => {
|
|
|
143
158
|
return emit('onViewFile', file)
|
|
144
159
|
}
|
|
145
160
|
|
|
146
|
-
const onFileAdded = (files) => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
161
|
+
const onFileAdded = async (files) => {
|
|
162
|
+
try {
|
|
163
|
+
const uploader = uploaderRef.value
|
|
164
|
+
if (!uploader) return // uploader destroyed
|
|
165
|
+
|
|
166
|
+
const convertedFiles = []
|
|
167
|
+
|
|
168
|
+
const heicConversions = files.map(async (file) => {
|
|
169
|
+
const key = file.__key
|
|
170
|
+
const isHeic =
|
|
171
|
+
/\.(heic|heif)$/i.test(file.name) ||
|
|
172
|
+
file.type === 'image/heic' ||
|
|
173
|
+
file.type === 'image/heif'
|
|
174
|
+
|
|
175
|
+
if (!isHeic) return
|
|
176
|
+
|
|
177
|
+
// Add to converting array
|
|
178
|
+
filesConverting.value.push({ key, name: file.name, converting: true })
|
|
179
|
+
|
|
180
|
+
// Safely remove HEIC file
|
|
181
|
+
if (uploaderRef.value) {
|
|
182
|
+
const idx = uploader.files.findIndex((f) => f.__key === key)
|
|
183
|
+
if (idx !== -1) uploader.removeFile(uploader.files[idx])
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const convertedBlob = await heic2any({
|
|
188
|
+
blob: file,
|
|
189
|
+
toType: 'image/jpeg',
|
|
190
|
+
quality: 0.8,
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// Skip if uploader closed while converting
|
|
194
|
+
if (!uploaderRef.value) return
|
|
195
|
+
|
|
196
|
+
const newFile = new File(
|
|
197
|
+
[convertedBlob],
|
|
198
|
+
file.name.replace(/\.(heic|heif)$/i, '.jpeg'),
|
|
199
|
+
{ type: 'image/jpeg' }
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
newFile.__key = key
|
|
203
|
+
newFile.displayName = newFile.name
|
|
204
|
+
convertedFiles.push(newFile)
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.error('HEIC conversion failed:', err)
|
|
207
|
+
} finally {
|
|
208
|
+
const index = filesConverting.value.findIndex((f) => f.key === key)
|
|
209
|
+
if (index !== -1) filesConverting.value.splice(index, 1)
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
await Promise.allSettled(heicConversions)
|
|
214
|
+
|
|
215
|
+
// Safely add converted files
|
|
216
|
+
if (convertedFiles.length > 0 && uploaderRef.value) {
|
|
217
|
+
uploaderRef.value.addFiles(convertedFiles)
|
|
218
|
+
}
|
|
151
219
|
|
|
152
|
-
|
|
220
|
+
emit('getFilesOnAdded', uploaderRef.value?.files || [])
|
|
221
|
+
} catch (err) {
|
|
222
|
+
console.warn('error during conversion:', err.message || err)
|
|
223
|
+
}
|
|
153
224
|
}
|
|
154
225
|
|
|
155
226
|
const onFileUploaded = ({ files, xhr }) => {
|
|
@@ -176,7 +247,6 @@ const removeFile = (payload) => {
|
|
|
176
247
|
if (uploadedIndex !== -1) {
|
|
177
248
|
const updated = [...uploadedFiles.value]
|
|
178
249
|
updated.splice(uploadedIndex, 1)
|
|
179
|
-
|
|
180
250
|
emit('update:uploadedFiles', updated)
|
|
181
251
|
}
|
|
182
252
|
|
|
@@ -195,6 +265,8 @@ const scopedFiles = (files) => {
|
|
|
195
265
|
__status: 'uploaded',
|
|
196
266
|
uploaded: true,
|
|
197
267
|
__key: uploadedFile.__key || uploadedFile.name,
|
|
268
|
+
displayName: uploadedFile.displayName || uploadedFile.name,
|
|
269
|
+
name: uploadedFile.displayName || uploadedFile.name,
|
|
198
270
|
})
|
|
199
271
|
}
|
|
200
272
|
})
|
|
@@ -217,6 +289,11 @@ const upload = () => {
|
|
|
217
289
|
uploaderRef.value.upload()
|
|
218
290
|
}
|
|
219
291
|
|
|
292
|
+
watch(
|
|
293
|
+
() => filesConverting.value.length,
|
|
294
|
+
(newVal) => emit('conversionStatus', newVal > 0)
|
|
295
|
+
)
|
|
296
|
+
|
|
220
297
|
defineExpose({ upload })
|
|
221
298
|
</script>
|
|
222
299
|
<template>
|
|
@@ -224,7 +301,7 @@ defineExpose({ upload })
|
|
|
224
301
|
<q-uploader
|
|
225
302
|
v-bind="$attrs"
|
|
226
303
|
class="u-uploader"
|
|
227
|
-
:auto-upload="
|
|
304
|
+
:auto-upload="autoUploadRef"
|
|
228
305
|
:batch="batch"
|
|
229
306
|
:dataTestId="dataTestId"
|
|
230
307
|
label="Drag and drop your file or select choose file."
|
|
@@ -239,55 +316,69 @@ defineExpose({ upload })
|
|
|
239
316
|
>
|
|
240
317
|
<template v-slot:header="scope">
|
|
241
318
|
<div
|
|
242
|
-
v-if="
|
|
243
|
-
class="
|
|
319
|
+
v-if="filesConverting.some((f) => f.converting)"
|
|
320
|
+
class="row items-center justify-between uploaded-container"
|
|
244
321
|
>
|
|
245
|
-
<div class="
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
<div class="text-body-md q-mt-ba" dataTestId="description">
|
|
251
|
-
{{ description }}
|
|
252
|
-
</div>
|
|
322
|
+
<div class="row items-center">
|
|
323
|
+
<q-spinner class="q-mr-sm" color="black" size="20px" />
|
|
324
|
+
<span class="text-black">
|
|
325
|
+
{{ conversionText }}
|
|
326
|
+
</span>
|
|
253
327
|
</div>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
>
|
|
266
|
-
{{ selectFileBtnLabel }}
|
|
267
|
-
<q-uploader-add-trigger
|
|
268
|
-
v-if="!selectFileBtnDisable"
|
|
269
|
-
:aria-label="selectFileBtnLabel"
|
|
270
|
-
/>
|
|
271
|
-
<UTooltip
|
|
272
|
-
v-if="selectFileBtnTooltip?.length"
|
|
273
|
-
anchor="bottom middle"
|
|
274
|
-
:description="selectFileBtnTooltip"
|
|
275
|
-
:offset="[4, 4]"
|
|
276
|
-
self="top middle"
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<div v-else>
|
|
331
|
+
<div
|
|
332
|
+
v-if="scope.canAddFiles && !uploadedFiles.length"
|
|
333
|
+
class="q-py-md q-px-xl bg-neutral-2 uploader-content"
|
|
334
|
+
>
|
|
335
|
+
<div class="text-center">
|
|
336
|
+
<img
|
|
337
|
+
alt="Upload Files"
|
|
338
|
+
src="../../assets/upload-illustration.svg"
|
|
277
339
|
/>
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
340
|
+
<div class="text-body-md q-mt-ba" dataTestId="description">
|
|
341
|
+
{{ description }}
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
<div class="list-items row">
|
|
345
|
+
<UBtnStd
|
|
346
|
+
class="q-mt-md selectFileBtn"
|
|
347
|
+
:aria-label="selectFileBtnLabel"
|
|
348
|
+
color="primary"
|
|
349
|
+
dataTestId="select-file-btn"
|
|
350
|
+
:disable="selectFileBtnDisable"
|
|
351
|
+
:full-width="true"
|
|
352
|
+
:label="selectFileBtnLabel"
|
|
353
|
+
size="lg"
|
|
354
|
+
@click="scope.pickFiles"
|
|
355
|
+
>
|
|
356
|
+
{{ selectFileBtnLabel }}
|
|
357
|
+
<q-uploader-add-trigger
|
|
358
|
+
v-if="!selectFileBtnDisable"
|
|
359
|
+
:aria-label="selectFileBtnLabel"
|
|
360
|
+
/>
|
|
361
|
+
<UTooltip
|
|
362
|
+
v-if="selectFileBtnTooltip?.length"
|
|
363
|
+
anchor="bottom middle"
|
|
364
|
+
:description="selectFileBtnTooltip"
|
|
365
|
+
:offset="[4, 4]"
|
|
366
|
+
self="top middle"
|
|
367
|
+
/>
|
|
368
|
+
</UBtnStd>
|
|
369
|
+
<UBtnStd
|
|
370
|
+
v-if="scope.canUpload && showUploadBtn"
|
|
371
|
+
class="q-mt-md q-mb-md"
|
|
372
|
+
color="primary"
|
|
373
|
+
dataTestId="upload-btn"
|
|
374
|
+
flat
|
|
375
|
+
icon="cloud_upload"
|
|
376
|
+
size="sm"
|
|
377
|
+
@click="scope.upload"
|
|
378
|
+
>
|
|
379
|
+
<UTooltip description="Upload Files" />
|
|
380
|
+
</UBtnStd>
|
|
381
|
+
</div>
|
|
291
382
|
</div>
|
|
292
383
|
</div>
|
|
293
384
|
</template>
|
package/src/utils/data.ts
CHANGED
|
@@ -24,7 +24,7 @@ export const fixStringLength = (str: string, length: number) => {
|
|
|
24
24
|
if (str?.length > length) {
|
|
25
25
|
return str.substring(0, length) + '....'
|
|
26
26
|
} else {
|
|
27
|
-
return str
|
|
27
|
+
return str?.padEnd(length, ' ')
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -55,6 +55,8 @@ export const getFileCategory = (mimeType) => {
|
|
|
55
55
|
'image/bmp': 'image',
|
|
56
56
|
'image/svg+xml': 'image',
|
|
57
57
|
'image/webp': 'image',
|
|
58
|
+
'image/heic': 'image',
|
|
59
|
+
'image/heif': 'image',
|
|
58
60
|
|
|
59
61
|
// Video MIME types
|
|
60
62
|
'video/mp4': 'video',
|