diff --git a/.github/workflows/CI_CD.yml b/.github/workflows/CI_CD.yml index c291416..9617324 100644 --- a/.github/workflows/CI_CD.yml +++ b/.github/workflows/CI_CD.yml @@ -38,6 +38,8 @@ jobs: integration-tests: name: Run Integration Tests + env: + AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }} runs-on: ubuntu-latest needs: unit-tests @@ -49,26 +51,4 @@ jobs: uses: ./.github/actions/setup-node - name: Run Jest Integration Tests - run: npm test -- --ci --testPathPattern ".*\\.integration\\.test\\.tsx$" - - cypress-tests: - name: Run Cypress Tests - runs-on: ubuntu-latest - needs: integration-tests - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js Environment - uses: ./.github/actions/setup-node - - - name: Start application - run: | - npm start & - npx wait-on http://localhost:3000 - - - name: Run Cypress tests - uses: cypress-io/github-action@v5 - with: - browser: chrome + run: npm test -- --ci --testPathPattern ".*\\.integration\\.test\\.tsx$" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9e52269..d4d0dda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,8 +19,9 @@ "web-vitals": "^2.1.4" }, "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@types/http-proxy-middleware": "^0.19.3", - "@types/jest": "^29.5.13", + "@types/jest": "^29.5.14", "@types/testing-library__jest-dom": "^5.14.8", "@types/webpack-dev-server": "^4.7.1", "cross-fetch": "^4.0.0", @@ -720,10 +721,18 @@ } }, "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", + "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, "engines": { "node": ">=6.9.0" }, @@ -2004,6 +2013,18 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/preset-modules": { "version": "0.1.6-no-external-plugins", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", @@ -4217,9 +4238,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.13", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz", - "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==", + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", "license": "MIT", "dependencies": { "expect": "^29.0.0", diff --git a/package.json b/package.json index 12f8fc5..327df20 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,9 @@ ] }, "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@types/http-proxy-middleware": "^0.19.3", - "@types/jest": "^29.5.13", + "@types/jest": "^29.5.14", "@types/testing-library__jest-dom": "^5.14.8", "@types/webpack-dev-server": "^4.7.1", "cross-fetch": "^4.0.0", diff --git a/src/App.tsx b/src/App.tsx index 14722de..e1b0dbc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,8 +7,12 @@ import Profile from './pages/Profile'; import Export from './pages/Export'; import ExperimentCreation from './pages/ExperimentCreation'; import Experiment from './pages/Experiment'; +import QuestionnaireDashboard from './pages/QuestionnaireDashboard'; +import QuestionaireCreation from './pages/QuestionnaireCreation'; +import Questionnaire from './pages/Questionnaire'; import MessageCreation from './pages/MessageCreation'; import MessageDashboard from './pages/MessageDashboard'; +import Message from './pages/Message'; import Unauthorized from './pages/Unauthorized'; import AuthWrapper from './components/AuthWrapper'; @@ -38,8 +42,12 @@ const App: React.FC = () => { } /> } /> } /> + } /> + } /> + } /> } /> } /> + } /> {/* Wildcard Route */} } /> diff --git a/src/components/DynamicView.tsx b/src/components/DynamicView.tsx new file mode 100644 index 0000000..aace618 --- /dev/null +++ b/src/components/DynamicView.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import '../styles/DynamicView.css'; + +interface DynamicViewProps { + fields: { + name: string; + value: string; + }[]; +} + +const DynamicView: React.FC = ({ fields }) => { + return ( +
+ {fields.map((field, index) => ( +
+
+
{field.name}
+
{field.value}
+
+
+
+ ))} +
+ ); +}; + +export default DynamicView; \ No newline at end of file diff --git a/src/pages/Experiment.tsx b/src/pages/Experiment.tsx index deae60b..1a3906e 100644 --- a/src/pages/Experiment.tsx +++ b/src/pages/Experiment.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import Navbar from '../components/Navbar'; +import DynamicView from '../components/DynamicView'; import '../styles/Experiment.css'; import { Experiment as ExperimentType } from '../types/experiment'; -import { updateExperiment } from '../services/experimentService'; // Import the update function +import { updateExperiment } from '../services/experimentService'; const Experiment: React.FC = () => { const location = useLocation(); @@ -20,7 +21,20 @@ const Experiment: React.FC = () => { ); } - + const fields = [ + { name: 'Experiment Title', value: experiment.name }, + { name: 'Description', value: experiment.description }, + { + name: 'Duration', + value: `${new Date(experiment.start).toLocaleDateString()} - ${new Date(experiment.end).toLocaleDateString()}`, + }, + { name: 'Specified LivingLab', value: experiment.livingLab?.name || 'N/A' }, + { name: 'Number of Questionnaires', value: experiment.numberOfQuestionnaires?.toString() || '0' }, + { name: 'Responses on Questionnaires', value: experiment.questionnaireActivity?.toString() || '0' }, + { name: 'Number of Messages', value: experiment.numberOfMessages?.toString() || '0' }, + { name: 'Messages Sent', value: experiment.messageActivity?.toString() || '0' }, + ]; + // Navigation handlers const handleQuestionnaireOverviewClick = () => { navigate(`/questionnairedashboard/${experiment.ID}`); @@ -32,7 +46,6 @@ const Experiment: React.FC = () => { navigate(`/messagedashboard/${experiment.ID}`); } catch (error) { console.error('Error fetching messages:', error); - // Optionally display an error message to the user } }; @@ -69,95 +82,48 @@ const Experiment: React.FC = () => { {/* Experiment Details Component */}
- {/* Experiment Title Component */} -
- Experiment title -
-

{experiment.name}

-
-
- - {/* Description Component */} -
- Description -
-

{experiment.description}

-
-
- - {/* Duration Set Component */} -
- Duration of experiment -
-
- {new Date(experiment.start).toLocaleDateString()} -
- - -
- {new Date(experiment.end).toLocaleDateString()} -
-
-
- - {/* Location Set Component */} -
- Specified LivingLab -
-

- {experiment.livingLab?.name || 'N/A'} -

