Skip to content

Commit

Permalink
WIP.
Browse files Browse the repository at this point in the history
Chnages to address first four items in this PRs to-do list.
  • Loading branch information
mattburnett-repo committed Feb 19, 2025
1 parent 66f6028 commit 8bb440c
Show file tree
Hide file tree
Showing 12 changed files with 141 additions and 34 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tailwind.json
!.vscode/extensions.json

# Webstorm
Expand Down
20 changes: 18 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
{
"eslint.validate": ["javascript", "typescript", "vue"],
"eslint.validate": [
"javascript",
"typescript",
"vue"
],
"eslint.useFlatConfig": true,
"typescript.tsdk": "./frontend/node_modules/typescript/lib",
}
"cSpell.words": [
"headlessui",
"organizationimage",
"viewsets",
"vuedraggable"
],
// This gets rid of 'Unknown at-rule @apply' messages in VSCode 'Problems' tab. First noticed in MediaImageCarousel.vue.
// Look at tailwind.json for more info.
// https://github.com/tailwindlabs/tailwindcss/discussions/5258#discussioncomment-1979394
"css.customData": [
".vscode/tailwind.json"
],
}
55 changes: 55 additions & 0 deletions .vscode/tailwind.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"version": 1.1,
"atDirectives": [
{
"name": "@tailwind",
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
}
]
},
{
"name": "@apply",
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
}
]
},
{
"name": "@responsive",
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
}
]
},
{
"name": "@screen",
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
}
]
},
{
"name": "@variants",
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
}
]
}
]
}
2 changes: 1 addition & 1 deletion backend/communities/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
GroupViewSet,
)
from communities.organizations.views import (
OrganizationImageViewSet,
OrganizationSocialLinkViewSet,
OrganizationTextViewSet,
OrganizationViewSet,
OrganizationImageViewSet,
)
from communities.views import StatusViewSet

Expand Down
2 changes: 1 addition & 1 deletion backend/content/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def __str__(self) -> str:

