Skip to content

Commit 7886d58

Browse files
committed
Generalise dropdowns, add concerts to views
1 parent 5fa3c73 commit 7886d58

7 files changed

+300
-153
lines changed

src/components/Details.svelte

+71-26
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,66 @@
55
concertViews,
66
currentViewName,
77
selectedConcertIndices,
8+
defaultViewName,
89
} from "src/lib/stores";
10+
import Dropdown from "src/components/Dropdown.svelte";
911
10-
let allConcerts: Concert[] = $concertViews.get($currentViewName) as Concert[];
12+
let allConcerts: Concert[] = $concertViews.get(
13+
$currentViewName,
14+
) as Concert[];
1115
let selectedConcerts: Concert[] = allConcerts.filter((_, idx) =>
1216
$selectedConcertIndices.includes(idx),
1317
);
1418
19+
function compareConcertTime(a: Concert, b: Concert): number {
20+
let aDate = new Date(a.datetime);
21+
let bDate = new Date(b.datetime);
22+
return aDate.getTime() - bDate.getTime();
23+
}
24+
25+
// TODO Reduce duplication with ViewList.svelte
26+
function addToView(concerts: Concert[], viewName: string) {
27+
const existingConcerts = $concertViews.get(viewName) as Concert[];
28+
// Remove dupes and sort by date
29+
const mergedConcerts = [...existingConcerts, ...concerts]
30+
.filter((c, idx, arr) => arr.indexOf(c) === idx)
31+
.sort(compareConcertTime);
32+
$concertViews.set(viewName, mergedConcerts);
33+
$concertViews = new Map($concertViews); // trigger store update
34+
}
35+
36+
function addToNewView(concerts: Concert[]) {
37+
const newViewName = getNewViewName();
38+
if (newViewName === null) {
39+
return;
40+
}
41+
$concertViews.set(newViewName, concerts);
42+
$concertViews = new Map($concertViews); // Required to trigger store update
43+
$currentViewName = newViewName;
44+
}
45+
46+
function getNewViewName(): string | null {
47+
const newViewName = prompt("Enter a name for the new view");
48+
if (newViewName === null) {
49+
return null;
50+
}
51+
if (newViewName === "") {
52+
alert("Please enter a name");
53+
return null;
54+
}
55+
if ($concertViews.has(newViewName)) {
56+
alert("A view with that name already exists");
57+
return null;
58+
}
59+
return newViewName;
60+
}
61+
1562
$: {
1663
allConcerts = $concertViews.get($currentViewName) as Concert[];
1764
selectedConcerts = allConcerts.filter((_, idx) =>
1865
$selectedConcertIndices.includes(idx),
1966
);
2067
}
21-
22-
function exportSelection() {
23-
alert("TODO: Export selected concerts to file");
24-
}
25-
26-
function makeNewView() {
27-
alert("TODO: Create new view with selected concerts");
28-
}
2968
</script>
3069

3170
<div class="details">
@@ -52,18 +91,29 @@
5291
{/each}
5392
</div>
5493

55-
<p>
56-
Don&rsquo;t click these buttons, they don&rsquo;t do anything
57-
yet. You&rsquo;ll just get an annoying popup.
58-
</p>
59-
60-
<button on:click={exportSelection}>
61-
TODO: Export selected concerts to file
62-
</button>
63-
64-
<button on:click={makeNewView}>
65-
TODO: Create new view with selected concerts
66-
</button>
94+
<Dropdown>
95+
<span slot="text">Add to view</span>
96+
<svelte:fragment slot="options">
97+
{#each $concertViews
98+
.keys()
99+
.filter((k) => k !== defaultViewName) as view}
100+
<button
101+
on:click={() => {
102+
addToView(selectedConcerts, view);
103+
}}
104+
>
105+
{view}
106+
</button>
107+
{/each}
108+
<button
109+
on:click={() => {
110+
addToNewView(selectedConcerts);
111+
}}
112+
>
113+
New empty view...
114+
</button>
115+
</svelte:fragment>
116+
</Dropdown>
67117
</div>
68118
{/if}
69119
</div>
@@ -102,9 +152,4 @@
102152
.italic {
103153
font-style: italic;
104154
}
105-
106-
button {
107-
margin-top: 5px;
108-
font-family: inherit;
109-
}
110155
</style>

