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
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/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 59f0520..846c664 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,62 @@ 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 + "T00:00:00")
+ ) {
+ return false;
+ }
+
+ if (
+ filters.endDate &&
+ new Date(issue.createdAt) > new Date(filters.endDate + "T23:59:59")
+ ) {
+ return false;
+ }
+ return true;
+ });
return (
@@ -61,20 +107,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 && (
-