diff --git a/apps/api/src/routers/orgs.py b/apps/api/src/routers/orgs.py index e3cdc735..737bf2ce 100644 --- a/apps/api/src/routers/orgs.py +++ b/apps/api/src/routers/orgs.py @@ -41,6 +41,7 @@ from src.services.orgs.orgs import ( update_org_signup_mechanism, update_org_thumbnail, update_org_landing, + upload_org_landing_content_service, ) @@ -428,3 +429,23 @@ async def api_update_org_landing( Update organization landing object """ return await update_org_landing(request, landing_object, org_id, current_user, db_session) + + +@router.post("/{org_id}/landing/content") +async def api_upload_org_landing_content( + request: Request, + org_id: int, + content_file: UploadFile, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): + """ + Upload content for organization landing page + """ + return await upload_org_landing_content_service( + request=request, + content_file=content_file, + org_id=org_id, + current_user=current_user, + db_session=db_session, + ) diff --git a/apps/api/src/services/orgs/orgs.py b/apps/api/src/services/orgs/orgs.py index c66e80b3..9f580b28 100644 --- a/apps/api/src/services/orgs/orgs.py +++ b/apps/api/src/services/orgs/orgs.py @@ -36,7 +36,7 @@ from src.db.organizations import ( ) from fastapi import HTTPException, UploadFile, status, Request -from src.services.orgs.uploads import upload_org_logo, upload_org_preview, upload_org_thumbnail +from src.services.orgs.uploads import upload_org_logo, upload_org_preview, upload_org_thumbnail, upload_org_landing_content async def get_organization( @@ -765,6 +765,35 @@ async def update_org_landing( return {"detail": "Landing object updated"} +async def upload_org_landing_content_service( + request: Request, + content_file: UploadFile, + org_id: int, + current_user: PublicUser | AnonymousUser, + db_session: Session, +) -> dict: + statement = select(Organization).where(Organization.id == org_id) + result = db_session.exec(statement) + + org = result.first() + + if not org: + raise HTTPException( + status_code=404, + detail="Organization not found", + ) + + # RBAC check + await rbac_check(request, org.org_uuid, current_user, "update", db_session) + + # Upload content + name_in_disk = await upload_org_landing_content(content_file, org.org_uuid) + + return { + "detail": "Landing content uploaded successfully", + "filename": name_in_disk + } + ## 🔒 RBAC Utils ## diff --git a/apps/api/src/services/orgs/uploads.py b/apps/api/src/services/orgs/uploads.py index 7c393d53..83296fd7 100644 --- a/apps/api/src/services/orgs/uploads.py +++ b/apps/api/src/services/orgs/uploads.py @@ -1,4 +1,6 @@ from uuid import uuid4 +from fastapi import UploadFile +from fastapi import HTTPException from src.services.utils.upload_content import upload_content @@ -45,4 +47,23 @@ async def upload_org_preview(file, org_uuid: str) -> str: name_in_disk, ) + return name_in_disk + + +async def upload_org_landing_content(file: UploadFile, org_uuid: str) -> str: + if not file or not file.filename: + raise HTTPException(status_code=400, detail="No file provided or invalid filename") + + contents = file.file.read() + name_in_disk = f"{uuid4()}.{file.filename.split('.')[-1]}" + + await upload_content( + "landing", + "orgs", + org_uuid, + contents, + name_in_disk, + ["jpg", "jpeg", "png", "gif", "webp", "mp4", "webm", "pdf"] # Common web content formats + ) + return name_in_disk \ No newline at end of file diff --git a/apps/web/components/Dashboard/Pages/Org/OrgEditLanding/OrgEditLanding.tsx b/apps/web/components/Dashboard/Pages/Org/OrgEditLanding/OrgEditLanding.tsx index b218fa49..24bb806c 100644 --- a/apps/web/components/Dashboard/Pages/Org/OrgEditLanding/OrgEditLanding.tsx +++ b/apps/web/components/Dashboard/Pages/Org/OrgEditLanding/OrgEditLanding.tsx @@ -10,7 +10,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Button } from "@components/ui/button" import { useOrg } from '@components/Contexts/OrgContext' import { useLHSession } from '@components/Contexts/LHSessionContext' -import { updateOrgLanding } from '@services/organizations/orgs' +import { updateOrgLanding, uploadLandingContent } from '@services/organizations/orgs' +import { getOrgLandingMediaDirectory } from '@services/media/media' import { getOrgCourses } from '@services/courses/courses' import toast from 'react-hot-toast' import useSWR from 'swr' @@ -273,7 +274,7 @@ const OrgEditLanding = () => { {/* Enable/Disable Landing Page */}
-

Landing Page

+

Landing Page
BETA

Customize your organization's landing page

@@ -954,6 +955,64 @@ const HeroSectionEditor: React.FC<{ ) } +interface ImageUploaderProps { + onImageUploaded: (imageUrl: string) => void + className?: string + buttonText?: string + id: string +} + +const ImageUploader: React.FC = ({ onImageUploaded, className, buttonText = "Upload Image", id }) => { + const org = useOrg() as any + const session = useLHSession() as any + const access_token = session?.data?.tokens?.access_token + const [isUploading, setIsUploading] = React.useState(false) + const inputId = `imageUpload-${id}` + + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + + setIsUploading(true) + try { + const response = await uploadLandingContent(org.id, file, access_token) + if (response.status === 200) { + const imageUrl = getOrgLandingMediaDirectory(org.org_uuid, response.data.filename) + onImageUploaded(imageUrl) + toast.success('Image uploaded successfully') + } else { + toast.error('Failed to upload image') + } + } catch (error) { + console.error('Error uploading image:', error) + toast.error('Failed to upload image') + } finally { + setIsUploading(false) + } + } + + return ( +
+ + +
+ ) +} + const TextAndImageSectionEditor: React.FC<{ section: LandingTextAndImageSection onChange: (section: LandingTextAndImageSection) => void @@ -1010,7 +1069,7 @@ const TextAndImageSectionEditor: React.FC<{
-
+
onChange({ @@ -1019,6 +1078,14 @@ const TextAndImageSectionEditor: React.FC<{ })} placeholder="Image URL" /> + onChange({ + ...section, + image: { ...section.image, url } + })} + buttonText="Upload New Image" + />
+ {section.image.url && ( +
+ {section.image.alt} +
+ )}
@@ -1064,24 +1140,44 @@ const LogosSectionEditor: React.FC<{ {section.logos.map((logo, index) => (
- { - const newLogos = [...section.logos] - newLogos[index] = { ...logo, url: e.target.value } - onChange({ ...section, logos: newLogos }) - }} - placeholder="Logo URL" - /> - { - const newLogos = [...section.logos] - newLogos[index] = { ...logo, alt: e.target.value } - onChange({ ...section, logos: newLogos }) - }} - placeholder="Alt text" - /> +
+ { + const newLogos = [...section.logos] + newLogos[index] = { ...logo, url: e.target.value } + onChange({ ...section, logos: newLogos }) + }} + placeholder="Logo URL" + /> + { + const newLogos = [...section.logos] + newLogos[index] = { ...section.logos[index], url } + onChange({ ...section, logos: newLogos }) + }} + buttonText="Upload Logo" + /> +
+
+ { + const newLogos = [...section.logos] + newLogos[index] = { ...logo, alt: e.target.value } + onChange({ ...section, logos: newLogos }) + }} + placeholder="Alt text" + /> + {logo.url && ( + {logo.alt} + )} +