From 628db6d4de5578fd6d0a5ad217f4ed8dfaaf5044 Mon Sep 17 00:00:00 2001 From: Justin Glotzbach <20047549+justinglotz@users.noreply.github.com> Date: Thu, 18 Dec 2025 18:52:29 -0600 Subject: [PATCH 1/3] feat: added filters for issues view --- client/src/Components/IssueFilters.jsx | 70 ++++++++++++ client/src/Pages/Issues.jsx | 147 ++++++++++++++++++++----- 2 files changed, 188 insertions(+), 29 deletions(-) create mode 100644 client/src/Components/IssueFilters.jsx diff --git a/client/src/Components/IssueFilters.jsx b/client/src/Components/IssueFilters.jsx new file mode 100644 index 0000000..5f354b8 --- /dev/null +++ b/client/src/Components/IssueFilters.jsx @@ -0,0 +1,70 @@ +export const IssueFilters = ({ filters, onFilterChange }) => { + return ( +
+ {/* Status filter */} + + + {/* Priority filter */} + + + {/* Category filter */} + + + {/* Date filter */} +
+ onFilterChange("startDate", e.target.value)} + className="border rounded px-3 py-2 mr-2" + /> + to + onFilterChange("endDate", e.target.value)} + className="border rounded px-3 py-2" + /> +
+
+ ); +}; diff --git a/client/src/Pages/Issues.jsx b/client/src/Pages/Issues.jsx index 59f0520..3b36be3 100644 --- a/client/src/Pages/Issues.jsx +++ b/client/src/Pages/Issues.jsx @@ -1,7 +1,8 @@ -import { useState, useEffect } from 'react'; -import IssueCard from '../components/IssueCard'; -import IssueForm from '../Components/issueForm'; -import { issueAPI } from '../services/api'; +import { useState, useEffect } from "react"; +import IssueCard from "../components/IssueCard"; +import IssueForm from "../Components/issueForm"; +import { issueAPI } from "../services/api"; +import { IssueFilters } from "../Components/IssueFilters"; const Issues = () => { const [showForm, setShowForm] = useState(false); @@ -9,10 +10,17 @@ const Issues = () => { const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); + const [filters, setFilters] = useState({ + status: "all", + priority: "all", + category: "all", + startDate: "", + endDate: "", + }); // Fetch issues when component mounts useEffect(() => { - fetchIssues(); + fetchIssues(); }, []); const fetchIssues = async () => { @@ -22,8 +30,8 @@ const Issues = () => { const data = await issueAPI.getAllIssues(); setIssues(data); } catch (err) { - console.error('Failed to fetch issues:', err); - setError('Failed to load issues. Please try again later.'); + console.error("Failed to fetch issues:", err); + setError("Failed to load issues. Please try again later."); } finally { setLoading(false); } @@ -33,24 +41,61 @@ const Issues = () => { try { setSubmitting(true); setError(null); - + // Call the API to create the issue const createdIssue = await issueAPI.createIssue(newIssue); - + // Add the new issue to the list setIssues([createdIssue, ...issues]); setShowForm(false); - + // Show success message (you could use a toast notification library) - alert('Issue submitted successfully!'); + alert("Issue submitted successfully!"); } catch (err) { - console.error('Failed to submit issue:', err); - setError('Failed to submit issue. Please try again.'); + console.error("Failed to submit issue:", err); + setError("Failed to submit issue. Please try again."); // Don't close the form so user can retry } finally { setSubmitting(false); } }; + // filterKey, value + const handleFiltersChange = (filterKey, value) => { + setFilters({ + ...filters, + [filterKey]: value, + }); + }; + + // Filter issues based on current filters + const filteredIssues = issues.filter((issue) => { + // Status filter + if (filters.status !== "all" && issue.status !== filters.status) { + return false; + } + // Priority filter + if (filters.priority !== "all" && issue.priority !== filters.priority) { + return false; + } + // Category filter + if (filters.category !== "all" && issue.category !== filters.category) { + return false; + } + // Date range filter (assuming issue has createdAt field) + if ( + filters.startDate && + new Date(issue.createdAt) < new Date(filters.startDate) + ) { + return false; + } + if ( + filters.endDate && + new Date(issue.createdAt) > new Date(filters.endDate) + ) { + return false; + } + return true; + }); return (
@@ -61,20 +106,28 @@ const Issues = () => { disabled={loading} className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:bg-blue-400 disabled:cursor-not-allowed transition-colors" > - {showForm ? 'View Issues' : 'Report New Issue'} + {showForm ? "View Issues" : "Report New Issue"}
{/* Error Message */} {error && (
- - + +

{error}

{!loading && ( -
) : ( -
- {issues.map((issue) => ( - - ))} -
+ <> + +
+ {filteredIssues.map((issue) => ( + + ))} +
+ )}
); From 1d20da01cc7c6ba80812de165988c8198bdeb01f Mon Sep 17 00:00:00 2001 From: Justin Glotzbach <20047549+justinglotz@users.noreply.github.com> Date: Thu, 18 Dec 2025 21:16:39 -0600 Subject: [PATCH 2/3] fix: updated date filter to have the end date be inclusive --- client/src/Components/PhotoUpload.jsx | 59 +++++++++++++++------------ client/src/Pages/Issues.jsx | 5 ++- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/client/src/Components/PhotoUpload.jsx b/client/src/Components/PhotoUpload.jsx index bab8e6c..775e998 100644 --- a/client/src/Components/PhotoUpload.jsx +++ b/client/src/Components/PhotoUpload.jsx @@ -1,5 +1,5 @@ -import { useState } from 'react'; -import { photoAPI } from '../services/api'; +import { useState } from "react"; +import { photoAPI } from "../services/api"; export const PhotoUpload = ({ issueId, onUploadComplete }) => { const [selectedFiles, setSelectedFiles] = useState([]); @@ -13,13 +13,14 @@ export const PhotoUpload = ({ issueId, onUploadComplete }) => { const fileArray = Array.from(files); // Validate files - const validFiles = fileArray.filter(file => { - if (!file.type.startsWith('image/')) { - setUploadError('Only image files are allowed'); + const validFiles = fileArray.filter((file) => { + if (!file.type.startsWith("image/")) { + setUploadError("Only image files are allowed"); return false; } - if (file.size > 5 * 1024 * 1024) { // 5MB limit - setUploadError('Files must be less than 5MB'); + if (file.size > 5 * 1024 * 1024) { + // 5MB limit + setUploadError("Files must be less than 5MB"); return false; } return true; @@ -37,9 +38,9 @@ export const PhotoUpload = ({ issueId, onUploadComplete }) => { try { // Get presigned URLs - const fileMetadata = selectedFiles.map(file => ({ + const fileMetadata = selectedFiles.map((file) => ({ name: file.name, - type: file.type + type: file.type, })); const { presignedUrls } = await photoAPI.getPresignedUrls(fileMetadata); @@ -50,9 +51,9 @@ export const PhotoUpload = ({ issueId, onUploadComplete }) => { const { uploadUrl } = presignedUrls[index]; const uploadResponse = await fetch(uploadUrl, { - method: 'PUT', + method: "PUT", body: file, - headers: { 'Content-Type': file.type } + headers: { "Content-Type": file.type }, }); if (!uploadResponse.ok) { @@ -67,30 +68,32 @@ export const PhotoUpload = ({ issueId, onUploadComplete }) => { fileName: file.name, fileSize: file.size, mimeType: file.type, - caption: '' + caption: "", })); - const saveResponse = await fetch(`${import.meta.env.VITE_API_URL}/photos`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - photos: photoMetadata, - issueId: issueId - }) - }); + const saveResponse = await fetch( + `${import.meta.env.VITE_API_URL}/photos`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + photos: photoMetadata, + issueId: issueId, + }), + } + ); if (!saveResponse.ok) { - throw new Error('Failed to save photo metadata'); + throw new Error("Failed to save photo metadata"); } // Clear selection and refresh setSelectedFiles([]); const savedPhotos = await saveResponse.json(); onUploadComplete(savedPhotos); - } catch (err) { - console.error('Upload failed:', err); - setUploadError(err.message || 'Upload failed. Please try again.'); + console.error("Upload failed:", err); + setUploadError(err.message || "Upload failed. Please try again."); } finally { setUploading(false); } @@ -104,7 +107,7 @@ export const PhotoUpload = ({ issueId, onUploadComplete }) => { multiple accept="image/*" onChange={handleFileSelect} - className="hidden" + style={{ display: "none" }} id="photo-upload" disabled={uploading} /> @@ -147,7 +150,11 @@ export const PhotoUpload = ({ issueId, onUploadComplete }) => { disabled={uploading} className="bg-green-600 text-white px-6 py-2 rounded-md hover:bg-green-700 transition-colors disabled:bg-gray-400" > - {uploading ? 'Uploading...' : `Upload ${selectedFiles.length} Photo${selectedFiles.length > 1 ? 's' : ''}`} + {uploading + ? "Uploading..." + : `Upload ${selectedFiles.length} Photo${ + selectedFiles.length > 1 ? "s" : "" + }`} )} diff --git a/client/src/Pages/Issues.jsx b/client/src/Pages/Issues.jsx index 3b36be3..846c664 100644 --- a/client/src/Pages/Issues.jsx +++ b/client/src/Pages/Issues.jsx @@ -84,13 +84,14 @@ const Issues = () => { // Date range filter (assuming issue has createdAt field) if ( filters.startDate && - new Date(issue.createdAt) < new Date(filters.startDate) + new Date(issue.createdAt) < new Date(filters.startDate + "T00:00:00") ) { return false; } + if ( filters.endDate && - new Date(issue.createdAt) > new Date(filters.endDate) + new Date(issue.createdAt) > new Date(filters.endDate + "T23:59:59") ) { return false; } From 0c9613e71e99ac723ab34e7489c09c786ecd5860 Mon Sep 17 00:00:00 2001 From: Justin Glotzbach <20047549+justinglotz@users.noreply.github.com> Date: Thu, 18 Dec 2025 21:46:12 -0600 Subject: [PATCH 3/3] added workflow to add automatic slack messages when a new PR is opened --- .github/workflows/pr-notification.yml | 57 +++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/pr-notification.yml diff --git a/.github/workflows/pr-notification.yml b/.github/workflows/pr-notification.yml new file mode 100644 index 0000000..9913f8a --- /dev/null +++ b/.github/workflows/pr-notification.yml @@ -0,0 +1,57 @@ +name: Slack PR Notification + +on: + pull_request: + types: [opened] + +jobs: + notify-slack: + runs-on: ubuntu-latest + steps: + - name: Send Slack Notification + uses: slackapi/slack-github-action@v1.26.0 + with: + payload: | + { + "text": "New Pull Request: ${{ github.event.pull_request.title }}", + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "🔔 New Pull Request" + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Title:*\n${{ github.event.pull_request.title }}" + }, + { + "type": "mrkdwn", + "text": "*Author:*\n${{ github.event.pull_request.user.login }}" + }, + { + "type": "mrkdwn", + "text": "*Repository:*\n${{ github.repository }}" + }, + { + "type": "mrkdwn", + "text": "*Branch:*\n${{ github.event.pull_request.head.ref }}" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "<${{ github.event.pull_request.html_url }}|View Pull Request>" + } + } + ] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK