Skip to content

Commit 651c242

Browse files
AllanKoderCopilotCopilot
authored
Everything tag, frontend (#55)
* Everything tag, frontend * Update resources/js/Pages/Resources/Form/TagsFields.vue Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update resources/js/Components/Form/TagSelector.vue Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Make TagSelector tooltip mode-aware for comma-separated tags (#57) * Initial plan * Make tooltip mode-aware to show comma-separated tags help Co-authored-by: AllanKoder <74692833+AllanKoder@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: AllanKoder <74692833+AllanKoder@users.noreply.github.com> * Add orange theme styling for count badge on "everything" tag (#56) * Initial plan * Add orange-themed styling for count badge on 'everything' tag Co-authored-by: AllanKoder <74692833+AllanKoder@users.noreply.github.com> * Add explicit handling for 'selected' variant in count badge styling Co-authored-by: AllanKoder <74692833+AllanKoder@users.noreply.github.com> * Consolidate duplicate styling logic for highlighted and selected variants Co-authored-by: AllanKoder <74692833+AllanKoder@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: AllanKoder <74692833+AllanKoder@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
1 parent 4443df8 commit 651c242

9 files changed

Lines changed: 368 additions & 189 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ Xdebug is pre-configured in the Sail Docker environment for local debugging.
184184

185185
## Deployment
186186

187+
We use Ansible with a `deploy.yaml` script, along with the inventory `production.ini`
188+
187189
`ansible-playbook -i ./ansible/inventory/production.ini deploy.yml`
188190

189191
## License

resources/js/Components/Form/TagSelector.vue

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from "vue"
55
import { defineModel } from "vue";
66
import axios from "axios";
77
import { Icon } from "@iconify/vue";
8+
import Tag from "@/Components/Tag.vue";
89
910
const DEBOUNCE_TIME = 450; // milliseconds
1011
@@ -13,6 +14,15 @@ const props = defineProps({
1314
type: String,
1415
required: true,
1516
},
17+
mode: {
18+
type: String,
19+
default: 'create', // 'create' or 'search'
20+
validator: (value) => ['create', 'search'].includes(value),
21+
},
22+
allowEverything: {
23+
type: Boolean,
24+
default: true,
25+
},
1626
});
1727
1828
const model = defineModel();
@@ -90,6 +100,9 @@ const availableTags = computed(() => {
90100
});
91101
92102
const canCreateNew = computed(() => {
103+
// In search mode, cannot create new tags
104+
if (props.mode === 'search') return false;
105+
93106
const query = searchQuery.value.trim();
94107
if (!query || isLoading.value) return false;
95108
@@ -102,6 +115,20 @@ const canCreateNew = computed(() => {
102115
);
103116
});
104117
118+
const tooltipText = computed(() => {
119+
const parts = [];
120+
121+
if (props.mode === 'create') {
122+
parts.push("Add multiple tags using commas");
123+
}
124+
125+
if (props.allowEverything) {
126+
parts.push("'everything' tag covers all possible tags");
127+
}
128+
129+
return parts.join('. ');
130+
});
131+
105132
// sync model
106133
watch(
107134
() => model.value,
@@ -176,7 +203,8 @@ function onKeydown(event) {
176203
canCreateNew.value
177204
) {
178205
selectTag(searchQuery.value.trim());
179-
} else if (searchQuery.value.trim()) {
206+
} else if (props.mode === 'create' && searchQuery.value.trim()) {
207+
// Only allow adding tags in create mode
180208
// Handle comma-separated tags
181209
if (searchQuery.value.includes(',')) {
182210
addMultipleTags(searchQuery.value);
@@ -258,20 +286,15 @@ onMounted(async () => {
258286
<div class="mb-2">
259287
<!-- list of selected tags -->
260288
<div class="mb-2 flex flex-wrap" v-if="selectedTags.length > 0">
261-
<span
289+
<Tag
262290
v-for="tag in selectedTags"
263291
:key="tag"
264-
class="inline-flex items-center mr-2 my-1 bg-secondary text-primaryDark dark:bg-gray-700 dark:text-white px-3 py-1 rounded-full text-sm font-medium transition-colors"
265-
>
266-
<button
267-
@click="removeTag(tag)"
268-
class="mr-2 text-primaryDark dark:text-white"
269-
type="button"
270-
>
271-
<Icon :icon="'mdi:close'" />
272-
</button>
273-
<span>{{ tag }}</span>
274-
</span>
292+
:tag="tag"
293+
variant="selected"
294+
removable
295+
@remove="removeTag"
296+
class="mr-2 my-1"
297+
/>
275298
</div>
276299
277300
<!-- search input container -->
@@ -289,13 +312,14 @@ onMounted(async () => {
289312
:class="{
290313
'rounded-b-none border-b-0':
291314
showDropdown &&
292-
(availableTags.length > 0 || canCreateNew || isLoading),
315+
(availableTags.length > 0 || canCreateNew || isLoading || searchQuery.trim()),
293316
}"
294317
/>
295318
<Icon
319+
v-if="tooltipText"
296320
icon="mdi:information-outline"
297321
class="absolute right-3 size-5 text-gray-400 dark:text-gray-500 cursor-help"
298-
v-tooltip.top="'You can add multiple tags at once by separating them with commas (e.g., tag1, tag2, tag3)'"
322+
v-tooltip.top="tooltipText"
299323
/>
300324
</div>
301325
@@ -304,7 +328,7 @@ onMounted(async () => {
304328
<div
305329
v-show="
306330
showDropdown &&
307-
(availableTags.length > 0 || canCreateNew || isLoading)
331+
(availableTags.length > 0 || canCreateNew || isLoading || searchQuery.trim())
308332
"
309333
class="z-[9999] bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-800 border-t-0 rounded-b-lg shadow-lg max-h-60 overflow-y-auto"
310334
:style="dropdownStyles"
@@ -325,6 +349,20 @@ onMounted(async () => {
325349
326350
<!-- available tags -->
327351
<template v-else>
352+
<!-- No results message -->
353+
<div
354+
v-if="availableTags.length === 0 && !canCreateNew"
355+
class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center"
356+
>
357+
<Icon icon="mdi:information-outline" class="inline-block w-4 h-4 mr-1" />
358+
<span v-if="searchQuery.trim()">
359+
No tags found matching "{{ searchQuery.trim() }}"
360+
</span>
361+
<span v-else-if="props.mode === 'search'">
362+
No tags available
363+
</span>
364+
</div>
365+
328366
<div
329367
v-for="(tag, index) in availableTags"
330368
:key="tag.name"
@@ -334,18 +372,15 @@ onMounted(async () => {
334372
:class="{
335373
'bg-secondary text-primaryDark dark:bg-gray-800 dark:text-primaryLight': highlightedIndex === index,
336374
'hover:bg-gray-50 dark:hover:bg-gray-900': highlightedIndex !== index,
375+
'bg-orange-50 dark:bg-orange-900/20 border-l-2 border-orange-400': tag.name === 'everything' && highlightedIndex !== index,
376+
'bg-orange-100 dark:bg-orange-900/30 border-l-2 border-orange-500': tag.name === 'everything' && highlightedIndex === index,
337377
}"
338378
>
339-
<span class="text-sm">{{ tag.name }}</span>
340-
<span
341-
v-if="tag.count"
342-
class="text-xs bg-gray-100 text-gray-600 dark:bg-gray-900 dark:text-gray-300 px-2 py-1 rounded-full"
343-
:class="{
344-
'bg-secondary text-primaryDark dark:bg-gray-800 dark:text-primaryLight': highlightedIndex === index,
345-
}"
346-
>
347-
{{ tag.count }}
348-
</span>
379+
<Tag
380+
:tag="tag.name"
381+
:variant="highlightedIndex === index ? 'highlighted' : 'default'"
382+
:count="tag.count || null"
383+
/>
349384
</div>
350385
351386
<!-- create new tag option -->

resources/js/Components/Resources/FilterBar.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ function resetFilters() {
321321
</label>
322322
<TagSelector
323323
:tag-type="'topics_tags'"
324+
:mode="'search'"
324325
v-model="selectedTopics"
325326
class="w-full"
326327
/>
@@ -334,6 +335,7 @@ function resetFilters() {
334335
</label>
335336
<TagSelector
336337
:tag-type="'programming_languages_tags'"
338+
:mode="'search'"
337339
v-model="selectedProgrammingLanguages"
338340
class="w-full"
339341
/>
@@ -347,6 +349,7 @@ function resetFilters() {
347349
</label>
348350
<TagSelector
349351
:tag-type="'general_tags'"
352+
:mode="'search'"
350353
v-model="selectedGeneralTags"
351354
class="w-full"
352355
/>

resources/js/Components/Resources/ResourceCard.vue

Lines changed: 20 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { platformIcons, pricingIcons, difficultyIcons } from "@/Helpers/icons";
1212
import ResourceThumbnail from "@/Components/Resources/ResourceThumbnail.vue";
1313
import StarRating from "@/Components/StarRating/StarRating.vue";
14+
import Tag from "@/Components/Tag.vue";
1415
1516
const props = defineProps({
1617
resource: {
@@ -90,55 +91,38 @@ const emit = defineEmits(["upvote", "downvote"]);
9091
<div class="flex items-center gap-3 flex-wrap">
9192
<!-- Top topics -->
9293
<div class="flex items-center gap-1">
93-
<span
94+
<Tag
9495
v-for="topic in resource.topics_tags?.slice(
9596
0,
9697
4
9798
)"
9899
:key="topic"
99-
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-100 bg-transparent" >
100-
<Icon
101-
icon="mdi:lightbulb-outline"
102-
width="12"
103-
height="12"
104-
class="mr-1"
105-
/>
106-
{{ topic }}
107-
</span>
100+
:tag="topic"
101+
icon="mdi:lightbulb-outline"
102+
:icon-size="12"
103+
/>
108104
</div>
109105

110106
<!-- Difficulty -->
111107
<div
112108
v-if="resource.difficulties?.length"
113109
class="flex items-center gap-1"
114110
>
115-
<span
111+
<Tag
116112
v-for="difficulty in resource.difficulties"
117113
:key="difficulty"
118-
class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-transparent text-primaryDark border border-primary/20"
119-
>
120-
<Icon
121-
:icon="difficultyIcons[difficulty]"
122-
width="12"
123-
height="12"
124-
class="mr-1"
125-
/>
126-
{{ difficultyLabels[difficulty] }}
127-
</span>
114+
:tag="difficultyLabels[difficulty]"
115+
:icon="difficultyIcons[difficulty]"
116+
:icon-size="12"
117+
/>
128118
</div>
129119

130120
<!-- Pricing -->
131-
<span
132-
class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-transparent text-primaryDark border border-primary/20"
133-
>
134-
<Icon
135-
:icon="pricingIcons[resource.pricing]"
136-
width="12"
137-
height="12"
138-
class="mr-1"
139-
/>
140-
{{ pricingLabels[resource.pricing] }}
141-
</span>
121+
<Tag
122+
:tag="pricingLabels[resource.pricing]"
123+
:icon="pricingIcons[resource.pricing]"
124+
:icon-size="12"
125+
/>
142126
</div>
143127
</div>
144128

@@ -162,19 +146,12 @@ const emit = defineEmits(["upvote", "downvote"]);
162146

163147
<!-- Secondary info row -->
164148
<div class="flex items-center gap-1">
165-
<span
149+
<Tag
166150
v-for="platform in resource.platforms.slice(0, 2)"
167151
:key="platform"
168-
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-100 bg-transparent"
169-
>
170-
<Icon
171-
:icon="platformIcons[platform]"
172-
width="10"
173-
height="10"
174-
class="mr-1"
175-
/>
176-
{{ platformLabels[platform] }}
177-
</span>
152+
:tag="platformLabels[platform]"
153+
:icon="platformIcons[platform]"
154+
/>
178155
<!-- Rest as comma-separated text -->
179156
<span
180157
v-if="

0 commit comments

Comments
 (0)