-
-
- - {/* Stop Experiment Button */} - +
- {/* Buttons */} + {/* Buttons Container */}
{/* Questionnaire Overview Button */} - {/* Message Overview Button */} + {/* View Messages Button */} + + {/* Stop Experiment Button */} +
); -}; +} export default Experiment; diff --git a/src/pages/ExperimentCreation.tsx b/src/pages/ExperimentCreation.tsx index b073072..4484f51 100644 --- a/src/pages/ExperimentCreation.tsx +++ b/src/pages/ExperimentCreation.tsx @@ -13,9 +13,8 @@ const ExperimentCreation: React.FC = () => { const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [selectedLivingLabID, setSelectedLivingLabID] = useState(''); - const [selectedLivingLabName, setSelectedLivingLabName] = useState('All'); + const [selectedLivingLabName, setSelectedLivingLabName] = useState('None'); const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [dateError, setDateError] = useState(''); // State variable for living labs const [livingLabs, setLivingLabs] = useState([]); const [loadingLabs, setLoadingLabs] = useState(true); @@ -223,12 +222,12 @@ const ExperimentCreation: React.FC = () => {
{ - setSelectedLivingLabID(''); // Empty string represents 'All' - setSelectedLivingLabName('All'); + setSelectedLivingLabID(''); // Empty string represents 'None' + setSelectedLivingLabName('None'); setIsDropdownOpen(false); }} > - All + None
{/* LivingLab Options */} {livingLabs.map((lab) => ( diff --git a/src/pages/Message.tsx b/src/pages/Message.tsx new file mode 100644 index 0000000..6b238ad --- /dev/null +++ b/src/pages/Message.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { useLocation } from 'react-router-dom'; +import Navbar from '../components/Navbar'; +import DynamicView from '../components/DynamicView'; +import '../styles/Message.css'; +import { Message as MessageType } from '../types/message'; + +const Message: React.FC = () => { + const location = useLocation(); + const message = location.state?.message as MessageType | null; + + if (!message) { + return ( + <> + +
+

Message not found.

+
+ + ); + } + + // Define severity labels + const severityLabels: { [key: number]: string } = { + 1: 'debug', + 2: 'info', + 3: 'warning', + 4: 'urgent', + 5: 'critical', + }; + + // Prepare fields to display + const fields = [ + { name: 'Message Name', value: message.name }, + { name: 'Text', value: message.text }, + { name: 'Trigger', value: message.trigger }, + { + name: 'Severity', + value: severityLabels[message.severity] || 'Unknown', + }, + { + name: 'Encounter Meters', + value: + message.encounterMeters !== undefined + ? message.encounterMeters.toString() + : 'N/A', + }, + { + name: 'Encounter Minutes', + value: + message.encounterMinutes !== undefined + ? message.encounterMinutes.toString() + : 'N/A', + }, + { + name: 'Species', + value: + message.species !== undefined + ? `${message.species.commonName} (${message.species.name})` + : 'N/A', + }, + { + name: 'Activity', + value: message.activity.toString(), + }, + { + name: 'Experiment', + value: message.experiment.name || 'N/A', + }, + ]; + + return ( + <> + +
+ {/* Title */} +

Message View

+ + {/* Message Details Component */} +
+ +
+
+ + ); +}; + +export default Message; diff --git a/src/pages/MessageCreation.tsx b/src/pages/MessageCreation.tsx index e4bf013..187fcd9 100644 --- a/src/pages/MessageCreation.tsx +++ b/src/pages/MessageCreation.tsx @@ -1,14 +1,11 @@ -import React, { useState, useRef} from 'react'; +import React, { useState, useEffect, useRef} from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import Navbar from '../components/Navbar'; import '../styles/MessageCreation.css'; import { addMessage } from '../services/messageService'; - -interface Species { - ID: string; - name: string; - commonName: string; -} +import { getAllSpecies } from '../services/speciesService'; +import { getAllTriggerTypes } from '../services/triggerTypeService'; +import { Species } from '../types/species'; const MessageCreation: React.FC = () => { const { id } = useParams<{ id: string }>(); @@ -19,10 +16,12 @@ const MessageCreation: React.FC = () => { const [messageText, setMessageText] = useState(''); const [severity, setSeverity] = useState(1); - const [selectedTriggerID, setSelectedTriggerID] = useState('encounter'); - const [selectedTriggerName, setSelectedTriggerName] = useState('Encounter'); - const [isTriggerDropdownOpen, setIsTriggerDropdownOpen] = useState(false); + const [triggerTypes, setTriggerTypes] = useState([]); + const [selectedTriggerID, setSelectedTriggerID] = useState('encounter'); // Default trigger ID + const [selectedTriggerName, setSelectedTriggerName] = useState('Encounter'); // Default trigger name + const [isTriggerDropdownOpen, setIsTriggerDropdownOpen] = useState(false); + const [speciesList, setSpeciesList] = useState([]); const [selectedSpeciesID, setSelectedSpeciesID] = useState(''); const [selectedSpeciesName, setSelectedSpeciesName] = useState('Select Species'); const [isSpeciesDropdownOpen, setIsSpeciesDropdownOpen] = useState(false); @@ -36,35 +35,37 @@ const MessageCreation: React.FC = () => { const encounterMinutesRef = useRef(null); const speciesDropdownRef = useRef(null); - // Triggers list - const triggers = [ - { id: 'encounter', name: 'Encounter' }, - { id: 'alarm', name: 'Alarm' }, - ]; + const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); + + // Fetch trigger types when component mounts + useEffect(() => { + const fetchTriggerTypes = async () => { + try { + const triggers = await getAllTriggerTypes(); + const filteredTriggers = triggers.filter((trigger) => trigger.toLowerCase() !== 'answer'); //Temporary Solution + setTriggerTypes(filteredTriggers); + } catch (error) { + console.error('Error fetching trigger types:', error); + // Optionally, handle the error (e.g., show a notification) + } + }; + fetchTriggerTypes(); + }, []); + // Species list - const speciesList: Species[] = [ - { - ID: '2e6e75fb-4888-4c8d-81c6-ab31c63a7ecb', - name: 'Bison bonasus', - commonName: 'Wisent', - }, - { - ID: '79952c1b-3f43-4d6e-9ff0-b6057fda6fc1', - name: 'Bos taurus', - commonName: 'Schotse hooglander', - }, - { - ID: 'cf83db9d-dab7-4542-bc00-08c87d1da68d', - name: 'Canis lupus', - commonName: 'Wolf', - }, - { - ID: '28775ecb-1af6-4b22-a87a-e15b1999d55c', - name: 'Sus scrofa', - commonName: 'Wild Zwijn', - }, - ]; + // Fetch species list when component mounts + useEffect(() => { + const fetchSpecies = async () => { + try { + const species = await getAllSpecies(); + setSpeciesList(species); + } catch (error) { + console.error('Error fetching species:', error); + } + }; + fetchSpecies(); + }, []); const validateForm = (): boolean => { let isValid = true; @@ -166,7 +167,6 @@ const MessageCreation: React.FC = () => {
{/* Title */}

New Message

- {/* Content Box */}
{ {isTriggerDropdownOpen && (
- {triggers.map((trigger) => ( + {triggerTypes.map((trigger) => (
{ - setSelectedTriggerID(trigger.id); - setSelectedTriggerName(trigger.name); + setSelectedTriggerID(trigger); + setSelectedTriggerName(capitalize(trigger)); setIsTriggerDropdownOpen(false); // Reset conditional fields setEncounterMeters(''); setEncounterMinutes(''); }} > - {trigger.name} + {capitalize(trigger)}
))}
@@ -298,7 +298,7 @@ const MessageCreation: React.FC = () => { className="message-creation-dropdown-item" onClick={() => { setSelectedSpeciesID(species.ID); - setSelectedSpeciesName(species.commonName); + setSelectedSpeciesName(`${species.commonName} (${species.name})`); setIsSpeciesDropdownOpen(false); speciesDropdownRef.current?.setCustomValidity(''); }} @@ -361,8 +361,8 @@ const MessageCreation: React.FC = () => { Submit Message
+
- ); } diff --git a/src/pages/MessageDashboard.tsx b/src/pages/MessageDashboard.tsx index 2e7a1fc..a90751a 100644 --- a/src/pages/MessageDashboard.tsx +++ b/src/pages/MessageDashboard.tsx @@ -3,8 +3,6 @@ import { useNavigate, useParams } from 'react-router-dom'; import Navbar from '../components/Navbar'; import '../styles/MessageDashboard.css'; import { getMessagesByExperimentID } from '../services/messageService'; - -// Import types import { Message } from '../types/message'; import { Experiment } from '../types/experiment'; @@ -17,22 +15,30 @@ const MessageDashboard: React.FC = () => { const navigate = useNavigate(); const { id } = useParams<{ id: string }>(); + const severityLabels: { [key: number]: string } = { + 1: 'debug', + 2: 'info', + 3: 'warning', + 4: 'urgent', + 5: 'critical', + }; const [interactionTypes, setInteractionTypes] = useState([]); + const [speciesList, setSpeciesList] = useState([]); const [messages, setMessages] = useState([]); const [experiment] = useState(null); const [filteredMessages, setFilteredMessages] = useState([]); const [selectedTriggerType, setSelectedInteractionType] = useState('All'); + const [selectedSpecies, setSelectedSpecies] = useState('All'); const [searchQuery, setSearchQuery] = useState(''); const [sortConfig, setSortConfig] = useState<{ key: keyof Message; direction: string } | null>(null); - - // State for loading + const [showEncounterMeters, setShowEncounterMeters] = useState(false); + const [showEncounterMinutes, setShowEncounterMinutes] = useState(false); const [isLoading, setIsLoading] = useState(true); - - // State for dropdown open/close const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [isSpeciesDropdownOpen, setIsSpeciesDropdownOpen] = useState(false); - // Fetch messages on component mount + // Fetch messages and extract unique species and triggers useEffect(() => { const fetchMessages = async () => { if (!id) return; @@ -40,60 +46,71 @@ const MessageDashboard: React.FC = () => { setIsLoading(true); const fetchedMessages = await getMessagesByExperimentID(id); setMessages(fetchedMessages); - setIsLoading(false); - } catch (error) { - console.error('Error fetching messages:', error); - setIsLoading(false); - } - }; - - fetchMessages(); - }, [id]); - - // Fetch interaction types on component mount - useEffect(() => { - const fetchData = async () => { - try { - setIsLoading(true); - // Extract unique interaction types from messages + // Extract unique species' common names + const speciesSet = new Set(); const interactionTypesSet = new Set(); - messages.forEach((msg: Message) => { + + fetchedMessages.forEach((msg: Message) => { + if (msg.species && msg.species.commonName) { + const speciesName = `${msg.species.commonName} (${msg.species.name})`; + speciesSet.add(speciesName); + } if (msg.trigger) { interactionTypesSet.add(msg.trigger); } }); - const interactionTypesData = Array.from(interactionTypesSet).map((type) => ({ - ID: type, - name: type, - })); + setSpeciesList(['All', ...Array.from(speciesSet)]); - // Include 'All TriggernTypes' as default option const allTriggerTypesOption: TriggerType = { ID: 'all', name: 'All', }; - + const interactionTypesData = Array.from(interactionTypesSet).map((type) => ({ + ID: type, + name: type, + })); setInteractionTypes([allTriggerTypesOption, ...interactionTypesData]); - setFilteredMessages(messages); - setIsLoading(false); - } catch (error) { - console.error('Error fetching data:', error); + console.error('Error fetching messages:', error); setIsLoading(false); } }; - fetchData(); - }, [messages]); + fetchMessages(); + }, [id]); + + useEffect(() => { + setShowEncounterMeters( + filteredMessages.some( + (msg) => msg.encounterMeters != null && msg.encounterMeters > 0 + ) + ); + + setShowEncounterMinutes( + filteredMessages.some( + (msg) => msg.encounterMinutes != null && msg.encounterMinutes > 0 + ) + ); + }, [filteredMessages]); // Apply filters whenever filter state changes useEffect(() => { let filtered = [...messages]; + // Filter by Species + if (selectedSpecies !== 'All') { + filtered = filtered.filter((msg: Message) => { + const speciesName = msg.species + ? `${msg.species.commonName} (${msg.species.name})` + : ''; + return speciesName === selectedSpecies; + }); + } + // Filter by TriggerType if (selectedTriggerType !== 'All') { filtered = filtered.filter((msg: Message) => msg.trigger === selectedTriggerType); @@ -112,6 +129,14 @@ const MessageDashboard: React.FC = () => { let aValue: any = a[sortConfig.key] || ''; let bValue: any = b[sortConfig.key] || ''; + if (sortConfig.key === 'species') { + aValue = a.species?.commonName.toLowerCase() || ''; + bValue = b.species?.commonName.toLowerCase() || ''; + } else if (typeof aValue === 'string' && typeof bValue === 'string') { + aValue = aValue.toLowerCase(); + bValue = bValue.toLowerCase(); + } + if (aValue < bValue) { return sortConfig.direction === 'ascending' ? -1 : 1; } @@ -123,7 +148,7 @@ const MessageDashboard: React.FC = () => { } setFilteredMessages(filtered); - }, [selectedTriggerType, searchQuery, sortConfig, messages]); + }, [selectedSpecies, selectedTriggerType, searchQuery, sortConfig, messages]); // Handle sorting const requestSort = (key: keyof Message) => { @@ -149,7 +174,7 @@ const MessageDashboard: React.FC = () => { // Handle navigation to message details page const handleMessageClick = (message: Message) => { - navigate(`/message/${message.answerID}`, { state: { message } }); + navigate(`/message/${id}`, { state: { message } }); }; // Function to truncate text to a specified length @@ -165,17 +190,51 @@ const MessageDashboard: React.FC = () => { {/* Messages Title */}

- Messages for Experiment: {truncateText(experiment?.name || `Experiment ${id}`, 35)} + Messages for Experiment: {truncateText(experiment?.name || `Experiment ${id}`, 35)}

{/* Filters Container */}
+ {/* Species Filter */} +
+ +
+ + {isSpeciesDropdownOpen && ( +
+ {speciesList.map((speciesName) => ( +
{ + setSelectedSpecies(speciesName); + setIsSpeciesDropdownOpen(false); + }} + > + {speciesName} +
+ ))} +
+ )} +
+
+ {/* TriggerType Filter */}
-
+
-
- {interactionTypes.map((type) => ( -
{ - setSelectedInteractionType(type.name); - setIsDropdownOpen(false); - }} - > - {type.name} -
- ))} -
+ {isDropdownOpen && ( +
+ {interactionTypes.map((type) => ( +
{ + setSelectedInteractionType(type.name); + setIsDropdownOpen(false); + }} + > + {type.name} +
+ ))} +
+ )}
@@ -244,6 +305,14 @@ const MessageDashboard: React.FC = () => { className={`sort-icon ${getSortIconClass('name')}`} /> + requestSort('species')}> + Species + Sort Icon + requestSort('trigger')}> Trigger { className={`sort-icon ${getSortIconClass('severity')}`} /> - requestSort('encounterMeters')}> - Encounter Meters - Sort Icon - - requestSort('encounterMinutes')}> - Encounter Minutes + requestSort('activity')}> + Activity Sort Icon + {showEncounterMeters && ( + requestSort('encounterMeters')}> + Encounter Meters + Sort Icon + + )} + {showEncounterMinutes && ( + requestSort('encounterMinutes')}> + Encounter Minutes + Sort Icon + + )} {filteredMessages.map((msg: Message, index: number) => { return ( @@ -301,6 +382,11 @@ const MessageDashboard: React.FC = () => { > {truncateText(msg.name, 20)} + handleMessageClick(msg)}> + {msg.species + ? `${msg.species.commonName} (${msg.species.name})` + : 'Unknown Species'} + handleMessageClick(msg)}> {msg.trigger} @@ -309,15 +395,22 @@ const MessageDashboard: React.FC = () => { {truncateText(msg.text, 12)} handleMessageClick(msg)}> - {msg.severity} + {severityLabels[msg.severity] || 'Unknown'} + + handleMessageClick(msg)}> + {msg.activity.toString()} + {showEncounterMeters && ( handleMessageClick(msg)}> {msg.encounterMeters} + )} + {showEncounterMinutes && ( handleMessageClick(msg)}> {msg.encounterMinutes} - + )} + ); })} diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 07fc286..c96e878 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -28,24 +28,48 @@ const Profile: React.FC = () => { }, []); const handleLogout = () => { - localStorage.removeItem('authToken'); // Remove the token from local storage - navigate('/login'); // Redirect to the login page + localStorage.removeItem('authToken'); + navigate('/login'); }; + if (isLoading) { + return ( +
+ +
+
Loading...
+
+
+ ); + } + + if (error) { + return ( +
+ +
+
{error}
+
+
+ ); + } + return (
- {/* Navbar */} - - {/* Profile Content */}
{user && (

Profile

-

Name: {user.name}

-

Email: {user.email}

-

Role: {user.roles[0]?.name || 'N/A'}

- +

+ Name: {user.name} +

+

+ Email: {user.email} +

+

+ Role: {user.roles[0]?.name || 'N/A'} +

diff --git a/src/pages/Questionnaire.tsx b/src/pages/Questionnaire.tsx new file mode 100644 index 0000000..f3a7c83 --- /dev/null +++ b/src/pages/Questionnaire.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useLocation } from 'react-router-dom'; +import Navbar from '../components/Navbar'; +import DynamicView from '../components/DynamicView'; +import '../styles/Questionnaire.css'; +import { Questionnaire as QuestionnaireType } from '../types/questionnaire'; + +const Questionnaire: React.FC = () => { + const location = useLocation(); + const questionnaire = location.state?.questionnaire as QuestionnaireType | null; + + if (!questionnaire) { + return ( + <> + +
+

Questionnaire not found.

+
+ + ); + } + + // Prepare fields to display + const fields = [ + { name: 'Name', value: questionnaire.name || 'N/A' }, + { name: 'Identifier', value: questionnaire.identifier || 'N/A' }, + { + name: 'Interaction Type', + value: questionnaire.interactionType?.name || 'N/A', + }, + { + name: 'Experiment', + value: questionnaire.experiment?.name || 'N/A', + }, + { + name: 'Amount of Questions', + value: questionnaire.questions + ? questionnaire.questions.length.toString() + : 'N/A', + }, + ]; + + return ( + <> + +
+ {/* Title */} +

Questionnaire View

+ + {/* Questionnaire Details Component */} +
+ +
+
+ + ); +}; + +export default Questionnaire; \ No newline at end of file diff --git a/src/pages/QuestionnaireCreation.tsx b/src/pages/QuestionnaireCreation.tsx new file mode 100644 index 0000000..aa6c04f --- /dev/null +++ b/src/pages/QuestionnaireCreation.tsx @@ -0,0 +1,176 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import Navbar from '../components/Navbar'; +import '../styles/QuestionnaireCreation.css'; +import { AddQuestionnaire } from '../types/questionnaire'; +import { addQuestionnaire } from '../services/questionnaireService'; +import { getAllInteractions } from '../services/interactionTypeService'; +import { InteractionType } from '../types/interactiontype'; + +const QuestionnaireCreation: React.FC = () => { + const { id: experimentID } = useParams<{ id: string }>(); // Get experimentID from URL + const [name, setName] = useState(''); + const [identifier, setIdentifier] = useState(''); + const [interactionTypes, setInteractionTypes] = useState([]); + const [interactionTypeID, setInteractionTypeID] = useState(null); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [interactionTypeError, setInteractionTypeError] = useState(''); + + const navigate = useNavigate(); + const formRef = useRef(null); + + useEffect(() => { + const fetchInteractionTypes = async () => { + try { + const interactionTypes: InteractionType[] = await getAllInteractions(); + setInteractionTypes(interactionTypes); + } catch (error) { + console.error('Failed to fetch interaction types:', error); + } + }; + fetchInteractionTypes(); + }, []); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + let isValid = true; + + // Reset custom error messages + setInteractionTypeError(''); + + // Validate Interaction Type + if (!interactionTypeID) { + setInteractionTypeError('Please select an interaction type.'); + isValid = false; + } + + // Trigger native validation messages + const form = formRef.current; + if (form && !form.checkValidity()) { + form.reportValidity(); + isValid = false; + } + + if (!isValid) { + return; + } + + const questionnaireData: AddQuestionnaire = { + experimentID: experimentID!, + name: name, + identifier: identifier, + interactionTypeID: interactionTypeID!, + }; + + try { + const response = await addQuestionnaire(questionnaireData); + console.log('Questionnaire added successfully:', response); + navigate(`/QuestionnaireDashboard/${experimentID}`); + } catch (error) { + console.error('Error adding questionnaire:', error); + } + }; + + return ( +
+ {/* Navbar */} + + + {/* Main Container */} +
+ {/* Title */} +

New Questionnaire

+ + {/* Content Box */} +
+ {/* Name */} + + setName(e.target.value)} + required + /> + + {/* Identifier */} + + setIdentifier(e.target.value)} + /> + + {/* Interaction Type */} + +
+ + {isDropdownOpen && ( +
+ {interactionTypes.map((type) => ( +
{ + setInteractionTypeID(type.ID); + setIsDropdownOpen(false); + setInteractionTypeError(''); + }} + > + {type.name} +
+ ))} +
+ )} +
+ {/* Interaction Type Error Message */} + {interactionTypeError && ( +
{interactionTypeError}
+ )} + + {/* Submit Button */} + +
+
+
+ ); +}; + +export default QuestionnaireCreation; diff --git a/src/pages/QuestionnaireDashboard.tsx b/src/pages/QuestionnaireDashboard.tsx new file mode 100644 index 0000000..0e64d6d --- /dev/null +++ b/src/pages/QuestionnaireDashboard.tsx @@ -0,0 +1,299 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import Navbar from '../components/Navbar'; +import '../styles/QuestionnaireDashboard.css'; +import { Questionnaire } from '../types/questionnaire'; +import { getQuestionnaireByExperimentID } from '../services/questionnaireService'; +import { Experiment } from '../types/experiment'; +import { getAllInteractions } from '../services/interactionTypeService'; +import { InteractionType } from '../types/interactiontype'; + +const QuestionnaireDashboard: React.FC = () => { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + + const [questionnaires, setQuestionnaires] = useState([]); + const [experiment] = useState(null); + const [filteredQuestionnaires, setFilteredQuestionnaires] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [isLoading, setIsLoading] = useState(true); + + // Interaction Type Filter State + const [interactionTypes, setInteractionTypes] = useState([]); + const [selectedInteractionType, setSelectedInteractionType] = useState('All'); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + // Sorting State + type SortKey = keyof Questionnaire | 'numberOfQuestions'; + const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: string } | null>(null); + + useEffect(() => { + const fetchQuestionnaires = async () => { + if (!id) return; + try { + setIsLoading(true); + const fetchedQuestionnaires = await getQuestionnaireByExperimentID(id); + + // Process the data to ensure interactionType and questions are defined + const processedQuestionnaires = fetchedQuestionnaires.map((questionnaire) => ({ + ...questionnaire, + interactionType: questionnaire.interactionType || { name: 'Unknown' }, + questions: questionnaire.questions || [], + })); + + setQuestionnaires(processedQuestionnaires); + setIsLoading(false); + } catch (error) { + console.error('Error fetching questionnaires:', error); + setIsLoading(false); + } + }; + + fetchQuestionnaires(); + }, [id]); + + // Fetch Interaction Types for the Filter + useEffect(() => { + const fetchInteractionTypes = async () => { + try { + const types = await getAllInteractions(); + setInteractionTypes(types); + } catch (error) { + console.error('Failed to fetch interaction types:', error); + } + }; + fetchInteractionTypes(); + }, []); + + useEffect(() => { + let filtered = [...questionnaires]; + + // Filter by Interaction Type + if (selectedInteractionType !== 'All') { + filtered = filtered.filter((q: Questionnaire) => + q.interactionType?.name === selectedInteractionType + ); + } + + // Filter by search query + if (searchQuery) { + filtered = filtered.filter((q: Questionnaire) => + q.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + } + + // Apply sorting + if (sortConfig !== null) { + filtered.sort((a: Questionnaire, b: Questionnaire) => { + let aValue: any; + let bValue: any; + + if (sortConfig.key === 'numberOfQuestions') { + aValue = a.questions ? a.questions.length : 0; + bValue = b.questions ? b.questions.length : 0; + } else if (sortConfig.key === 'interactionType') { + aValue = a.interactionType?.name || ''; + bValue = b.interactionType?.name || ''; + } else { + aValue = a[sortConfig.key] || ''; + bValue = b[sortConfig.key] || ''; + } + + if (typeof aValue === 'string' && typeof bValue === 'string') { + aValue = aValue.toLowerCase(); + bValue = bValue.toLowerCase(); + } + + if (aValue < bValue) { + return sortConfig.direction === 'ascending' ? -1 : 1; + } + if (aValue > bValue) { + return sortConfig.direction === 'ascending' ? 1 : -1; + } + return 0; + }); + } + + setFilteredQuestionnaires(filtered); + }, [searchQuery, sortConfig, questionnaires, selectedInteractionType]); + + const requestSort = (key: SortKey) => { + let direction = 'ascending'; + if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') { + direction = 'descending'; + } + setSortConfig({ key, direction }); + }; + + const getSortIconClass = (columnKey: SortKey) => { + if (sortConfig?.key === columnKey) { + return sortConfig.direction === 'ascending' ? 'sort-ascending' : 'sort-descending'; + } + return ''; + }; + + const navigateToCreateQuestionnaire = () => { + navigate(`/questionnairecreation/${id}`); + }; + + const handleQuestionnaireClick = (questionnaire: Questionnaire) => { + navigate(`/questionnaire/${id}`, { state: { questionnaire } }); + }; + + const truncateText = (text: string, maxLength: number): string => { + if (text.length <= maxLength) return text; + return text.substring(0, maxLength) + '...'; + }; + + return ( +
+ {/* Navbar */} + + + {/* Questionnaires Title */} +

+ Questionnaires for Experiment: {truncateText(experiment?.name || `Experiment ${id}`, 35)} +

+ + {/* Filters Container */} +
+ {/* Interaction Type Filter */} +
+ +
+ + {isDropdownOpen && ( +
+
{ + setSelectedInteractionType('All'); + setIsDropdownOpen(false); + }} + > + All +
+ {interactionTypes.map((type) => ( +
{ + setSelectedInteractionType(type.name); + setIsDropdownOpen(false); + }} + > + {type.name} +
+ ))} +
+ )} +
+
+ + {/* Search Filter */} +
+
+ setSearchQuery(e.target.value)} + data-testid="search-input" + /> + Search Icon +
+
+
+ + {/* Loading Animation or Questionnaires Table */} + {isLoading ? ( +
+
+
+ ) : ( + <> + {/* Questionnaires Table */} +
+ + + + + + + + + + + {filteredQuestionnaires.map((questionnaire, index) => ( + handleQuestionnaireClick(questionnaire)} + data-testid={`questionnaire-row-${index}`} + > + + + + + + ))} + +
requestSort('name')}> + Name + Sort Icon + requestSort('identifier')}> + Internal Name + Sort Icon + requestSort('interactionType')}> + Interaction Type + Sort Icon + requestSort('numberOfQuestions')}> + Amount of Questions + Sort Icon +
{truncateText(questionnaire.name, 35)}{questionnaire.identifier}{questionnaire.interactionType?.name || 'N/A'}{questionnaire.questions ? questionnaire.questions.length : 0}
+
+ + {/* Add Questionnaire Button */} + + + )} +
+ ); +}; + +export default QuestionnaireDashboard; diff --git a/src/pages/__tests__/Dashboard.integration.test.tsx b/src/pages/__tests__/Dashboard.integration.test.tsx index 1b2b1ba..2e0ee4c 100644 --- a/src/pages/__tests__/Dashboard.integration.test.tsx +++ b/src/pages/__tests__/Dashboard.integration.test.tsx @@ -1,101 +1,76 @@ // Dashboard.integration.test.tsx import React from 'react'; -import { render, screen, waitFor, fireEvent, within } from '@testing-library/react'; -import Dashboard from '../Dashboard'; -import '@testing-library/jest-dom/extend-expect'; -import { getAllLivingLabs } from '../../services/livingLabService'; -import { getMyExperiments } from '../../services/experimentService'; +import { render, screen, fireEvent, waitFor, within } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; +import Dashboard from '../Dashboard'; +import {getAllLivingLabs} from '../../services/livingLabService'; +import { addExperiment, getMyExperiments } from '../../services/experimentService'; +import dotenv from 'dotenv'; -// Mock the API functions -jest.mock('../../services/livingLabService', () => ({ - getAllLivingLabs: jest.fn(), -})); - -jest.mock('../../services/experimentService', () => ({ - getMyExperiments: jest.fn(), -})); - -const mockLivingLabs = [ - { - ID: 'lab1', - name: 'LivingLab 1', - definition: [], - $schema: '', - }, - { - ID: 'lab2', - name: 'LivingLab 2', - definition: [], - $schema: '', - }, -]; - -const mockExperiments = [ - { - ID: 'exp1', - name: 'Experiment 1', - start: '2023-01-01', - end: '2023-12-31', - user: { name: 'User 1' }, - livingLab: { name: 'LivingLab 1' }, - questionnaires: 5, - messages: 10, - responses: 100, - }, - { - ID: 'exp2', - name: 'Experiment 2', - start: '2023-02-01', - end: '2023-11-30', - user: { name: 'User 2' }, - livingLab: { name: 'LivingLab 2' }, - questionnaires: 3, - messages: 5, - responses: 50, - }, -]; +// Load environment variables +dotenv.config(); describe('Dashboard Integration Tests', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - // CATEGORY 1: DATA FETCHING AND INITIAL RENDERING - - test('FETCH LIVINGLABS AND EXPERIMENTS ON MOUNT', async () => { - (getAllLivingLabs as jest.Mock).mockResolvedValueOnce(mockLivingLabs); - (getMyExperiments as jest.Mock).mockResolvedValueOnce(mockExperiments); + // Generate a unique identifier for each test run + const uniqueId = new Date().toISOString().replace(/[:.]/g, '-'); + + // Store created experiment IDs and names + let createdExperimentIds: string[] = []; + let experimentNames: string[] = []; + let startDate: Date; + let endDate: Date; + + // Use existing LivingLab + const existingLivingLabId = '9c7dbce1-6c4f-46b6-b0c6-ec5d2c2b48c1'; + const existingLivingLabName = 'Living Lab Eindhoven'; + + beforeAll(async () => { + // Insert authentication token into localStorage + const authToken = process.env.AUTH_TOKEN; + if (authToken) { + window.localStorage.setItem('authToken', authToken); + } else { + console.warn('AUTH_TOKEN is not defined in the environment variables.'); + } - render( - - - - ); + // Generate unique experiment names + experimentNames = [`Experiment 1 ${uniqueId}`, `Experiment 2 ${uniqueId}`]; - await waitFor(() => expect(getAllLivingLabs).toHaveBeenCalled()); - await waitFor(() => expect(getMyExperiments).toHaveBeenCalled()); + // Calculate future start and end dates + const today = new Date(); + startDate = new Date(today.getTime() + 24 * 60 * 60 * 1000); // Tomorrow + endDate = new Date(today.getTime() + 10 * 24 * 60 * 60 * 1000); // 10 days from today - expect(await screen.findByText('Experiment 1')).toBeInTheDocument(); - expect(await screen.findByText('Experiment 2')).toBeInTheDocument(); - }); + for (const name of experimentNames) { + const experimentData = { + name, + description: 'Sample experiment description', + start: startDate.toISOString(), + end: endDate.toISOString(), + // Removed unexpected properties + }; + try { + const experiment = await addExperiment(experimentData); + createdExperimentIds.push(experiment.ID); + } catch (error) { + console.error('Failed to add experiment:', error); + } + } + }); - test('RENDER LOADING STATE', async () => { - let resolveLivingLabs!: (value: any) => void; - let resolveExperiments!: (value: any) => void; - - const livingLabsPromise = new Promise((resolve) => { - resolveLivingLabs = resolve; - }); + afterAll(() => { + // Clear localStorage after tests + window.localStorage.clear(); + }); - const experimentsPromise = new Promise((resolve) => { - resolveExperiments = resolve; - }); + // CATEGORY 1: DATA FETCHING AND INITIAL RENDERING - (getAllLivingLabs as jest.Mock).mockReturnValue(livingLabsPromise); - (getMyExperiments as jest.Mock).mockReturnValue(experimentsPromise); + test('FETCH LIVINGLABS AND EXPERIMENTS ON MOUNT', async () => { + // Fetch data directly + const livingLabs = await getAllLivingLabs(); + const myExperiments = await getMyExperiments(); render( @@ -103,81 +78,45 @@ describe('Dashboard Integration Tests', () => { ); - expect(screen.getByTestId('loading-container')).toBeInTheDocument(); - - resolveLivingLabs(mockLivingLabs); - resolveExperiments(mockExperiments); - - await waitFor(() => { - expect(screen.queryByTestId('loading-container')).not.toBeInTheDocument(); - }); - }); - - test('RENDER EXPERIMENTS TABLE AFTER FETCH', async () => { - (getAllLivingLabs as jest.Mock).mockResolvedValueOnce(mockLivingLabs); - (getMyExperiments as jest.Mock).mockResolvedValueOnce(mockExperiments); - - render( - - - - ); - + // Wait for the Dashboard to render await waitFor(() => { expect(screen.getByTestId('experiments-table')).toBeInTheDocument(); }); - - expect(await screen.findByText('Experiment 1')).toBeInTheDocument(); - expect(await screen.findByText('Experiment 2')).toBeInTheDocument(); - - // Use getAllByText and ensure you have the expected number of elements - const livingLabCells = screen.getAllByText('LivingLab 1', { selector: 'td' }); - expect(livingLabCells).toHaveLength(1); - - const livingLab2Cells = screen.getAllByText('LivingLab 2', { selector: 'td' }); - expect(livingLab2Cells).toHaveLength(1); - }); - - // CATEGORY 2: FILTER COMBINATIONS + // Verify that the experiments from getMyExperiments are displayed + for (const experiment of myExperiments) { + expect(screen.getByText(experiment.name)).toBeInTheDocument(); + } - test('APPLYING MULTIPLE FILTERS SIMULTANEOUSLY', async () => { - (getAllLivingLabs as jest.Mock).mockResolvedValueOnce(mockLivingLabs); - (getMyExperiments as jest.Mock).mockResolvedValueOnce(mockExperiments); + // Verify that the living labs from getAllLivingLabs are available in the filters + fireEvent.click(screen.getByTestId('livinglab-dropdown-button')); + for (const lab of livingLabs) { + expect(screen.getByText(lab.name)).toBeInTheDocument(); + } + }); + test('RENDER LOADING STATE', async () => { render( ); + // Initially, the loading container should be in the document + expect(screen.getByTestId('loading-container')).toBeInTheDocument(); + + // Wait for the experiments table to appear await waitFor(() => { expect(screen.getByTestId('experiments-table')).toBeInTheDocument(); }); - // LivingLab filter - const livingLabDropdownButton = screen.getByTestId('livinglab-dropdown-button'); - fireEvent.click(livingLabDropdownButton); - const livingLabOption = screen.getByTestId('livinglab-option-lab1'); - fireEvent.click(livingLabOption); - - // Date range filter - const startDateInput = screen.getByTestId('start-date-input'); - const endDateInput = screen.getByTestId('end-date-input'); - fireEvent.change(startDateInput, { target: { value: '2023-01-01' } }); - fireEvent.change(endDateInput, { target: { value: '2023-12-31' } }); - - // Search query - const searchInput = screen.getByTestId('search-input'); - fireEvent.change(searchInput, { target: { value: 'Experiment 1' } }); - - expect(screen.getByText('Experiment 1')).toBeInTheDocument(); - expect(screen.queryByText('Experiment 2')).not.toBeInTheDocument(); + // Now, the loading container should not be in the document + expect(screen.queryByTestId('loading-container')).not.toBeInTheDocument(); }); - test('CLEARING FILTERS RESETS EXPERIMENTS LIST', async () => { - (getAllLivingLabs as jest.Mock).mockResolvedValueOnce(mockLivingLabs); - (getMyExperiments as jest.Mock).mockResolvedValueOnce(mockExperiments); + test('RENDER EXPERIMENTS TABLE AFTER FETCH', async () => { + // Fetch experiments + const myExperiments = await getMyExperiments(); render( @@ -185,47 +124,23 @@ describe('Dashboard Integration Tests', () => { ); + // Wait for the experiments table to appear await waitFor(() => { expect(screen.getByTestId('experiments-table')).toBeInTheDocument(); }); - // Apply filters - const livingLabDropdownButton = screen.getByTestId('livinglab-dropdown-button'); - fireEvent.click(livingLabDropdownButton); - const livingLabOption = screen.getByTestId('livinglab-option-lab1'); - fireEvent.click(livingLabOption); - - const startDateInput = screen.getByTestId('start-date-input'); - const endDateInput = screen.getByTestId('end-date-input'); - fireEvent.change(startDateInput, { target: { value: '2023-01-01' } }); - fireEvent.change(endDateInput, { target: { value: '2023-12-31' } }); - - const searchInput = screen.getByTestId('search-input'); - fireEvent.change(searchInput, { target: { value: 'Experiment 1' } }); - - // Verify only Experiment 1 is shown - expect(screen.getByText('Experiment 1')).toBeInTheDocument(); - expect(screen.queryByText('Experiment 2')).not.toBeInTheDocument(); - - // Clear filters - fireEvent.click(livingLabDropdownButton); - const allLivingLabsOption = screen.getByTestId('livinglab-option-all'); - fireEvent.click(allLivingLabsOption); - - fireEvent.change(startDateInput, { target: { value: '' } }); - fireEvent.change(endDateInput, { target: { value: '' } }); - fireEvent.change(searchInput, { target: { value: '' } }); - - // Verify both experiments are shown - expect(screen.getByText('Experiment 1')).toBeInTheDocument(); - expect(screen.getByText('Experiment 2')).toBeInTheDocument(); + // Verify that the experiments are displayed + for (const experiment of myExperiments) { + expect(screen.getByText(experiment.name)).toBeInTheDocument(); + } }); - // CATEGORY 3: SORTING AND FILTERING INTERACTION + + // CATEGORY 2: SORTING AND FILTERING INTERACTION test('SORTING APPLIES AFTER FILTERING', async () => { - (getAllLivingLabs as jest.Mock).mockResolvedValueOnce(mockLivingLabs); - (getMyExperiments as jest.Mock).mockResolvedValueOnce(mockExperiments); + // Fetch experiments + const myExperiments = await getMyExperiments(); render( @@ -233,29 +148,36 @@ describe('Dashboard Integration Tests', () => { ); - await waitFor(() => { - expect(screen.getByTestId('experiments-table')).toBeInTheDocument(); - }); + // Wait for experiments to be displayed + for (const experiment of myExperiments) { + expect(await screen.findByText(experiment.name)).toBeInTheDocument(); + } // Apply search filter const searchInput = screen.getByTestId('search-input'); fireEvent.change(searchInput, { target: { value: 'Experiment' } }); - // Sort by 'Responses' + // Sort by 'Responses' (assuming 'Responses' is a sortable column) const responsesHeader = screen.getByText('Responses'); fireEvent.click(responsesHeader); + // Verify the order of experiments const experimentRows = screen.getAllByTestId(/experiment-row-/); - const firstExperiment = within(experimentRows[0]).getByText('Experiment 2'); - const secondExperiment = within(experimentRows[1]).getByText('Experiment 1'); - expect(firstExperiment).toBeInTheDocument(); - expect(secondExperiment).toBeInTheDocument(); + // Sort experiments manually to get expected order + const sortedExperiments = [...myExperiments] + .filter((exp) => exp.name.includes('Experiment')) + .sort((a, b) => (b.responses ?? 0) - (a.responses ?? 0)); + + for (let i = 0; i < sortedExperiments.length; i++) { + const row = experimentRows[i]; + expect(within(row).getByText(sortedExperiments[i].name)).toBeInTheDocument(); + } }); test('FILTERING UPDATES SORTED LIST', async () => { - (getAllLivingLabs as jest.Mock).mockResolvedValueOnce(mockLivingLabs); - (getMyExperiments as jest.Mock).mockResolvedValueOnce(mockExperiments); + // Fetch experiments + const myExperiments = await getMyExperiments(); render( @@ -263,9 +185,10 @@ describe('Dashboard Integration Tests', () => { ); - await waitFor(() => { - expect(screen.getByTestId('experiments-table')).toBeInTheDocument(); - }); + // Wait for experiments to be displayed + for (const experiment of myExperiments) { + expect(await screen.findByText(experiment.name)).toBeInTheDocument(); + } // Sort by 'Responses' const responsesHeader = screen.getByText('Responses'); @@ -273,100 +196,44 @@ describe('Dashboard Integration Tests', () => { // Apply search filter const searchInput = screen.getByTestId('search-input'); - fireEvent.change(searchInput, { target: { value: 'Experiment 1' } }); + fireEvent.change(searchInput, { target: { value: experimentNames[0] } }); const experimentRows = screen.getAllByTestId(/experiment-row-/); - const firstExperiment = within(experimentRows[0]).getByText('Experiment 1'); - - expect(firstExperiment).toBeInTheDocument(); + expect(within(experimentRows[0]).getByText(experimentNames[0])).toBeInTheDocument(); + expect(experimentRows.length).toBe(1); }); - // CATEGORY 4: NAVIGATION FUNCTIONALITY (NOT IMPLEMENTED YET!!!!) - - /* test('NAVIGATE TO CREATE EXPERIMENT PAGE', async () => { - (getAllLivingLabs as jest.Mock).mockResolvedValueOnce(mockLivingLabs); - (getMyExperiments as jest.Mock).mockResolvedValueOnce(mockExperiments); - - const mockNavigate = jest.fn(); - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useNavigate: () => mockNavigate, - })); + // CATEGORY 3: RESPONSIVENESS AND UI ELEMENTS + test('DROPDOWN CLICK STATES', async () => { render( ); + // Wait for the component to render await waitFor(() => { - expect(screen.getByTestId('add-experiment-button')).toBeInTheDocument(); + expect(screen.getByTestId('livinglab-dropdown-button')).toBeInTheDocument(); }); - const addButton = screen.getByTestId('add-experiment-button'); - fireEvent.click(addButton); - - expect(mockNavigate).toHaveBeenCalledWith('/experimentcreation'); - }); + const dropdownButton = screen.getByTestId('livinglab-dropdown-button'); + const dropdownIcon = screen.getByAltText('Dropdown Icon'); - test('NAVIGATE TO EXPERIMENT DETAILS PAGE', async () => { - (getAllLivingLabs as jest.Mock).mockResolvedValueOnce(mockLivingLabs); - (getMyExperiments as jest.Mock).mockResolvedValueOnce(mockExperiments); + expect(dropdownIcon).toHaveClass('dropdown-icon'); + expect(dropdownIcon).not.toHaveClass('dropdown-icon-hover'); - const mockNavigate = jest.fn(); - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useNavigate: () => mockNavigate, - })); + fireEvent.click(dropdownButton); + expect(dropdownIcon).toHaveClass('dropdown-icon-hover'); - render( - - - - ); - - await waitFor(() => { - expect(screen.getByTestId('experiments-table')).toBeInTheDocument(); - }); - - const exp1Row = screen.getByTestId('experiment-row-exp1'); - fireEvent.click(exp1Row); - - expect(mockNavigate).toHaveBeenCalledWith('/experiment/exp1'); + fireEvent.click(dropdownButton); + expect(dropdownIcon).not.toHaveClass('dropdown-icon-hover'); }); - */ - - // CATEGORY 5: RESPONSIVENESS AND UI ELEMENTS - - test('DROPDOWN CLICK STATES', async () => { - // Mock API calls - (getAllLivingLabs as jest.Mock).mockResolvedValueOnce(mockLivingLabs); - (getMyExperiments as jest.Mock).mockResolvedValueOnce(mockExperiments); - - // Render the Dashboard component - render( - - - - ); - - const dropdownButton = screen.getByTestId('livinglab-dropdown-button'); - - const dropdownIcon = screen.getByAltText('Dropdown Icon'); - expect(dropdownIcon).toHaveClass('dropdown-icon'); - expect(dropdownIcon).not.toHaveClass('dropdown-icon-hover'); - - fireEvent.click(dropdownButton); - expect(dropdownIcon).toHaveClass('dropdown-icon-hover'); - - fireEvent.click(dropdownButton); - expect(dropdownIcon).not.toHaveClass('dropdown-icon-hover'); -}); test('ALTERNATING ROW COLORS', async () => { - (getAllLivingLabs as jest.Mock).mockResolvedValueOnce(mockLivingLabs); - (getMyExperiments as jest.Mock).mockResolvedValueOnce(mockExperiments); + // Fetch experiments + const myExperiments = await getMyExperiments(); render( @@ -374,21 +241,21 @@ describe('Dashboard Integration Tests', () => { ); - await waitFor(() => { - expect(screen.getByTestId('experiments-table')).toBeInTheDocument(); - }); + // Wait for experiments to be displayed + for (const experiment of myExperiments) { + expect(await screen.findByText(experiment.name)).toBeInTheDocument(); + } const experimentRows = screen.getAllByTestId(/experiment-row-/); expect(experimentRows[0]).toHaveClass('row-even'); expect(experimentRows[1]).toHaveClass('row-odd'); }); - - // CATEGORY 6: STATE MANAGEMENT AND SIDE EFFECTS + // CATEGORY 4: STATE MANAGEMENT AND SIDE EFFECTS test('USEEFFECT DEPENDENCIES TRIGGER CORRECTLY', async () => { - (getAllLivingLabs as jest.Mock).mockResolvedValueOnce(mockLivingLabs); - (getMyExperiments as jest.Mock).mockResolvedValueOnce(mockExperiments); + // Fetch experiments + const myExperiments = await getMyExperiments(); render( @@ -396,48 +263,15 @@ describe('Dashboard Integration Tests', () => { ); - await waitFor(() => { - expect(screen.getByTestId('experiments-table')).toBeInTheDocument(); - }); + // Wait for experiments to be displayed + expect(await screen.findByText(experimentNames[0])).toBeInTheDocument(); + // Change search input const searchInput = screen.getByTestId('search-input'); - fireEvent.change(searchInput, { target: { value: 'Experiment 1' } }); - - expect(screen.getByText('Experiment 1')).toBeInTheDocument(); - expect(screen.queryByText('Experiment 2')).not.toBeInTheDocument(); - }); - - test('SELECT ALL STATE REFLECTS INDIVIDUAL SELECTIONS', async () => { - (getAllLivingLabs as jest.Mock).mockResolvedValueOnce(mockLivingLabs); - (getMyExperiments as jest.Mock).mockResolvedValueOnce(mockExperiments); - - render( - - - - ); - - await waitFor(() => { - expect(screen.getByTestId('experiments-table')).toBeInTheDocument(); - }); - - const selectAllCheckbox = screen.getByTestId('select-all-checkbox'); - const exp1Checkbox = screen.getByTestId('experiment-checkbox-exp1'); - const exp2Checkbox = screen.getByTestId('experiment-checkbox-exp2'); - - // Initially, select all is unchecked - expect(selectAllCheckbox).not.toBeChecked(); - - // Select all - fireEvent.click(selectAllCheckbox); - expect(selectAllCheckbox).toBeChecked(); - expect(exp1Checkbox).toBeChecked(); - expect(exp2Checkbox).toBeChecked(); + fireEvent.change(searchInput, { target: { value: experimentNames[0] } }); - // Uncheck one experiment - fireEvent.click(exp1Checkbox); - expect(selectAllCheckbox).not.toBeChecked(); - expect(exp1Checkbox).not.toBeChecked(); - expect(exp2Checkbox).toBeChecked(); + // Verify that only the searched experiment is displayed + expect(screen.getByText(experimentNames[0])).toBeInTheDocument(); + expect(screen.queryByText(experimentNames[1])).not.toBeInTheDocument(); }); -}); +}); \ No newline at end of file diff --git a/src/pages/__tests__/Dashboard.unit.test.tsx b/src/pages/__tests__/Dashboard.unit.test.tsx index 8add7ca..0105685 100644 --- a/src/pages/__tests__/Dashboard.unit.test.tsx +++ b/src/pages/__tests__/Dashboard.unit.test.tsx @@ -70,12 +70,11 @@ describe('Dashboard Component - Unit Tests', () => { $schema: '', } as User, livingLab: mockLivingLabs[1], - responses: 5, + responses: 8, numberOfQuestionnaires: 2, numberOfMessages: 10, messageActivity: 5, questionnaireActivity: 3, - $schema: '', }, { ID: '2', @@ -94,9 +93,8 @@ describe('Dashboard Component - Unit Tests', () => { responses: 15, numberOfQuestionnaires: 5, numberOfMessages: 20, - messageActivity: 5, - questionnaireActivity: 3, - $schema: '', + messageActivity: 10, + questionnaireActivity: 5, }, { ID: '3', @@ -117,7 +115,6 @@ describe('Dashboard Component - Unit Tests', () => { numberOfMessages: 15, messageActivity: 5, questionnaireActivity: 3, - $schema: '', }, ]; @@ -311,6 +308,7 @@ describe('Dashboard Component - Unit Tests', () => { // Wait for the component to re-render and get the updated experiment rows await waitFor(() => { + screen.debug(); const table = screen.getByTestId('experiments-table'); const experimentRows = within(table).getAllByTestId(/experiment-row-/); @@ -320,102 +318,8 @@ describe('Dashboard Component - Unit Tests', () => { }); - // CATEGORY 5: CHECKBOX SELECTION - describe('CATEGORY 5: CHECKBOX SELECTION', () => { - test('INDIVIDUAL CHECKBOX RENDERING', async () => { - render( - - - - ); - - // Wait for experiments to be displayed - await waitFor(() => expect(screen.getByText('Past Experiment')).toBeInTheDocument()); - - // Check that each experiment row has a checkbox - mockExperiments.forEach(exp => { - const checkbox = screen.getByTestId(`experiment-checkbox-${exp.ID}`); - expect(checkbox).toBeInTheDocument(); - expect(checkbox).not.toBeChecked(); - }); - }); - - test('INDIVIDUAL CHECKBOX TOGGLES STATE', async () => { - render( - - - - ); - - // Wait for experiments to be displayed - await waitFor(() => expect(screen.getByText('Past Experiment')).toBeInTheDocument()); - - const checkbox = screen.getByTestId('experiment-checkbox-1'); - - // Initially unchecked - expect(checkbox).not.toBeChecked(); - - // Click to check - fireEvent.click(checkbox); - expect(checkbox).toBeChecked(); - - // Click again to uncheck - fireEvent.click(checkbox); - expect(checkbox).not.toBeChecked(); - }); - - test('SELECT ALL CHECKBOX RENDERS CORRECTLY', async () => { - render( - - - - ); - - // Wait for the select all checkbox to be in the document - await waitFor(() => expect(screen.getByTestId('select-all-checkbox')).toBeInTheDocument()); - - const selectAllCheckbox = screen.getByTestId('select-all-checkbox'); - - // Initially unchecked - expect(selectAllCheckbox).not.toBeChecked(); - }); - - test('SELECT ALL CHECKBOX TOGGLES ALL EXPERIMENTS', async () => { - render( - - - - ); - - // Wait for experiments and select all checkbox to be displayed - await waitFor(() => expect(screen.getByTestId('select-all-checkbox')).toBeInTheDocument()); - - const selectAllCheckbox = screen.getByTestId('select-all-checkbox'); - - // Click to check all - fireEvent.click(selectAllCheckbox); - expect(selectAllCheckbox).toBeChecked(); - - // All individual checkboxes should be checked - mockExperiments.forEach(exp => { - const checkbox = screen.getByTestId(`experiment-checkbox-${exp.ID}`); - expect(checkbox).toBeChecked(); - }); - - // Click again to uncheck all - fireEvent.click(selectAllCheckbox); - expect(selectAllCheckbox).not.toBeChecked(); - - // All individual checkboxes should be unchecked - mockExperiments.forEach(exp => { - const checkbox = screen.getByTestId(`experiment-checkbox-${exp.ID}`); - expect(checkbox).not.toBeChecked(); - }); - }); - }); - - // CATEGORY 6: GETSTATUS FUNCTION - describe('CATEGORY 6: GETSTATUS FUNCTION', () => { + // CATEGORY 5: GETSTATUS FUNCTION + describe('CATEGORY 5: GETSTATUS FUNCTION', () => { test("Displays 'Upcoming' status for future experiments", async () => { render( diff --git a/src/services/interactionService.ts b/src/services/interactionService.ts new file mode 100644 index 0000000..83ea844 --- /dev/null +++ b/src/services/interactionService.ts @@ -0,0 +1,36 @@ +import { InteractionType } from '../types/interactiontype'; +const API_URL = 'https://wildlifenl-uu-michi011.apps.cl01.cp.its.uu.nl/interactions/'; + + +const getAuthToken = (): string | null => { + return localStorage.getItem('authToken'); +}; + +export const getAllInteractions = async (): Promise => { + try { + const token = getAuthToken(); + if (!token) { + throw new Error('No authentication token found'); + } + + const response = await fetch(`${API_URL}`, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + }, + }); + + if (response.ok) { + const data = await response.json(); + return data; + } else { + const errorData = await response.json(); + console.error('Failed to fetch interaction types:', errorData); + throw new Error('Failed to fetch interaction types'); + } + } catch (error) { + console.error('Error fetching interaction types:', error); + throw error; + } +}; \ No newline at end of file diff --git a/src/services/interactionTypeService.ts b/src/services/interactionTypeService.ts new file mode 100644 index 0000000..34e0409 --- /dev/null +++ b/src/services/interactionTypeService.ts @@ -0,0 +1,36 @@ +import { InteractionType } from '../types/interactiontype'; +const API_URL = 'https://wildlifenl-uu-michi011.apps.cl01.cp.its.uu.nl/interactionTypes/'; + + +const getAuthToken = (): string | null => { + return localStorage.getItem('authToken'); +}; + +export const getAllInteractions = async (): Promise => { + try { + const token = getAuthToken(); + if (!token) { + throw new Error('No authentication token found'); + } + + const response = await fetch(`${API_URL}`, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + }, + }); + + if (response.ok) { + const data = await response.json(); + return data; + } else { + const errorData = await response.json(); + console.error('Failed to fetch interaction types:', errorData); + throw new Error('Failed to fetch interaction types'); + } + } catch (error) { + console.error('Error fetching interaction types:', error); + throw error; + } +}; \ No newline at end of file diff --git a/src/services/questionnaireService.ts b/src/services/questionnaireService.ts new file mode 100644 index 0000000..5d23065 --- /dev/null +++ b/src/services/questionnaireService.ts @@ -0,0 +1,70 @@ +import {AddQuestionnaire, Questionnaire} from '../types/questionnaire' +const QUESTIONNAIRE_API_URL = 'https://wildlifenl-uu-michi011.apps.cl01.cp.its.uu.nl/questionnaire/'; +const QUESTIONNAIRES_API_URL = 'https://wildlifenl-uu-michi011.apps.cl01.cp.its.uu.nl/questionnaires/'; + + +const getAuthToken = (): string | null => { + return localStorage.getItem('authToken'); +}; + +export const addQuestionnaire = async (questionnaireData: AddQuestionnaire): Promise => { + try { + const token = getAuthToken(); + if (!token) { + throw new Error('No authentication token found'); + } + + const response = await fetch(`${QUESTIONNAIRE_API_URL}`, { + method: 'POST', + headers: { + Accept: 'application/json, application/problem+json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(questionnaireData), + }); + + console.log('Add Experiment response:', response); + + if (response.ok) { + const data = await response.json(); + return data; + } else { + const errorData = await response.json(); + console.error('Failed to add experiment:', errorData); + throw new Error('Failed to add experiment'); + } + } catch (error) { + console.error('Add Experiment error:', error); + throw error; + } +}; + +export const getQuestionnaireByExperimentID = async (id: string): Promise => { + try { + const token = getAuthToken(); + if (!token) { + throw new Error('No authentication token found'); + } + + const response = await fetch(`${QUESTIONNAIRES_API_URL}${id}`, { + method: 'GET', + headers: { + Accept: 'application/json, application/problem+json', + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to fetch messages: ${errorText}`); + } + + const data = await response.json(); + return data as Questionnaire[]; + } catch (error) { + console.error('Error fetching messages:', error); + throw error; + } +}; + diff --git a/src/services/speciesService.ts b/src/services/speciesService.ts new file mode 100644 index 0000000..2da3bd3 --- /dev/null +++ b/src/services/speciesService.ts @@ -0,0 +1,38 @@ +import { Species } from "../types/species"; +const API_URL = 'https://wildlifenl-uu-michi011.apps.cl01.cp.its.uu.nl/species/'; + + +const getAuthToken = (): string | null => { + return localStorage.getItem('authToken'); +}; + +export const getAllSpecies = async (): Promise => { + try { + const token = getAuthToken(); + if (!token) { + throw new Error('No authentication token found'); + } + + const response = await fetch(`${API_URL}`, { + method: 'GET', + headers: { + Accept: 'application/json, application/problem+json', + Authorization: `Bearer ${token}`, + }, + }); + + console.log('Get All Species response:', response); + + if (response.ok) { + const data = await response.json(); + return data; + } else { + const errorData = await response.json(); + console.error('Failed to fetch all species:', errorData); + throw new Error('Failed to fetch all species'); + } + } catch (error) { + console.error('Get All Species error:', error); + throw error; + } +}; \ No newline at end of file diff --git a/src/services/triggerTypeService.ts b/src/services/triggerTypeService.ts new file mode 100644 index 0000000..4b5b2c7 --- /dev/null +++ b/src/services/triggerTypeService.ts @@ -0,0 +1,37 @@ +const API_URL = 'https://wildlifenl-uu-michi011.apps.cl01.cp.its.uu.nl/triggerTypes/'; + + +const getAuthToken = (): string | null => { + return localStorage.getItem('authToken'); +}; + +export const getAllTriggerTypes = async (): Promise => { + try { + const token = getAuthToken(); + if (!token) { + throw new Error('No authentication token found'); + } + + const response = await fetch(`${API_URL}`, { + method: 'GET', + headers: { + Accept: 'application/json, application/problem+json', + Authorization: `Bearer ${token}`, + }, + }); + + console.log('Get All Trigger Types response:', response); + + if (response.ok) { + const data: string[] = await response.json(); + return data; + } else { + const errorData = await response.json(); + console.error('Failed to fetch all trigger types:', errorData); + throw new Error('Failed to fetch all trigger types'); + } + } catch (error) { + console.error('Get All Trigger Types error:', error); + throw error; + } +}; \ No newline at end of file diff --git a/src/styles/Dashboard.css b/src/styles/Dashboard.css index dde39ee..711a8e8 100644 --- a/src/styles/Dashboard.css +++ b/src/styles/Dashboard.css @@ -2,7 +2,9 @@ .dashboard-container { font-family: 'Roboto', sans-serif; position: relative; - overflow-x: hidden; + display: flex; + flex-direction: column; + /* Removed overflow-x to enable scrollbar */ } /* Experiments Title */ @@ -13,6 +15,7 @@ margin-top: 100px; /* Adjust for navbar height */ margin-left: 24px; user-select: none; + flex-shrink: 0; /* Prevent shrinking */ } /* Filters Container */ @@ -22,7 +25,8 @@ align-items: center; margin-top: 20px; margin-left: 5%; - margin-right: 5%; /* Added right margin to align with left */ + margin-right: 5%; /* Align with left margin */ + flex-shrink: 0; /* Prevent shrinking */ } /* Filter Styles */ @@ -155,7 +159,7 @@ padding-right: 40px; /* Prevent text overlap with icon */ font-size: 25px; font-weight: 300; - color: rgba(0, 0, 0, 0); + color: rgba(0, 0, 0, 0); /* Hidden text */ border: 3px solid rgba(0, 0, 0, 0.25); border-radius: 12px; box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.25); @@ -167,6 +171,10 @@ box-shadow: 0 0 5px #27A966; } +.search-input input[type="text"]:focus { + color: #000; /* Reveal text on focus */ +} + .search-icon { position: absolute; right: 10px; @@ -180,12 +188,15 @@ /* Table Container */ .table-container { margin-top: 20px; - margin-left: 5%; /* Added margins to prevent touching edges */ + margin-left: 5%; /* Margins to prevent touching edges */ margin-right: 5%; overflow-x: auto; + overflow-y: auto; /* Enables vertical scrolling */ + height: calc(100vh - 270px); /* Adjusted total offset */ user-select: none; } +/* Experiments Table */ .experiments-table { width: 100%; border-collapse: collapse; @@ -193,7 +204,7 @@ .experiments-table th, .experiments-table td { - padding: 10px; + padding: 12px; text-align: center; } @@ -250,7 +261,6 @@ background-color: rgba(217, 217, 217, 0.40); } - /* Add Experiment Button */ .add-experiment-button { position: fixed; @@ -262,6 +272,7 @@ border: none; padding: 0; cursor: pointer; + z-index: 1000; /* Ensure it appears above other elements */ } .add-experiment-button img { @@ -269,6 +280,7 @@ height: auto; } +/* Loading Container */ .loading-container { display: flex; justify-content: center; @@ -289,3 +301,62 @@ 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } + +/* Scrollbar Styling (Optional) */ +.table-container::-webkit-scrollbar { + width: 8px; /* Increased width for better visibility */ +} + +.table-container::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +.table-container::-webkit-scrollbar-thumb { + background: #888; + border-radius: 4px; +} + +.table-container::-webkit-scrollbar-thumb:hover { + background: #555; +} + +/* Responsive Adjustments (Optional) */ +@media (max-width: 768px) { + .experiments-title { + font-size: 36px; + margin-top: 70px; + margin-left: 16px; + } + + .filter-label { + font-size: 18px; + } + + .dropdown-button { + height: 45px; + font-size: 18px; + } + + .search-input input[type="text"] { + width: 250px; + height: 45px; + font-size: 20px; + } + + .search-icon { + width: 25px; + height: 25px; + } + + .experiments-table th, + .experiments-table td { + padding: 8px; + font-size: 14px; + } + + .add-experiment-button { + width: 90px; + height: 90px; + } +} diff --git a/src/styles/DynamicView.css b/src/styles/DynamicView.css new file mode 100644 index 0000000..8b7a950 --- /dev/null +++ b/src/styles/DynamicView.css @@ -0,0 +1,75 @@ +.dynamic-view-container { + /* Remove fixed width to allow dynamic sizing */ + border: 1px solid #000; + background: rgba(217, 217, 217, 0.0); + padding: 16px; + box-sizing: border-box; + display: flex; + flex-direction: column; + width: fit-content; /* Allows the width to adjust based on content */ + height: fit-content; + max-width: 100%; /* Prevents the container from exceeding the viewport width */ + overflow-y: auto; + max-height: 62vh; +} + +.dynamic-view-field { + margin-bottom: 16px; +} + +.dynamic-view-field-content { + display: flex; + align-items: center; +} + +.dynamic-view-field-name { + width: 550px; /* You can adjust or remove this fixed width */ + color: #000; + font-family: Roboto; + font-size: 25px; + font-weight: 500; + letter-spacing: 0.75px; +} + +.dynamic-view-field-value { + width: 816px; /* You can adjust or remove this fixed width */ + color: #0F0F0F; + font-family: Roboto; + font-size: 25px; + font-weight: 400; + letter-spacing: 0.75px; + margin-left: 16px; +} + +.dynamic-view-line { + width: 100%; /* Makes the line span the full width of the container */ + height: 3px; + background-color: #00000058; + margin-top: 8px; +} + +.dynamic-view-field-value { + height: auto; /* Allows dynamic height based on content */ + overflow-wrap: break-word; + word-break: break-word; + white-space: pre-wrap; +} + +/* Scrollbar Styling (Optional) */ +.dynamic-view-container::-webkit-scrollbar { + width: 8px; +} + +.dynamic-view-container::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +.dynamic-view-container::-webkit-scrollbar-thumb { + background: #888; + border-radius: 4px; +} + +.dynamic-view-container::-webkit-scrollbar-thumb:hover { + background: #555; +} \ No newline at end of file diff --git a/src/styles/Experiment.css b/src/styles/Experiment.css index 5ad50ea..563356f 100644 --- a/src/styles/Experiment.css +++ b/src/styles/Experiment.css @@ -1,4 +1,3 @@ -/* Reset and base styles */ * { margin: 0; padding: 0; @@ -9,11 +8,11 @@ body { font-family: 'Roboto', sans-serif; } +/* Experiment Container */ .experiment-container { - position: relative; - max-width: 1920px; - margin: 0 auto; - width: 100%; + max-width: 1200px; /* Set a max width */ + margin: 0 auto; /* Center the container */ + padding: 20px; /* Add padding for spacing */ } /* Title styling */ @@ -34,330 +33,125 @@ body { /* Experiment Details Component */ .experiment-details-component { - position: absolute; - top: 160px; - left: 12%; - width: 1300px; - height: 470px; - - border: 1px solid #000; - background: rgba(217, 217, 217, 0); - display: flex; - flex-direction: column; -} - -/* Experiment Title Component */ -.experiment-title-comp { - position: relative; - margin-top: 25px; - margin-left: 142px; - width: 1040px; - height: 105px; -} - -.experiment-title-text { - position: absolute; - top: 0; - left: 4px; - width: 250px; - height: 36px; - - color: #000; - font-family: 'Roboto', sans-serif; - font-size: 27px; - font-weight: 300; - line-height: normal; -} - -.experiment-textbar { - position: absolute; - top: 50px; - left: 0; - width: 1040px; - height: 56px; - - border-radius: 12px; - border: 3px solid rgba(0, 0, 0, 0.651); - background: rgba(196, 196, 196, 0); - box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.25); + width: 100%; + margin-top: 160px; } -.experiment-textbar-content { - margin: 13px 15px 17px 15px; - width: 723px; - color: #000; - font-family: 'Roboto', sans-serif; - font-size: 22px; - font-weight: 500; - line-height: normal; -} - -/* Description Component */ -.description-comp { - position: relative; - margin-top: 25px; - margin-left: 142px; - width: 1040px; - height: 185px; -} - -.description-text { - position: absolute; - top: 0; - left: 4px; - width: 250px; - height: 36px; - - color: #000; - font-family: 'Roboto', sans-serif; - font-size: 27px; - font-weight: 300; - line-height: normal; -} - -.description-textbar { - position: absolute; - top: 50px; - left: 0; - width: 1040px; - height: 136px; - - border-radius: 12px; - border: 3px solid rgba(0, 0, 0, 0.651); - background: rgba(196, 196, 196, 0); - box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.25); -} - -.description-textbar-content { - margin: 13px 15px 17px 15px; - width: 985px; - height: 100px; - - color: #000; - font-family: 'Roboto', sans-serif; - font-size: 20px; - font-weight: 500; - line-height: normal; -} - -/* Duration Set Component */ -.duration-set-comp { - position: absolute; - top: 360px; - left: 146px; - width: 543px; - height: 93px; -} - -.duration-text { - position: absolute; - top: 0; - left: 8px; - width: 220px; - height: 23px; - - color: #000; - text-align: center; - font-family: 'Roboto', sans-serif; - font-size: 20px; - font-weight: 400; - letter-spacing: 0.6px; -} - -.time-set { - position: absolute; - top: 43px; - left: 0; +/* Buttons Container */ +.buttons-container { display: flex; + justify-content: center; align-items: center; + gap: 20px; + margin-top: 23px; /* Spacing above the buttons */ } -.time-set-box { - width: 250px; - height: 48px; - - border-radius: 12px; - border: 3px solid rgba(0, 0, 0, 0.651); - background: rgba(196, 196, 196, 0); - box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.25); +/* View Questionnaires Button */ +.view-questionnaires-button { + width: 500px; + height: 91px; + flex-shrink: 0; + border-radius: 19px; + background-color: #27A966; + position: relative; display: flex; align-items: center; - padding-left: 27px; - - color: #000; - font-family: 'Roboto', sans-serif; - font-size: 22px; - font-weight: 500; -} - -.date-separator { - margin-right: 10px; - font-size: 20px; - font-weight: bold; + padding-left: 45px; + padding-right: 111px; /* Ensures the text ends 111px from the right edge */ + border: none; + cursor: pointer; } -/* Location Set Component */ -.location-set-comp { - position: absolute; - top: 360px; - left: 725px; - width: 250px; - height: 93px; /* Updated height to match .duration-set-comp */ +.view-questionnaires-button:hover { + background-color: #1e8a52; } -.location-text { - position: absolute; - top: 0px; - right: 22px; - width: 250px; - height: 45px; - - color: #000; - text-align: center; +/* Text Inside the Button */ +.view-questionnaires-button-text { + color: #FFF; font-family: 'Roboto', sans-serif; - font-size: 20px; - font-weight: 400; - letter-spacing: 0.6px; + font-size: 34px; + font-weight: 500; + letter-spacing: 1.35px; + line-height: normal; } -.location-box { +/* Icon Inside the Button */ +.view-questionnaires-button-icon { + width: 46px; + height: 52px; position: absolute; - top: 43px; - left: 0; - width: 250px; - height: 48px; - - border-radius: 12px; - border: 3px solid rgba(0, 0, 0, 0.651); - background: rgba(196, 196, 196, 0); - box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.25); + right: 30px; + top: 19px; + fill: #FFF; +} +/* View Messages Button */ +.view-messages-button { + width: 415px; + height: 91px; + flex-shrink: 0; + border-radius: 19px; + background-color: #27A966; + position: relative; display: flex; align-items: center; - padding: 0 15px; /* Even padding on both sides */ - overflow: hidden; /* Prevent overflow */ + padding-left: 45px; + padding-right: 111px; + border: none; + cursor: pointer; } -.location-box-text { - flex: 1; - color: #000; - font-family: 'Roboto', sans-serif; - font-size: clamp(16px, 5vw, 25px); - font-weight: 400; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - text-align: left; - line-height: 48px; +.view-messages-button:hover { + background-color: #1e8a52; } -/* Stop Experiment Button */ -.stop-experiment-button { - position: absolute; - top: 395px; - left: 1015px; - width: 250px; - height: 62px; - - border-radius: 12px; - background: #E81E21; - box-shadow: 0px 4px 10px rgba(233, 68, 75, 0.25); - +/* Text Inside the Button */ +.view-messages-button-text { color: #FFF; - text-align: center; font-family: 'Roboto', sans-serif; - font-size: 30px; + font-size: 34px; font-weight: 500; - letter-spacing: 0.9px; - line-height: 62px; /* Center text vertically */ - border: none; - cursor: pointer; + letter-spacing: 1.35px; + line-height: normal; } - -/* Buttons Container */ -.buttons-container { +/* Icon Inside the Button */ +.view-messages-button-icon { + width: 46px; + height: 52px; position: absolute; - top: 600px; /* Adjust as needed */ - width: 100%; - display: flex; - justify-content: space-between; - padding: 38px 300px; + right: 30px; /* 30px from the right edge */ + top: 23px; /* 19px from the top edge */ + fill: #FFF; /* For SVG icons */ } -/* Common Styles for Overview Buttons */ -.overview-button { - display: flex; - flex-direction: column; - width: 445px; - height: 147px; +/* Stop Experiment Button */ +.stop-experiment-button { + width: 468px; + height: 91px; flex-shrink: 0; - border-radius: 14px; - border: 1px solid #000; - background-color: #ffffff; + border-radius: 19px; + background-color: #E81E21; position: relative; - padding: 0; - margin: 0; - cursor: pointer; - text-align: left; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -.overview-button:focus { - outline: none; -} - -.overview-button .button-top { - width: 445px; - height: 64px; - border-radius: 13px 13px 0 0; - background: #27A966; display: flex; align-items: center; - position: relative; + justify-content: center; + border: none; + cursor: pointer; } /* Text Inside the Top Section */ -.button-text { - margin-left: 21px; +.stop-button-text { color: #FFF; font-family: 'Roboto', sans-serif; - font-size: 32px; + font-size: 34px; font-weight: 500; letter-spacing: 0.95px; } -/* Icon Inside the Top Section */ -.questionnaire-overview-button .button-icon { - position: absolute; - top: 13px; - left: 267px; - width: 32px; - height: 36px; -} - -.message-overview-button .button-icon { - position: absolute; - top: 20px; - left: 194px; - width: 27px; - height: 27px; -} - -/* Content Section of the Button */ -.button-content { - position: absolute; - top: 80px; - left: 14px; - width: 350px; - height: 49px; - color: #000; - font-family: 'Roboto', sans-serif; - font-size: 21px; - font-weight: 400; - letter-spacing: 0.63px; -} +.stop-experiment-button:hover { + background-color: #bf181e; +} \ No newline at end of file diff --git a/src/styles/ExperimentCreation.css b/src/styles/ExperimentCreation.css index 9cb5692..762f732 100644 --- a/src/styles/ExperimentCreation.css +++ b/src/styles/ExperimentCreation.css @@ -3,7 +3,7 @@ color: #000; font-size: 50px; font-weight: 700; - margin-top: 90px; /* Adjust for navbar height */ + margin-top: 100px; margin-left: 24px; user-select: none; } @@ -58,7 +58,7 @@ font-family: 'Roboto', sans-serif; font-size: 25px; font-weight: 300; - color: rgba(0, 0, 0, 0.55); + color: rgb(0, 0, 0); margin-left: 140px; /* Align with labels */ margin-top: 5px; /* Space below labels */ } @@ -110,7 +110,7 @@ padding: 0 10px; font-size: 25px; font-weight: 300; - color: rgba(0, 0, 0, 0.55); + color: rgb(0, 0, 0); border: 3px solid rgba(0, 0, 0, 0.25); border-radius: 12px; box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.25); diff --git a/src/styles/Message.css b/src/styles/Message.css new file mode 100644 index 0000000..e609bff --- /dev/null +++ b/src/styles/Message.css @@ -0,0 +1,71 @@ +/* Message.css */ + +/* Reset and Base Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Roboto', sans-serif; +} + +/* Message Container */ +.message-container { + max-width: 1200px; /* Set a max width */ + margin: 0 auto; /* Center the container */ + padding: 20px; +} + +/* Title Styling */ +.message-view-title { + position: absolute; + top: 90px; + left: 25px; + width: 433px; + height: 60px; + color: #000; + font-family: 'Inter', sans-serif; + font-size: 50px; + font-weight: 700; + line-height: normal; +} + +/* Message Details Component */ +.message-details-component { + width: 100%; + margin-top: 160px; +} + +/* Message Not Found Styling */ +.message-not-found { + display: flex; + justify-content: center; + align-items: center; + height: 80vh; + font-size: 24px; + color: #555; +} + +/* Additional Responsive Styles (Optional) */ +@media (max-width: 768px) { + .message-view-title { + font-size: 36px; + width: auto; + left: 10px; + } + + .message-details-component { + margin-top: 140px; + } + + .dynamic-view { + padding: 15px; + } + + .field-name { + display: block; + margin-bottom: 5px; + } +} diff --git a/src/styles/MessageCreation.css b/src/styles/MessageCreation.css index 2c6426e..fcef8af 100644 --- a/src/styles/MessageCreation.css +++ b/src/styles/MessageCreation.css @@ -10,16 +10,14 @@ /* Content Box */ .message-creation-content-box { - position: absolute; - top: 170px; /* Position it below the title */ - left: 50%; - transform: translateX(-50%); width: 1460px; - height: auto; /* Adjust height to fit content */ - flex-shrink: 0; + margin: 20px auto; /* Center the content box */ border: 1px solid #000; background: rgba(217, 217, 217, 0.00); - padding: 20px 0; /* Add padding for inner spacing */ + padding: 20px; + border-radius: 12px; /* Optional: Add rounded corners */ + max-height: calc(100vh - 200px); /* Adjust based on header and margins */ + overflow-y: auto; /* Enable vertical scrolling */ } /* Section Label */ @@ -47,7 +45,7 @@ font-family: 'Roboto', sans-serif; font-size: 25px; font-weight: 300; - color: rgba(0, 0, 0, 0.55); + color: rgba(0, 0, 0,); margin-left: 140px; /* Align with labels */ margin-top: 5px; /* Space below labels */ height: 25px; /* Match the input height */ @@ -56,8 +54,8 @@ /* Flex Container for Sections */ .message-creation-flex-container { display: flex; - align-items: flex-start; - justify-content: space-between; + flex-direction: column; /* Stack items vertically */ + gap: 20px; /* Adjust spacing between sections */ margin-top: 20px; padding: 0 140px; } @@ -147,7 +145,7 @@ /* Dropdown Button */ .message-creation-dropdown-button { - width: 100%; + width: 40%; height: 50px; background: #27A966; color: #fff; @@ -185,7 +183,7 @@ bottom: 55px; left: 0; background-color: #fff; - min-width: 100%; + min-width: 40%; max-height: 200px; overflow-y: auto; box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2); @@ -219,7 +217,7 @@ font-family: 'Roboto', sans-serif; font-size: 25px; font-weight: 300; - color: rgba(0, 0, 0, 0.55); + color: rgb(0, 0, 0); margin-top: 5px; } @@ -227,7 +225,7 @@ .message-creation-submit-button { position: fixed; bottom: 12px; - right: 6px; + right: 20px; width: 106px; height: 102px; background: none; @@ -237,6 +235,25 @@ z-index: 1000; } +/* Custom Scrollbar Styles */ +.message-creation-content-box::-webkit-scrollbar { + width: 6px; /* Increased width for better visibility */ +} + +.message-creation-content-box::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 10px; +} + +.message-creation-content-box::-webkit-scrollbar-thumb { + background: #888; + border-radius: 10px; +} + +.message-creation-content-box::-webkit-scrollbar-thumb:hover { + background: #555; +} + @media (max-width: 1600px) { .message-creation-flex-container { flex-direction: column; diff --git a/src/styles/MessageDashboard.css b/src/styles/MessageDashboard.css index bb8b3eb..a855aa5 100644 --- a/src/styles/MessageDashboard.css +++ b/src/styles/MessageDashboard.css @@ -2,10 +2,9 @@ .message-dashboard-container { font-family: 'Roboto', sans-serif; position: relative; - overflow-x: hidden; - min-height: 100%; display: flex; flex-direction: column; + /* Removed overflow-x and min-height to enable scrollbar */ } /* Messages Title */ @@ -16,6 +15,7 @@ margin-top: 100px; /* Adjust for navbar height */ margin-left: 24px; user-select: none; + flex-shrink: 0; /* Prevent shrinking */ } /* Filters Container */ @@ -26,6 +26,7 @@ margin-top: 20px; margin-left: 5%; margin-right: 5%; + flex-shrink: 0; /* Prevent shrinking */ } /* Filter Styles */ @@ -44,12 +45,17 @@ user-select: none; } -/* InteractionType Filter */ +.interactiontype-filter{ + margin-right: 20px; +} +/* Dropdown Filter */ .dropdown { position: relative; width: 225px; } - +.species-filter .dropdown { + width: 400px; /* Adjust the value as needed */ +} .dropdown-button { width: 100%; height: 50px; @@ -124,7 +130,7 @@ padding-right: 40px; font-size: 25px; font-weight: 300; - color: rgba(0, 0, 0, 0); + color: rgba(0, 0, 0, 0); /* Hidden text */ border: 3px solid rgba(0, 0, 0, 0.25); border-radius: 12px; box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.25); @@ -136,6 +142,10 @@ box-shadow: 0 0 5px #27A966; } +.search-input input[type="text"]:focus { + color: #000; /* Reveal text on focus */ +} + .search-icon { position: absolute; right: 10px; @@ -153,10 +163,11 @@ margin-right: 2%; overflow-x: auto; overflow-y: auto; /* Enables vertical scrolling */ - max-height: calc(100vh - 180px); /* Set desired maximum height */ + height: calc(100vh - 270px); /* Adjusted total offset */ user-select: none; } +/* Messages Table */ .messages-table { width: 100%; border-collapse: collapse; @@ -164,7 +175,7 @@ .messages-table th, .messages-table td { - padding: 10px; + padding: 12px; text-align: center; } @@ -232,6 +243,7 @@ border: none; padding: 0; cursor: pointer; + z-index: 1000; /* Ensure it appears above other elements */ } .add-message-button img { @@ -239,6 +251,7 @@ height: auto; } +/* Loading Container */ .loading-container { display: flex; justify-content: center; @@ -262,7 +275,7 @@ /* Scrollbar Styling (Optional) */ .message-table-container::-webkit-scrollbar { - width: 8px; + width: 8px; } .message-table-container::-webkit-scrollbar-track { @@ -277,4 +290,44 @@ .message-table-container::-webkit-scrollbar-thumb:hover { background: #555; -} \ No newline at end of file +} + +/* Responsive Adjustments (Optional) */ +@media (max-width: 768px) { + .messages-title { + font-size: 36px; + margin-top: 70px; + margin-left: 16px; + } + + .filter-label { + font-size: 18px; + } + + .dropdown-button { + height: 45px; + font-size: 18px; + } + + .search-input input[type="text"] { + width: 250px; + height: 45px; + font-size: 20px; + } + + .search-icon { + width: 25px; + height: 25px; + } + + .messages-table th, + .messages-table td { + padding: 8px; + font-size: 14px; + } + + .add-message-button { + width: 90px; + height: 90px; + } +} diff --git a/src/styles/Questionnaire.css b/src/styles/Questionnaire.css new file mode 100644 index 0000000..3386dfc --- /dev/null +++ b/src/styles/Questionnaire.css @@ -0,0 +1,62 @@ +/* Questionnaire.css */ + +/* Container for the Questionnaire View */ +.questionnaire-view-container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; + font-family: 'Roboto', sans-serif; +} + +/* Title Styling */ +.questionnaire-view-title { + position: absolute; + top: 90px; + left: 25px; + width: 600px; + height: 60px; + color: #000; + font-family: 'Inter', sans-serif; + font-size: 50px; + font-weight: 700; + line-height: normal; +} + +/* Details Component */ +.questionnaire-view-details { + width: 100%; + margin-top: 160px; +} + +/* Not Found Styling */ +.questionnaire-view-not-found { + display: flex; + justify-content: center; + align-items: center; + height: 80vh; + font-size: 24px; + color: #555; + font-family: 'Roboto', sans-serif; +} + +/* Additional Responsive Styles (Optional) */ +@media (max-width: 768px) { + .questionnaire-view-title { + font-size: 36px; + width: auto; + left: 10px; + } + + .questionnaire-view-details { + margin-top: 140px; + } + + .questionnaire-view-container .dynamic-view { + padding: 15px; + } + + .questionnaire-view-container .field-name { + display: block; + margin-bottom: 5px; + } +} diff --git a/src/styles/QuestionnaireCreation.css b/src/styles/QuestionnaireCreation.css new file mode 100644 index 0000000..fe7c964 --- /dev/null +++ b/src/styles/QuestionnaireCreation.css @@ -0,0 +1,149 @@ +/* Container */ +.questionnaire-creation-container { + overflow: hidden; +} + +/* Page Title */ +.questionnaire-creation-page-title { + color: #000; + font-size: 50px; + font-weight: 700; + margin-top: 100px; + margin-left: 24px; + user-select: none; +} + +/* Content Box */ +.questionnaire-creation-content-box { + position: absolute; + top: 170px; + left: 50%; + transform: translateX(-50%); + width: 1460px; + height: auto; + flex-shrink: 0; + border: 1px solid #000; + background: rgba(217, 217, 217, 0.0); + padding: 20px 0; +} + +/* Section Label */ +.questionnaire-creation-section-label { + display: block; + width: 250px; + height: 32px; + color: #000; + font-family: 'Roboto', sans-serif; + font-size: 27px; + font-weight: 300; + line-height: normal; + margin-left: 140px; + margin-top: 15px; +} + +/* Text Input */ +.questionnaire-creation-text-input { + width: calc(100% - 280px); + border-radius: 12px; + border: 3px solid rgba(0, 0, 0, 0.25); + background: rgba(196, 196, 196, 0.0); + box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.25); + padding: 22px 41px; + font-family: 'Roboto', sans-serif; + font-size: 25px; + font-weight: 300; + color: rgb(0, 0, 0); + margin-left: 140px; + margin-top: 5px; +} + +/* Dropdown */ +.questionnaire-creation-dropdown { + position: relative; + width: calc(100% - 280px); + margin-left: 140px; + margin-top: 5px; +} + +.questionnaire-creation-dropdown-button { + width: 30%; + height: 50px; + background: #27A966; + color: #fff; + font-size: 20px; + font-weight: 500; + border: none; + border-radius: 12px; + box-shadow: 0px 4px 10px rgba(39, 169, 102, 0.25); + display: flex; + align-items: center; + justify-content: center; + position: relative; + user-select: none; + cursor: pointer; + padding: 0 15px; +} + +.questionnaire-creation-dropdown-button:focus { + outline: none; +} + +.questionnaire-creation-dropdown-icon { + position: absolute; + right: 15px; + width: 17px; + height: 11px; + transition: transform 0.2s; +} + +.questionnaire-creation-dropdown.open .questionnaire-creation-dropdown-icon { + transform: rotate(180deg); +} + +.questionnaire-creation-dropdown-content { + display: none; + position: absolute; + top: 55px; + left: 0; + background-color: #fff; + min-width: 30%; + max-height: 200px; + overflow-y: auto; + box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.2); + z-index: 1; + border-radius: 12px; +} + +.questionnaire-creation-dropdown.open .questionnaire-creation-dropdown-content { + display: block; +} + +.questionnaire-creation-dropdown-item { + padding: 10px; + font-size: 18px; + cursor: pointer; +} + +.questionnaire-creation-dropdown-item:hover { + background-color: #f1f1f1; +} + +/* Submit Button */ +.questionnaire-creation-submit-button { + position: fixed; + bottom: 12px; + right: 6px; + width: 106px; + height: 102px; + background: none; + border: none; + padding: 0; + cursor: pointer; + z-index: 1000; +} + +@media (max-width: 1600px) { + .questionnaire-creation-content-box { + width: 90%; + } +} diff --git a/src/styles/QuestionnaireDashboard.css b/src/styles/QuestionnaireDashboard.css new file mode 100644 index 0000000..975eb63 --- /dev/null +++ b/src/styles/QuestionnaireDashboard.css @@ -0,0 +1,328 @@ +/* Questionnaire Dashboard Container */ +.questionnaire-dashboard-container { + font-family: 'Roboto', sans-serif; + position: relative; + display: flex; + flex-direction: column; +} + +/* Questionnaires Title */ +.questionnaires-title { + color: #000; + font-size: 50px; + font-weight: 700; + margin-top: 100px; + margin-left: 24px; + user-select: none; + flex-shrink: 0; +} + +/* Filters Container */ +.questionnaire-filters-container { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 20px; + margin-left: 5%; + margin-right: 5%; + flex-shrink: 0; +} + +/* Filter Styles */ +.questionnaire-filter { + display: flex; + align-items: center; + margin-bottom: 20px; +} + +/* Filter Labels */ +.questionnaire-filter-label { + color: #000; + font-size: 20px; + font-weight: 500; + margin-right: 10px; + user-select: none; +} + +/* Dropdown */ +.dropdown { + position: relative; + width: 225px; +} + +.dropdown-button { + width: 100%; + height: 50px; + background: #27A966; + color: #fff; + font-size: 20px; + font-weight: 500; + border: none; + border-radius: 12px; + box-shadow: 0px 4px 10px rgba(233, 68, 75, 0.25); + display: flex; + align-items: center; + justify-content: center; + position: relative; + user-select: none; + cursor: pointer; +} + +.dropdown-icon { + position: absolute; + right: 15px; + width: 17px; + height: 11px; + transition: transform 0.2s; + transform: rotate(180deg); +} + +.dropdown-icon-hover { + transform: rotate(0deg); +} + +.dropdown-content { + display: none; + position: absolute; + background-color: #fff; + min-width: 100%; + max-height: 200px; + overflow-y: auto; + box-shadow: 0px 8px 16px rgba(0,0,0,0.2); + z-index: 1; + border-radius: 12px; + margin-top: 5px; +} + +.dropdown.open .dropdown-content { + display: block; +} + +.dropdown-item { + padding: 10px; + font-size: 18px; + cursor: pointer; +} + +.dropdown-item:hover { + background-color: #f1f1f1; +} + +/* Search Filter */ +.search-filter { + position: relative; +} + +.search-input { + position: relative; +} + +.search-input input[type="text"] { + width: 350px; + height: 50px; + padding: 0 10px; + padding-right: 40px; + font-size: 25px; + font-weight: 300; + color: rgba(0, 0, 0, 0); + border: 3px solid rgba(0, 0, 0, 0.25); + border-radius: 12px; + box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.25); + transition: border-color 0.2s, box-shadow 0.2s; +} + +.search-input input[type="text"]:hover:not(:focus) { + border-color: #27A966; + box-shadow: 0 0 5px #27A966; +} + +.search-input input[type="text"]:focus { + color: #000; +} + +.search-icon { + position: absolute; + right: 10px; + top: 10px; + width: 30px; + height: 30px; + user-select: none; + pointer-events: none; +} + +/* Table Container */ +.questionnaire-table-container { + margin-top: 20px; + margin-left: 2%; + margin-right: 2%; + overflow-x: auto; + overflow-y: auto; + height: calc(100vh - 270px); + user-select: none; +} + +/* Questionnaires Table */ +.questionnaires-table { + width: 100%; + border-collapse: collapse; +} + +.questionnaires-table th, +.questionnaires-table td { + padding: 12px; + text-align: center; +} + +.questionnaires-table th { + background: #D9D9D9; + border: 1px solid #000; + font-size: 20px; + font-weight: 500; + letter-spacing: 0.6px; + position: sticky; + top: 0; + cursor: pointer; +} + +.questionnaires-table td { + font-size: 15px; + font-weight: 400; + letter-spacing: 0.45px; + cursor: pointer; +} + +.questionnaires-table th, +.questionnaires-table td { + border-bottom: 1px solid rgba(0, 0, 0, 0.30); +} + +.sort-icon { + margin-left: 5px; + width: 10px; + height: 10px; + transition: transform 0.2s; + pointer-events: none; +} + +.sort-ascending { + transform: rotate(180deg); +} + +.sort-descending { + transform: rotate(0deg); +} + +/* Hover Effect for Questionnaire Rows */ +.questionnaires-table tbody tr:hover { + background-color: rgba(217, 217, 217, 0.8); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); +} + +/* Row Styles */ +.row-even { + background-color: rgba(217, 217, 217, 0.60); +} + +.row-odd { + background-color: rgba(217, 217, 217, 0.40); +} + +/* Add Questionnaire Button */ +.add-questionnaire-button { + position: fixed; + bottom: 12px; + right: 6px; + width: 106px; + height: 102px; + background: none; + border: none; + padding: 0; + cursor: pointer; + z-index: 1000; +} + +.add-questionnaire-button img { + width: 100%; + height: auto; +} + +/* Loading Container */ +.loading-container { + display: flex; + justify-content: center; + align-items: center; + height: 300px; +} + +.spinner { + border: 16px solid #f3f3f3; + border-top: 16px solid #27A966; + border-radius: 50%; + width: 120px; + height: 120px; + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Scrollbar Styling */ +.questionnaire-table-container::-webkit-scrollbar { + width: 8px; +} + +.questionnaire-table-container::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +.questionnaire-table-container::-webkit-scrollbar-thumb { + background: #888; + border-radius: 4px; +} + +.questionnaire-table-container::-webkit-scrollbar-thumb:hover { + background: #555; +} + +/* Responsive Adjustments */ +@media (max-width: 768px) { + .questionnaires-title { + font-size: 36px; + margin-top: 70px; + margin-left: 16px; + } + + .filter-label { + font-size: 18px; + } + + .dropdown-button { + height: 45px; + font-size: 18px; + } + + .search-input input[type="text"] { + width: 250px; + height: 45px; + font-size: 20px; + } + + .search-icon { + width: 25px; + height: 25px; + } + + .questionnaires-table th, + .questionnaires-table td { + padding: 8px; + font-size: 14px; + } + + .add-questionnaire-button { + width: 90px; + height: 90px; + } +} diff --git a/src/types/answer.ts b/src/types/answer.ts new file mode 100644 index 0000000..cf6bb5f --- /dev/null +++ b/src/types/answer.ts @@ -0,0 +1,6 @@ +export interface Answer{ + index: number; + nextQuestionID: string; + questionId: string; + text: string; +} \ No newline at end of file diff --git a/src/types/experiment.ts b/src/types/experiment.ts index d436234..9bab5aa 100644 --- a/src/types/experiment.ts +++ b/src/types/experiment.ts @@ -2,7 +2,6 @@ import { LivingLab } from './livinglab'; import { User } from './user'; export interface Experiment { - $schema: string; ID: string; name: string; description: string; diff --git a/src/types/interactiontype.ts b/src/types/interactiontype.ts new file mode 100644 index 0000000..fe1a132 --- /dev/null +++ b/src/types/interactiontype.ts @@ -0,0 +1,5 @@ +export interface InteractionType { + ID: string; + description: string; + name: string; +} \ No newline at end of file diff --git a/src/types/message.ts b/src/types/message.ts index 9ab99dc..cd074ab 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -1,12 +1,17 @@ +import { Experiment } from "./experiment"; +import { Answer } from "./answer"; +import { Species } from "./species"; + export interface Message{ - $schema: string; - answerID: string; + ID: string; + answer: Answer; encounterMeters: number; encounterMinutes: number; - experimentID: string; + experiment: Experiment; name: string; severity: number; - speciesID: number; + species: Species; text: string; trigger: string; + activity: number; } \ No newline at end of file diff --git a/src/types/question.ts b/src/types/question.ts new file mode 100644 index 0000000..e36459a --- /dev/null +++ b/src/types/question.ts @@ -0,0 +1,13 @@ +import { Answer } from './answer'; + +export interface Question { + ID: string; + allowMultipleResponse: boolean; + allowOpenResponse: boolean; + options: string[]; + answers: Array; + description: string; + index: number; + openResponseFormat: string; + text: string; +} \ No newline at end of file diff --git a/src/types/questionnaire.ts b/src/types/questionnaire.ts new file mode 100644 index 0000000..a9a049b --- /dev/null +++ b/src/types/questionnaire.ts @@ -0,0 +1,19 @@ +import { Experiment } from "./experiment"; +import { InteractionType } from "./interactiontype"; +import { Question } from "./question"; + +export interface Questionnaire{ + ID: string; + experiment: Experiment; + identifier: string; + interactionType: InteractionType + name: string; + questions: Array; +} + +export interface AddQuestionnaire{ + experimentID: string; + identifier: string; + interactionTypeID: string; + name: string; +} \ No newline at end of file diff --git a/src/types/species.ts b/src/types/species.ts new file mode 100644 index 0000000..50201cd --- /dev/null +++ b/src/types/species.ts @@ -0,0 +1,5 @@ +export interface Species { + ID: string; + name: string; + commonName: string; +} \ No newline at end of file