From 5b3fc800c75b81b49f3b29f1ff47e0d797850eca Mon Sep 17 00:00:00 2001 From: Nurfog Date: Wed, 25 Feb 2026 16:17:40 -0300 Subject: [PATCH] a11y: Enhance accessibility of form and interactive components using semantic HTML, ARIA attributes, and keyboard navigation. --- .../lms-service/src/handlers_peer_review.rs | 110 ++++++++---------- web/experience/src/app/globals.css | 4 +- .../src/components/BrandingSettings.tsx | 17 +-- web/studio/src/components/Combobox.tsx | 4 +- web/studio/src/components/FileUpload.tsx | 35 ++++-- .../src/components/OrganizationSelector.tsx | 15 ++- 6 files changed, 104 insertions(+), 81 deletions(-) diff --git a/services/lms-service/src/handlers_peer_review.rs b/services/lms-service/src/handlers_peer_review.rs index 7409053..f2f1a12 100644 --- a/services/lms-service/src/handlers_peer_review.rs +++ b/services/lms-service/src/handlers_peer_review.rs @@ -19,30 +19,28 @@ pub async fn submit_assignment( Json(payload): Json, ) -> Result, (StatusCode, String)> { // Check if submission already exists - let existing: Option = sqlx::query_as!( - CourseSubmission, - "SELECT * FROM course_submissions WHERE user_id = $1 AND lesson_id = $2", - claims.sub, - lesson_id + let existing: Option = sqlx::query_as( + "SELECT * FROM course_submissions WHERE user_id = $1 AND lesson_id = $2" ) + .bind(claims.sub) + .bind(lesson_id) .fetch_optional(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; if let Some(_) = existing { // Update existing submission - let updated = sqlx::query_as!( - CourseSubmission, + let updated: CourseSubmission = sqlx::query_as( r#" UPDATE course_submissions SET content = $1, updated_at = NOW() WHERE user_id = $2 AND lesson_id = $3 RETURNING * - "#, - payload.content, - claims.sub, - lesson_id + "# ) + .bind(&payload.content) + .bind(claims.sub) + .bind(lesson_id) .fetch_one(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -51,19 +49,18 @@ pub async fn submit_assignment( } // Create new submission - let submission = sqlx::query_as!( - CourseSubmission, + let submission: CourseSubmission = sqlx::query_as( r#" INSERT INTO course_submissions (user_id, course_id, lesson_id, organization_id, content) VALUES ($1, $2, $3, $4, $5) RETURNING * - "#, - claims.sub, - course_id, - lesson_id, - org_ctx.id, - payload.content + "# ) + .bind(claims.sub) + .bind(course_id) + .bind(lesson_id) + .bind(org_ctx.id) + .bind(&payload.content) .fetch_one(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -81,8 +78,7 @@ pub async fn get_peer_review_assignment( // 1. Is not my own // 2. Has fewer than 2 reviews (configurable, but hardcoded for now) // 3. I haven't reviewed yet - let submission = sqlx::query_as!( - CourseSubmission, + let submission: Option = sqlx::query_as( r#" SELECT s.* FROM course_submissions s @@ -99,12 +95,12 @@ pub async fn get_peer_review_assignment( HAVING COUNT(pr.id) < 2 ORDER BY s.submitted_at ASC LIMIT 1 - "#, - course_id, - lesson_id, - claims.sub, - org_ctx.id + "# ) + .bind(course_id) + .bind(lesson_id) + .bind(claims.sub) + .bind(org_ctx.id) .fetch_optional(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -120,20 +116,20 @@ pub async fn submit_peer_review( Json(payload): Json, ) -> Result, (StatusCode, String)> { // Verify valid submission - let submission = sqlx::query!( - "SELECT user_id FROM course_submissions WHERE id = $1", - payload.submission_id + let submission_row = sqlx::query( + "SELECT user_id FROM course_submissions WHERE id = $1" ) + .bind(payload.submission_id) .fetch_optional(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - let submission = match submission { - Some(s) => s, + let submission_user_id = match submission_row { + Some(row) => row.get::("user_id"), None => return Err((StatusCode::NOT_FOUND, "Submission not found".to_string())), }; - if submission.user_id == claims.sub { + if submission_user_id == claims.sub { return Err(( StatusCode::BAD_REQUEST, "Cannot review your own submission".to_string(), @@ -141,11 +137,11 @@ pub async fn submit_peer_review( } // Check if already reviewed - let existing = sqlx::query!( - "SELECT id FROM peer_reviews WHERE submission_id = $1 AND reviewer_id = $2", - payload.submission_id, - claims.sub + let existing = sqlx::query( + "SELECT id FROM peer_reviews WHERE submission_id = $1 AND reviewer_id = $2" ) + .bind(payload.submission_id) + .bind(claims.sub) .fetch_optional(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -158,19 +154,18 @@ pub async fn submit_peer_review( } // Create review - let review = sqlx::query_as!( - PeerReview, + let review: PeerReview = sqlx::query_as( r#" INSERT INTO peer_reviews (submission_id, reviewer_id, score, feedback, organization_id) VALUES ($1, $2, $3, $4, $5) RETURNING * - "#, - payload.submission_id, - claims.sub, - payload.score, - payload.feedback, - org_ctx.id + "# ) + .bind(payload.submission_id) + .bind(claims.sub) + .bind(payload.score) + .bind(&payload.feedback) + .bind(org_ctx.id) .fetch_one(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -185,17 +180,16 @@ pub async fn get_my_submission_feedback( Path((_course_id, lesson_id)): Path<(Uuid, Uuid)>, ) -> Result>, (StatusCode, String)> { // Get reviews for my submission on this lesson - let reviews = sqlx::query_as!( - PeerReview, + let reviews: Vec = sqlx::query_as( r#" SELECT pr.* FROM peer_reviews pr JOIN course_submissions cs ON pr.submission_id = cs.id WHERE cs.user_id = $1 AND cs.lesson_id = $2 - "#, - claims.sub, - lesson_id + "# ) + .bind(claims.sub) + .bind(lesson_id) .fetch_all(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -209,12 +203,11 @@ pub async fn list_lesson_submissions( State(pool): State, Path((_course_id, lesson_id)): Path<(Uuid, Uuid)>, ) -> Result>, (StatusCode, String)> { - let submissions = sqlx::query_as!( - SubmissionWithReviews, + let submissions: Vec = sqlx::query_as( r#" SELECT s.id, s.user_id, u.full_name, u.email, s.submitted_at, - COUNT(pr.id) as "review_count!", + COUNT(pr.id) as review_count, AVG(pr.score)::float8 as average_score FROM course_submissions s JOIN users u ON s.user_id = u.id @@ -222,10 +215,10 @@ pub async fn list_lesson_submissions( WHERE s.lesson_id = $1 AND s.organization_id = $2 GROUP BY s.id, u.full_name, u.email ORDER BY s.submitted_at DESC - "#, - lesson_id, - org_ctx.id + "# ) + .bind(lesson_id) + .bind(org_ctx.id) .fetch_all(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; @@ -239,11 +232,10 @@ pub async fn get_submission_reviews( State(pool): State, Path(submission_id): Path, ) -> Result>, (StatusCode, String)> { - let reviews = sqlx::query_as!( - PeerReview, - "SELECT * FROM peer_reviews WHERE submission_id = $1", - submission_id + let reviews: Vec = sqlx::query_as( + "SELECT * FROM peer_reviews WHERE submission_id = $1" ) + .bind(submission_id) .fetch_all(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; diff --git a/web/experience/src/app/globals.css b/web/experience/src/app/globals.css index eeb4216..100336c 100644 --- a/web/experience/src/app/globals.css +++ b/web/experience/src/app/globals.css @@ -13,7 +13,8 @@ --accent-primary: var(--primary-color); --accent-secondary: var(--secondary-color); - --glass-bg: rgba(255, 255, 255, 0.05); /* Increased slightly for contrast */ + --glass-bg: rgba(255, 255, 255, 0.05); + /* Increased slightly for contrast */ --glass-border: rgba(255, 255, 255, 0.1); --glass-blur: blur(16px); } @@ -48,6 +49,7 @@ body { .gradient-text { background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); + background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; } diff --git a/web/studio/src/components/BrandingSettings.tsx b/web/studio/src/components/BrandingSettings.tsx index c09cb6e..a266de7 100644 --- a/web/studio/src/components/BrandingSettings.tsx +++ b/web/studio/src/components/BrandingSettings.tsx @@ -58,16 +58,17 @@ export default function BrandingSettings() { return (
-
-

- 🎨 Brand Identity -

+
+ + Brand Identity +
{/* Platform Name */}
- + setFormData({ ...formData, platform_name: e.target.value })} @@ -94,6 +95,7 @@ export default function BrandingSettings() { )}
{ @@ -127,6 +129,7 @@ export default function BrandingSettings() { )}
{ @@ -141,7 +144,7 @@ export default function BrandingSettings() {

Recommended: ICO or PNG, 32x32px.

- +
@@ -211,6 +214,6 @@ export default function BrandingSettings() { {saving ? "Saving Changes..." : "Save Branding Settings"} - + ); } diff --git a/web/studio/src/components/Combobox.tsx b/web/studio/src/components/Combobox.tsx index dd09cb1..512f754 100644 --- a/web/studio/src/components/Combobox.tsx +++ b/web/studio/src/components/Combobox.tsx @@ -13,9 +13,10 @@ interface ComboboxProps { value: string; onChange: (value: string) => void; placeholder?: string; + id?: string; } -export default function Combobox({ options, value, onChange, placeholder = "Search..." }: ComboboxProps) { +export default function Combobox({ options, value, onChange, placeholder = "Search...", id }: ComboboxProps) { const [isOpen, setIsOpen] = useState(false); const [search, setSearch] = useState(""); const containerRef = useRef(null); @@ -39,6 +40,7 @@ export default function Combobox({ options, value, onChange, placeholder = "Sear return (
+ +

+ Leave empty to use the Default Organization. +