diff --git a/README.md b/README.md
index d7276a48..a7da683e 100644
--- a/README.md
+++ b/README.md
@@ -184,6 +184,8 @@ Xdebug is pre-configured in the Sail Docker environment for local debugging.
## Deployment
+We use Ansible with a `deploy.yaml` script, along with the inventory `production.ini`
+
`ansible-playbook -i ./ansible/inventory/production.ini deploy.yml`
## License
diff --git a/resources/js/Components/Form/TagSelector.vue b/resources/js/Components/Form/TagSelector.vue
index 01fc95f3..339a6e90 100644
--- a/resources/js/Components/Form/TagSelector.vue
+++ b/resources/js/Components/Form/TagSelector.vue
@@ -5,6 +5,7 @@ import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from "vue"
import { defineModel } from "vue";
import axios from "axios";
import { Icon } from "@iconify/vue";
+import Tag from "@/Components/Tag.vue";
const DEBOUNCE_TIME = 450; // milliseconds
@@ -13,6 +14,15 @@ const props = defineProps({
type: String,
required: true,
},
+ mode: {
+ type: String,
+ default: 'create', // 'create' or 'search'
+ validator: (value) => ['create', 'search'].includes(value),
+ },
+ allowEverything: {
+ type: Boolean,
+ default: true,
+ },
});
const model = defineModel();
@@ -90,6 +100,9 @@ const availableTags = computed(() => {
});
const canCreateNew = computed(() => {
+ // In search mode, cannot create new tags
+ if (props.mode === 'search') return false;
+
const query = searchQuery.value.trim();
if (!query || isLoading.value) return false;
@@ -102,6 +115,20 @@ const canCreateNew = computed(() => {
);
});
+const tooltipText = computed(() => {
+ const parts = [];
+
+ if (props.mode === 'create') {
+ parts.push("Add multiple tags using commas");
+ }
+
+ if (props.allowEverything) {
+ parts.push("'everything' tag covers all possible tags");
+ }
+
+ return parts.join('. ');
+});
+
// sync model
watch(
() => model.value,
@@ -176,7 +203,8 @@ function onKeydown(event) {
canCreateNew.value
) {
selectTag(searchQuery.value.trim());
- } else if (searchQuery.value.trim()) {
+ } else if (props.mode === 'create' && searchQuery.value.trim()) {
+ // Only allow adding tags in create mode
// Handle comma-separated tags
if (searchQuery.value.includes(',')) {
addMultipleTags(searchQuery.value);
@@ -258,20 +286,15 @@ onMounted(async () => {
-
-
- {{ tag }}
-
+ :tag="tag"
+ variant="selected"
+ removable
+ @remove="removeTag"
+ class="mr-2 my-1"
+ />
@@ -289,13 +312,14 @@ onMounted(async () => {
:class="{
'rounded-b-none border-b-0':
showDropdown &&
- (availableTags.length > 0 || canCreateNew || isLoading),
+ (availableTags.length > 0 || canCreateNew || isLoading || searchQuery.trim()),
}"
/>
@@ -304,7 +328,7 @@ onMounted(async () => {
{
+
+
+
+
+ No tags found matching "{{ searchQuery.trim() }}"
+
+
+ No tags available
+
+
+
{
:class="{
'bg-secondary text-primaryDark dark:bg-gray-800 dark:text-primaryLight': highlightedIndex === index,
'hover:bg-gray-50 dark:hover:bg-gray-900': highlightedIndex !== index,
+ 'bg-orange-50 dark:bg-orange-900/20 border-l-2 border-orange-400': tag.name === 'everything' && highlightedIndex !== index,
+ 'bg-orange-100 dark:bg-orange-900/30 border-l-2 border-orange-500': tag.name === 'everything' && highlightedIndex === index,
}"
>
- {{ tag.name }}
-
- {{ tag.count }}
-
+
diff --git a/resources/js/Components/Resources/FilterBar.vue b/resources/js/Components/Resources/FilterBar.vue
index 7ce987d6..64d819a3 100644
--- a/resources/js/Components/Resources/FilterBar.vue
+++ b/resources/js/Components/Resources/FilterBar.vue
@@ -321,6 +321,7 @@ function resetFilters() {
@@ -334,6 +335,7 @@ function resetFilters() {
@@ -347,6 +349,7 @@ function resetFilters() {
diff --git a/resources/js/Components/Resources/ResourceCard.vue b/resources/js/Components/Resources/ResourceCard.vue
index b5fdc02c..93fd9e91 100644
--- a/resources/js/Components/Resources/ResourceCard.vue
+++ b/resources/js/Components/Resources/ResourceCard.vue
@@ -11,6 +11,7 @@ import {
import { platformIcons, pricingIcons, difficultyIcons } from "@/Helpers/icons";
import ResourceThumbnail from "@/Components/Resources/ResourceThumbnail.vue";
import StarRating from "@/Components/StarRating/StarRating.vue";
+import Tag from "@/Components/Tag.vue";
const props = defineProps({
resource: {
@@ -90,21 +91,16 @@ const emit = defineEmits(["upvote", "downvote"]);
-
-
- {{ topic }}
-
+ :tag="topic"
+ icon="mdi:lightbulb-outline"
+ :icon-size="12"
+ />
@@ -112,33 +108,21 @@ const emit = defineEmits(["upvote", "downvote"]);
v-if="resource.difficulties?.length"
class="flex items-center gap-1"
>
-
-
- {{ difficultyLabels[difficulty] }}
-
+ :tag="difficultyLabels[difficulty]"
+ :icon="difficultyIcons[difficulty]"
+ :icon-size="12"
+ />
-
-
- {{ pricingLabels[resource.pricing] }}
-
+
@@ -162,19 +146,12 @@ const emit = defineEmits(["upvote", "downvote"]);
-
-
- {{ platformLabels[platform] }}
-
+ :tag="platformLabels[platform]"
+ :icon="platformIcons[platform]"
+ />
-
-
- Topics:
-
- {{ topic }}
-
-
+
Difficulty:
-
-
- {{ difficultyLabels[difficulty] }}
-
+ :tag="difficultyLabels[difficulty]"
+ :icon="difficultyIcons[difficulty]"
+ />
@@ -149,17 +130,10 @@ const props = defineProps({
class="text-xs font-semibold text-gray-600 dark:text-gray-100"
>Pricing:
-
-
- {{ pricingLabels[props.resource.pricing] }}
-
+
@@ -179,71 +153,32 @@ const props = defineProps({
class="text-xs font-semibold text-gray-600 dark:text-gray-100"
>Platforms:
-
-
- {{ platformLabels[platform] }}
-
+ :tag="platformLabels[platform]"
+ :icon="platformIcons[platform]"
+ />
-
-
- Languages:
-
- {{ language }}
-
-
+ :tags="props.resource.programming_languages_tags"
+ label="Languages:"
+ label-icon="mdi:code-tags"
+ />
-
-
- Tags:
-
- {{ tag }}
-
-
+ :tags="props.resource.general_tags"
+ label="Tags:"
+ label-icon="mdi:label-outline"
+ />
diff --git a/resources/js/Components/Tag.vue b/resources/js/Components/Tag.vue
new file mode 100644
index 00000000..b4401af3
--- /dev/null
+++ b/resources/js/Components/Tag.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+ {{ tag }}
+
+ {{ count }}
+
+
+
diff --git a/resources/js/Components/TagList.vue b/resources/js/Components/TagList.vue
new file mode 100644
index 00000000..97843932
--- /dev/null
+++ b/resources/js/Components/TagList.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+ {{ label }}
+
+
+
+
diff --git a/resources/js/Pages/ResourceEdits/Create.vue b/resources/js/Pages/ResourceEdits/Create.vue
index 3d57d4e9..d8a9ffdb 100644
--- a/resources/js/Pages/ResourceEdits/Create.vue
+++ b/resources/js/Pages/ResourceEdits/Create.vue
@@ -455,6 +455,7 @@ const submit = async () => {
{
{
:initialValues="props.form"
class="flex flex-col gap-4 w-full bg-white dark:bg-gray-900 border border-transparent dark:border-gray-800 rounded-lg p-4"
>
-
+
-
+
What's this resource about?
*
- software-engineering, career-consulting, data-science
+ software-engineering, career-consulting, data-science, everything
-
+
Programming Languages/Frameworks taught (if any)?
-
python, c++, c#, vue.js, pytorch
+
+ python, c++, c#, vue.js, pytorch, everything
+
-
+
Additional general tags
-
non-profit, open-source, funny
+
+ non-profit, open-source, funny
+
-
- How to choose good tags
-
+ How to choose good tags
-
Topic tags (required)
+
+ Topic tags (required)
+
- Use broad, descriptive categories that capture what the resource is mainly about.
- Avoid super niche labels here.
+ Use broad, descriptive categories that capture what
+ the resource is mainly about. Avoid super niche
+ labels here.
-
- - Examples: software-engineering, data-science, devops, algorithms, system-design, career
+
+ -
+ Examples: software-engineering, data-science,
+ devops, algorithms, system-design, career
+
+ -
+ If it covers too many topics to list (e.g.
+ codecademy), add 'everything'
+
-
Programming languages/frameworks
+
+ Programming languages/frameworks
+
- Be specific and only include languages or frameworks the resource actively teaches or uses.
- Please don’t type "everything" as a tag.
+ Be specific and only include languages or frameworks
+ the resource actively teaches or uses. If it covers
+ too many languages and frameworks to list (e.g.
+ codecademy), add 'everything'
-
+
- Good: python, c++, c#, vue.js, pytorch
- - Avoid: "all-languages" or adding many unrelated ones
+ -
+ In case there are too many: 'everything'
+
Other/general tags
- Everything else that helps discovery: tone, format, credentials, audience, etc.
+ Everything else that helps discovery: tone, format,
+ credentials, audience, etc.
-
- - Examples: humour, certifications, non-profit, open-source, interview-prep
+
+ -
+ Examples: humour, certifications, non-profit,
+ open-source, interview-prep
+
-
+