src/components/Dropdown.svelte

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<script lang="ts">
2+
export let selected: boolean = false;
3+
export let hasOptions: boolean = true;
4+
export let alignment: "left" | "right" = "left";
5+
6+
import { createEventDispatcher } from "svelte";
7+
const dispatch = createEventDispatcher();
8+
function mainButtonClick() {
9+
dispatch("mainButtonClick", {});
10+
}
11+
</script>
12+
13+
<div class="dropdown-trigger">
14+
<button class="dropdown-button" class:selected on:click={mainButtonClick}>
15+
<slot name="text" />
16+
{#if hasOptions}
17+
<span class="smol">▼</span>
18+
<div
19+
class="dropdown-options"
20+
class:left={alignment === "left"}
21+
class:right={alignment === "right"}
22+
>
23+
<slot name="options" />
24+
</div>
25+
{/if}
26+
</button>
27+
</div>
28+
29+
<style>
30+
button.dropdown-button {
31+
position: relative;
32+
background-color: #f0f0f0;
33+
border: 1px solid #ccc;
34+
border-radius: 5px;
35+
padding: 5px;
36+
}
37+
38+
button.dropdown-button.selected {
39+
background-color: #c1eaf5;
40+
border-color: #32aecf;
41+
box-shadow: 0 0 1px #32aecf;
42+
}
43+
44+
div.dropdown-options {
45+
display: none;
46+
}
47+
48+
button.dropdown-button:hover > div.dropdown-options {
49+
display: flex;
50+
flex-direction: column;
51+
gap: 0px;
52+
position: absolute;
53+
top: 26.5px;
54+
width: max-content;
55+
z-index: 1;
56+
align-items: stretch;
57+
}
58+
59+
button.dropdown-button:hover > div.dropdown-options.left {
60+
left: -1px;
61+
}
62+
63+
button.dropdown-button:hover > div.dropdown-options.right {
64+
right: 1px;
65+
}
66+
67+
div.dropdown-options :global(button) {
68+
background-color: #f0f0f0;
69+
border: 1px solid #ccc;
70+
border-radius: 5px;
71+
padding: 5px;
72+
margin: 0;
73+
text-align: left;
74+
}
75+
76+
div.dropdown-options.left :global(button) {
77+
text-align: left;
78+
}
79+
80+
div.dropdown-options.right :global(button) {
81+
text-align: right;
82+
}
83+
84+
div.dropdown-options :global(button:hover) {
85+
background-color: #ddd;
86+
}
87+
88+
span.smol {
89+
font-size: 0.7em;
90+
margin-left: 2px;
91+
}
92+
</style>

src/components/SelectedConcertDetails.svelte

+103-20
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,116 @@
22
import Tags from "src/components/Tags.svelte";
33
import { formatDate, getPriceString } from "src/lib/utils";
44
import { type Concert } from "src/lib/bindings/Concert";
5+
import { concertViews, currentViewName } from "src/lib/stores";
6+
import Dropdown from "src/components/Dropdown.svelte";
57
68
export let selectedConcert: Concert;
9+
10+
function getValidViews(concert: Concert): string[] {
11+
let validViews = [];
12+
for (const [view, concerts] of $concertViews.entries()) {
13+
if (concerts.find((c) => c.id === concert.id) === undefined) {
14+
validViews.push(view);
15+
}
16+
}
17+
return validViews;
18+
}
19+
20+
function addToView(concert: Concert, viewName: string) {
21+
const existingConcerts = $concertViews.get(viewName) as Concert[];
22+
$concertViews.set(viewName, [...existingConcerts, concert]);
23+
$concertViews = new Map($concertViews); // trigger store update
24+
}
25+
26+
// TODO Reduce duplication with ViewList.svelte
27+
function addToNewView(concert: Concert) {
28+
const newViewName = getNewViewName();
29+
if (newViewName === null) {
30+
return;
31+
}
32+
$concertViews.set(newViewName, [concert]);
33+
$concertViews = new Map($concertViews); // Required to trigger store update
34+
$currentViewName = newViewName;
35+
}
36+
37+
function getNewViewName(): string | null {
38+
const newViewName = prompt("Enter a name for the new view");
39+
if (newViewName === null) {
40+
return null;
41+
}
42+
if (newViewName === "") {
43+
alert("Please enter a name");
44+
return null;
45+
}
46+
if ($concertViews.has(newViewName)) {
47+
alert("A view with that name already exists");
48+
return null;
49+
}
50+
return newViewName;
51+
}
752
</script>
853

954
<div id="selected">
10-
<Tags concert={selectedConcert} />
1155
<h2>
1256
{selectedConcert.title}
1357
{#if selectedConcert.subtitle}
1458
— {selectedConcert.subtitle}
1559
{/if}
1660
</h2>
17-
<p>
61+
<div class="left-and-right">
62+
<Tags concert={selectedConcert} />
63+
<Dropdown alignment="right">
64+
<span slot="text">Add to view</span>
65+
<svelte:fragment slot="options">
66+
{#each getValidViews(selectedConcert) as view}
67+
<button
68+
on:click={() => {
69+
addToView(selectedConcert, view);
70+
}}
71+
>
72+
{view}
73+
</button>
74+
{/each}
75+
<button
76+
on:click={() => {
77+
addToNewView(selectedConcert);
78+
}}
79+
>
80+
New empty view...
81+
</button>
82+
</svelte:fragment>
83+
</Dropdown>
84+
</div>
85+
<div>
1886
{formatDate(new Date(selectedConcert.datetime))}
1987
|
2088
{getPriceString(selectedConcert)}
2189
<br />
22-
<a href={selectedConcert.url}>Link to concert</a>
90+
<a href={selectedConcert.url} target="_blank">Link to concert</a>
2391
{#if selectedConcert.programme_pdf_url}
24-
| <a href={selectedConcert.programme_pdf_url}
92+
| <a href={selectedConcert.programme_pdf_url} target="_blank"
2593
>Link to programme (PDF)</a
2694
>
2795
{/if}
28-
</p>
96+
</div>
2997

30-
<h3>Performer(s)</h3>
31-
{#if selectedConcert.performers.length === 0}
32-
None listed.
33-
{:else}
34-
<div class="two-col-grid">
35-
{#each selectedConcert.performers as performer}
36-
<span>{performer.name}</span>
37-
<span>{performer.instrument ? performer.instrument : ""}</span>
38-
{/each}
39-
</div>
40-
{/if}
98+
<div>
99+
<h3>Performer(s)</h3>
100+
{#if selectedConcert.performers.length === 0}
101+
None listed.
102+
{:else}
103+
<div class="two-col-grid">
104+
{#each selectedConcert.performers as performer}
105+
<span>{performer.name}</span>
106+
<span
107+
>{performer.instrument
108+
? performer.instrument
109+
: ""}</span
110+
>
111+
{/each}
112+
</div>
113+
{/if}
114+
</div>
41115

42116
<h3>Programme</h3>
43117
{#if selectedConcert.pieces.length === 0}
@@ -63,18 +137,27 @@
63137
</div>
64138

65139
<style>
66-
#selected > *:first-child {
67-
margin-top: 0;
140+
#selected {
141+
display: flex;
142+
flex-direction: column;
143+
gap: 10px;
68144
}
69145
70-
#selected > *:last-child {
71-
margin-bottom: 0;
146+
h2 {
147+
margin: 0;
72148
}
73149
74150
h3 {
151+
margin-top: 0;
75152
margin-bottom: 5px;
76153
}
77154
155+
div.left-and-right {
156+
display: grid;
157+
grid-template-columns: 1fr max-content;
158+
align-items: baseline;
159+
}
160+
78161
div.two-col-grid {
79162
display: grid;
80163
grid-template-columns: max-content 1fr;

src/components/Tags.svelte

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
div#tags {
1919
display: flex;
2020
justify-content: flex-start;
21+
flex-wrap: wrap;
2122
gap: 5px;
2223
margin-bottom: 5px;
2324
}

0 commit comments

Comments
 (0)