···
1
1
<script>
2
2
-
import { userStore } from '$lib/stores/user.svelte.js';
3
3
-
import { getAllLanguages, filterLanguages } from '$lib/utils/languages.js';
4
4
-
import { flip } from 'svelte/animate';
2
2
+
import { userStore } from '$lib/stores/user.svelte.js';
3
3
+
import { getAllLanguages, filterLanguages } from '$lib/utils/languages.js';
4
4
+
import { flip } from 'svelte/animate';
5
5
6
6
-
let filter = $state('');
7
7
-
let { data } = $props();
8
8
-
let { languages = [], country_languages = [], ip_country } = data;
6
6
+
let filter = $state('');
7
7
+
let { data } = $props();
8
8
+
let { languages = [], country_languages = [], ip_country } = data;
9
9
10
10
-
let allLanguages = $state(languages);
10
10
+
let allLanguages = $state(languages);
11
11
12
12
-
// userLanguages: allLanguages that are in userStore.user.selectedLanguages (no filter applied)
13
13
-
let userLanguages = $derived.by(() =>
14
14
-
allLanguages.filter(lang => userStore.user.selectedLanguages.includes(lang.code))
15
15
-
);
16
16
-
let otherLanguages = $derived.by(() =>
17
17
-
allLanguages.filter(lang => !userStore.user.selectedLanguages.includes(lang.code))
18
18
-
);
12
12
+
// userLanguages: allLanguages that are in userStore.user.selectedLanguages (no filter applied)
13
13
+
let userLanguages = $derived.by(() =>
14
14
+
allLanguages.filter((lang) => (userStore.user?.selectedLanguages ?? []).includes(lang.code))
15
15
+
);
16
16
+
let otherLanguages = $derived.by(() =>
17
17
+
allLanguages.filter((lang) => !(userStore.user?.selectedLanguages ?? []).includes(lang.code))
18
18
+
);
19
19
20
20
-
function formatSpeakers(n) {
21
21
-
if (!n) return '';
22
22
-
return (n / 1000).toFixed(0) + 'M';
23
23
-
}
20
20
+
function formatSpeakers(n) {
21
21
+
if (!n) return '';
22
22
+
return (n / 1000).toFixed(0) + 'M';
23
23
+
}
24
24
</script>
25
25
26
26
<div class="filter-bar">
27
27
-
<input
28
28
-
class="language-filter-input"
29
29
-
type="text"
30
30
-
placeholder="Filter languages..."
31
31
-
bind:value={filter}
32
32
-
autocomplete="off"
33
33
-
/>
27
27
+
<input
28
28
+
class="language-filter-input"
29
29
+
type="text"
30
30
+
placeholder="Filter languages..."
31
31
+
bind:value={filter}
32
32
+
autocomplete="off"
33
33
+
/>
34
34
</div>
35
35
36
36
<ul class="languages-list">
37
37
-
{#each userLanguages as lang (lang.code)}
38
38
-
<li class="language-item selected"
39
39
-
animate:flip={{ duration: 222 }}
40
40
-
onclick={() => userStore.removeLanguage(lang.code)}
41
41
-
tabindex="0"
42
42
-
aria-label={`Remove ${lang.name}`}>
43
43
-
<span class="lang-action">–</span>
44
44
-
<span class="lang-speakers">{formatSpeakers(lang.speakers)}</span>
45
45
-
<span class="lang-label">{lang.name}</span>
46
46
-
{#if lang.nativeName && lang.nativeName !== lang.name}
47
47
-
<span class="lang-native">{lang.nativeName}</span>
48
48
-
{/if}
49
49
-
</li>
50
50
-
{/each}
37
37
+
{#each userLanguages as lang (lang.code)}
38
38
+
<li
39
39
+
class="language-item selected"
40
40
+
animate:flip={{ duration: 222 }}
41
41
+
onclick={() => userStore.removeLanguage(lang.code)}
42
42
+
tabindex="0"
43
43
+
aria-label={`Remove ${lang.name}`}
44
44
+
>
45
45
+
<span class="lang-action">–</span>
46
46
+
<span class="lang-speakers">{formatSpeakers(lang.speakers)}</span>
47
47
+
<span class="lang-label">{lang.name}</span>
48
48
+
{#if lang.nativeName && lang.nativeName !== lang.name}
49
49
+
<span class="lang-native">{lang.nativeName}</span>
50
50
+
{/if}
51
51
+
</li>
52
52
+
{/each}
51
53
52
52
-
{#if userLanguages.length && filterLanguages(otherLanguages, filter).length}
53
53
-
<li class="language-separator" style="margin: 0.5em 0; border-bottom: 1px solid #eee; list-style: none;"></li>
54
54
-
{/if}
54
54
+
{#if userLanguages.length && filterLanguages(otherLanguages, filter).length}
55
55
+
<li
56
56
+
class="language-separator"
57
57
+
style="margin: 0.5em 0; border-bottom: 1px solid #eee; list-style: none;"
58
58
+
></li>
59
59
+
{/if}
55
60
56
56
-
{#each filterLanguages(otherLanguages, filter) as lang (lang.code)}
57
57
-
<li class="language-item"
58
58
-
animate:flip={{ duration: 222 }}
59
59
-
onclick={() => userStore.addLanguage(lang.code)}
60
60
-
tabindex="0"
61
61
-
aria-label={`Add ${lang.name}`}>
62
62
-
<span class="lang-action">+</span>
63
63
-
<span class="lang-speakers">{formatSpeakers(lang.speakers)}</span>
64
64
-
<span class="lang-label">{lang.name}</span>
65
65
-
{#if lang.nativeName && lang.nativeName !== lang.name}
66
66
-
<span class="lang-native">{lang.nativeName}</span>
67
67
-
{/if}
68
68
-
</li>
69
69
-
{/each}
61
61
+
{#each filterLanguages(otherLanguages, filter) as lang (lang.code)}
62
62
+
<li
63
63
+
class="language-item"
64
64
+
animate:flip={{ duration: 222 }}
65
65
+
onclick={() => userStore.addLanguage(lang.code)}
66
66
+
tabindex="0"
67
67
+
aria-label={`Add ${lang.name}`}
68
68
+
>
69
69
+
<span class="lang-action">+</span>
70
70
+
<span class="lang-speakers">{formatSpeakers(lang.speakers)}</span>
71
71
+
<span class="lang-label">{lang.name}</span>
72
72
+
{#if lang.nativeName && lang.nativeName !== lang.name}
73
73
+
<span class="lang-native">{lang.nativeName}</span>
74
74
+
{/if}
75
75
+
</li>
76
76
+
{/each}
70
77
</ul>
71
78
72
79
<style>
73
73
-
.languages-list {
74
74
-
max-width: 480px;
75
75
-
margin: 0 auto;
76
76
-
padding: 0;
77
77
-
list-style: none;
78
78
-
}
79
79
-
.language-item {
80
80
-
display: flex;
81
81
-
align-items: center;
82
82
-
gap: 0.5em;
83
83
-
padding: 0.15em 0.5em;
84
84
-
border-radius: 5px;
85
85
-
margin-bottom: 0.1em;
86
86
-
font-size: 1em;
87
87
-
min-height: 32px;
88
88
-
transition: background 0.12s, box-shadow 0.12s;
89
89
-
will-change: transform, opacity;
90
90
-
cursor: pointer;
91
91
-
outline: none;
92
92
-
}
93
93
-
.language-item:hover, .language-item:focus {
94
94
-
background: #f6f8fa;
95
95
-
box-shadow: 0 0 0 2px #6366f1;
96
96
-
}
80
80
+
.languages-list {
81
81
+
max-width: 480px;
82
82
+
margin: 0 auto;
83
83
+
padding: 0;
84
84
+
list-style: none;
85
85
+
}
86
86
+
.language-item {
87
87
+
display: flex;
88
88
+
align-items: center;
89
89
+
gap: 0.5em;
90
90
+
padding: 0.15em 0.5em;
91
91
+
border-radius: 5px;
92
92
+
margin-bottom: 0.1em;
93
93
+
font-size: 1em;
94
94
+
min-height: 32px;
95
95
+
transition:
96
96
+
background 0.12s,
97
97
+
box-shadow 0.12s;
98
98
+
will-change: transform, opacity;
99
99
+
cursor: pointer;
100
100
+
outline: none;
101
101
+
}
102
102
+
.language-item:hover,
103
103
+
.language-item:focus {
104
104
+
background: #f6f8fa;
105
105
+
box-shadow: 0 0 0 2px #6366f1;
106
106
+
}
97
107
98
98
-
.lang-label {
99
99
-
font-weight: 600;
100
100
-
margin-left: 0.3em;
101
101
-
}
102
102
-
.lang-native {
103
103
-
color: #888;
104
104
-
margin-left: 0.3em;
105
105
-
font-size: 0.97em;
106
106
-
}
107
107
-
.lang-speakers {
108
108
-
color: #aaa;
109
109
-
font-size: 0.97em;
110
110
-
min-width: 2.5em;
111
111
-
text-align: right;
112
112
-
margin-left: 0.2em;
113
113
-
margin-right: 0.2em;
114
114
-
}
115
115
-
.lang-label {
116
116
-
font-weight: 600;
117
117
-
font-size: 1.05em;
118
118
-
display: block;
119
119
-
}
120
120
-
.lang-native {
121
121
-
display: block;
122
122
-
font-style: italic;
123
123
-
color: #888;
124
124
-
font-size: 0.97em;
125
125
-
margin-top: -0.15em;
126
126
-
margin-bottom: 0.1em;
127
127
-
margin-left: 0.2em;
128
128
-
}
108
108
+
.lang-label {
109
109
+
font-weight: 600;
110
110
+
margin-left: 0.3em;
111
111
+
}
112
112
+
.lang-native {
113
113
+
color: #888;
114
114
+
margin-left: 0.3em;
115
115
+
font-size: 0.97em;
116
116
+
}
117
117
+
.lang-speakers {
118
118
+
color: #aaa;
119
119
+
font-size: 0.97em;
120
120
+
min-width: 2.5em;
121
121
+
text-align: right;
122
122
+
margin-left: 0.2em;
123
123
+
margin-right: 0.2em;
124
124
+
}
125
125
+
.lang-label {
126
126
+
font-weight: 600;
127
127
+
font-size: 1.05em;
128
128
+
display: block;
129
129
+
}
130
130
+
.lang-native {
131
131
+
display: block;
132
132
+
font-style: italic;
133
133
+
color: #888;
134
134
+
font-size: 0.97em;
135
135
+
margin-top: -0.15em;
136
136
+
margin-bottom: 0.1em;
137
137
+
margin-left: 0.2em;
138
138
+
}
129
139
130
130
-
.filter-bar {
131
131
-
display: flex;
132
132
-
align-items: center;
133
133
-
max-width: 420px;
134
134
-
margin: .7rem auto .5rem auto;
135
135
-
padding: 0 0.5rem;
136
136
-
border-bottom: 2px solid #e5e7eb;
137
137
-
background: none;
138
138
-
}
139
139
-
.language-filter-input {
140
140
-
flex: 1;
141
141
-
border: none;
142
142
-
outline: none;
143
143
-
background: none;
144
144
-
font-size: 1.15em;
145
145
-
padding: 0em 0 0em 0;
146
146
-
color: #222;
147
147
-
transition: box-shadow 0.15s, border-color 0.15s;
148
148
-
}
149
149
-
.language-filter-input:focus {
150
150
-
box-shadow: 0 2px 0 0 #6366f1;
151
151
-
border-bottom: 2px solid #6366f1;
152
152
-
}
153
153
-
</style>
140
140
+
.filter-bar {
141
141
+
display: flex;
142
142
+
align-items: center;
143
143
+
max-width: 420px;
144
144
+
margin: 0.7rem auto 0.5rem auto;
145
145
+
padding: 0 0.5rem;
146
146
+
border-bottom: 2px solid #e5e7eb;
147
147
+
background: none;
148
148
+
}
149
149
+
.language-filter-input {
150
150
+
flex: 1;
151
151
+
border: none;
152
152
+
outline: none;
153
153
+
background: none;
154
154
+
font-size: 1.15em;
155
155
+
padding: 0em 0 0em 0;
156
156
+
color: #222;
157
157
+
transition:
158
158
+
box-shadow 0.15s,
159
159
+
border-color 0.15s;
160
160
+
}
161
161
+
.language-filter-input:focus {
162
162
+
box-shadow: 0 2px 0 0 #6366f1;
163
163
+
border-bottom: 2px solid #6366f1;
164
164
+
}
165
165
+
</style>