class Image(models.Model):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
file_location = models.ImageField(
file_object = models.ImageField(
upload_to="images/",
validators=[validate_image_file_extension]
)
Expand Down
8 changes: 4 additions & 4 deletions backend/content/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,20 @@ class Meta:
class ImageSerializer(serializers.ModelSerializer[Image]):
class Meta:
model = Image
fields = ["id", "file_location", "creation_date"]
fields = ["id", "file_object", "creation_date"]
read_only_fields = ["id", "creation_date"]

def validate(self, data: Dict[str, Union[str, int]]) -> Dict[str, Union[str, int]]:
# Remove string validation since we're getting a file object
if "file_location" not in data:
if "file_object" not in data:
raise serializers.ValidationError("No file was submitted.")
return data

def create(self, validated_data):
# Handle file upload properly
file_obj = self.context["request"].FILES.get("file_location")
file_obj = self.context["request"].FILES.get("file_object")
if file_obj:
validated_data["file_location"] = file_obj
validated_data["file_object"] = file_obj

# Create the image first
image = super().create(validated_data)
Expand Down
57 changes: 41 additions & 16 deletions frontend/components/media/image-carousel/MediaImageCarousel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
<template>
<div class="relative">
<swiper-container
ref="swiperRef"
class="swiper card-style h-full w-full cursor-pointer overflow-clip"
:slidesPerView="1"
:spaceBetween="0"
:loop="true"
:keyboard="true"
:pagination="{ clickable: true }"
ref="swiperRef"
>
<swiper-slide
v-for="[idx, img] of imageUrls.entries()"
:key="idx"
class="flex items-center justify-center bg-layer-2"
class="swiper-zoom-container flex items-center justify-center bg-layer-2"
>
<img
class="object-cover object-center"
Expand All @@ -26,6 +26,12 @@
/>
</swiper-slide>
</swiper-container>
<p
v-if="uploadError"
class="absolute bottom-2 right-12 z-10 rounded bg-white/80 p-1 text-sm text-action-red dark:bg-black/80"
>
{{ $t(i18nMap.components.media_image_carousel.upload_error) }}
</p>
<button
@click="openModal()"
class="focus-brand absolute bottom-2 right-2 z-10 flex rounded-lg border border-black/80 bg-white/80 p-1 text-black/80 dark:border-white/80 dark:bg-black/80 dark:text-white/80"
Expand All @@ -35,6 +41,7 @@
<ModalUploadImages
@closeModal="handleCloseModal"
@upload-complete="fetchOrganizationImages"
@upload-error="uploadError = true"
:isOpen="modalIsOpen"
:organizationId="organizationId"
/>
Expand All @@ -43,7 +50,7 @@

<script setup lang="ts">
import { register } from "swiper/element/bundle";
import { Swiper } from "swiper";
import type { Swiper } from "swiper";
import { i18nMap } from "~/types/i18n-map";
import { IconMap } from "~/types/icon-map";

Expand All @@ -68,25 +75,43 @@ const swiperRef = ref<{ swiper: Swiper | null }>(null);
const modals = useModals();
const modalName = "ModalUploadImages";
const modalIsOpen = ref(false);
const uploadError = ref(false);

// Use this to get rid of 'any' type in imageUrls.value data.map function
interface OrganizationImage {
id: string;
fileObject: string;
creation_date: string;
}

async function fetchOrganizationImages() {
if (props.organizationId) {
const response = await fetch(
`${BASE_BACKEND_URL}/communities/organizations/${props.organizationId}/images/`,
{
headers: {
Authorization: `Token ${localStorage.getItem("accessToken")}`,
},
}
);
try {
const response = await fetch(
`${BASE_BACKEND_URL}/communities/organizations/${props.organizationId}/images/`,
{
headers: {
Authorization: `Token ${localStorage.getItem("accessToken")}`,
},
}
);

if (response.ok) {
const data = await response.json();
if (data.length > 0) {
imageUrls.value = data.map((img: any) => img.fileLocation);
if (response.ok) {
const data = await response.json();
if (data.length > 0) {
imageUrls.value = data.map(
(img: OrganizationImage) => img.fileObject
);
uploadError.value = false;
} else {
imageUrls.value = defaultImageUrls;
}
} else {
imageUrls.value = defaultImageUrls;
uploadError.value = true;
}
} catch (error) {
console.error("Error fetching organization images:", error);
uploadError.value = true;
}
}
}
Expand Down
17 changes: 12 additions & 5 deletions frontend/components/modal/upload-images/ModalUploadImages.vue
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
</template>
</draggable>
<BtnAction
v-if="files.length > 0"
@click="handleUpload"
:cta="true"
:label="$t(i18nMap.components.modal_upload_images.upload)"
Expand Down Expand Up @@ -113,12 +114,18 @@ const props = withDefaults(defineProps<Props>(), {

const modalName = "ModalUploadImages";

const emit = defineEmits(["upload-complete"]);
const emit = defineEmits(["upload-complete", "upload-error"]);

const handleUpload = async () => {
await uploadFiles(props.organizationId);
const modals = useModals();
modals.closeModal(modalName);
emit("upload-complete");
try {
await uploadFiles(props.organizationId);
const modals = useModals();
modals.closeModal(modalName);
emit("upload-complete");
uploadError.value = false;
} catch (error) {
console.error("Error uploading images:", error);
emit("upload-error");
}
};
</script>
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
<template>
<div
class="flex h-32 w-full cursor-pointer items-center justify-center rounded-md border-2 border-dashed border-primary-text bg-layer-0 p-4 text-center text-primary-text"
@dragenter.prevent="isDropZoneActive = true"
@dragleave.prevent="isDropZoneActive = false"
@dragover.prevent="isDropZoneActive = true"
Expand All @@ -10,15 +9,16 @@
$emit('files-dropped', ($event.dataTransfer as DataTransfer)?.files);
"
@click="$refs.file.click()"
class="flex h-32 w-full cursor-pointer items-center justify-center rounded-md border-2 border-dashed border-primary-text bg-layer-0 p-4 text-center text-primary-text"
>
<input
@change="
$emit('files-dropped', ($event.target as HTMLInputElement)?.files)
"
ref="file"
type="file"
class="hidden"
accept="image/jpeg,image/png"
@change="
$emit('files-dropped', ($event.target as HTMLInputElement)?.files)
"
multiple
/>
<slot :isDropZoneActive="isDropZoneActive" />
Expand All @@ -27,4 +27,5 @@

<script setup lang="ts">
const isDropZoneActive = ref(false);
defineEmits(["files-dropped"]);
</script>
2 changes: 1 addition & 1 deletion frontend/composables/useFileManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default function useFileManager(initialFiles: File[] = []) {
async function uploadFiles(organizationId?: string) {
const formData = new FormData();
files.value.forEach((uploadableFile: UploadableFile) => {
formData.append("file_location", uploadableFile.file);
formData.append("file_object", uploadableFile.file);
});

if (organizationId) {
Expand Down
1 change: 1 addition & 0 deletions frontend/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@
"components.landing_tech_banner.open_source_text": "We're dedicated to working in the open to build trust with our partner organizations and fellow activists. All who want to help us build are welcome!",
"components.logo_activist.aria_label": "The activist logo that links to the home page",
"components.media_image_carousel.img_alt_text": "Image of the organization or their work.",
"components.media_image_carousel.upload_error": "Error fetching or uploading organization images.",
"components.media_map.change_profile": "Change profile [p]",
"components.media_map.clear_directions": "Clear directions",
"components.media_map.fullscreen": "Fullscreen",
Expand Down
1 change: 1 addition & 0 deletions frontend/types/i18n-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ export const i18nMap = {
},
media_image_carousel: {
img_alt_text: "components.media_image_carousel.img_alt_text",
upload_error: "components.media_image_carousel.upload_error",
},
media_map: {
change_profile: "components.media_map.change_profile",
Expand Down

0 comments on commit 8bb440c

Please sign in to comment.