From 691b3f0d8258cd33589ecc0ff058fb21abf221c1 Mon Sep 17 00:00:00 2001 From: John Manhart Date: Sat, 23 Aug 2025 19:13:41 -0700 Subject: [PATCH 1/6] Adding in an all votes table with sorting --- src/components/VoteTable.css | 131 +++++++++++++++++++++++++++++++++++ src/components/VoteTable.js | 119 +++++++++++++++++++++++++++++++ src/pages/ManageVotes.js | 17 ++++- 3 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 src/components/VoteTable.css create mode 100644 src/components/VoteTable.js diff --git a/src/components/VoteTable.css b/src/components/VoteTable.css new file mode 100644 index 0000000..1444858 --- /dev/null +++ b/src/components/VoteTable.css @@ -0,0 +1,131 @@ +.vote-table-container { + margin-bottom: 2rem; +} + +.vote-table-container h3 { + margin-bottom: 1rem; + color: var(--color-gray100, #1d1127); + font-size: 1.75rem; + font-weight: 600; +} + +.vote-table-wrapper { + overflow-x: auto; + border-radius: 8px; + border: 1px solid var(--color-gray400); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + min-width: 1200px; /* Ensure minimum comfortable width on desktop */ +} + +.vote-table { + width: 100%; + min-width: 1000px; /* Match wrapper minimum width */ + border-collapse: collapse; + background: white; + font-size: 1.1rem; +} + +.vote-table th, +.vote-table td { + padding: 1rem 1.5rem; /* More comfortable padding on desktop */ + text-align: left; + border-bottom: 1px solid var(--color-gray400); +} + +.vote-table th { + background: var(--color-gray400); + color: var(--color-gray100); + font-weight: 600; + font-size: 1.2rem; + position: sticky; + top: 0; + z-index: 10; +} + +.vote-table th:first-child { + border-top-left-radius: 8px; +} + +.vote-table th:last-child { + border-top-right-radius: 8px; +} + +.vote-table tbody tr:hover { + background: var(--color-gray400); +} + +.vote-table tbody tr:last-child td { + border-bottom: none; +} + +.sortable-header { + cursor: pointer; + user-select: none; + transition: background-color 0.2s ease; +} + +.sortable-header:hover { + background: var(--color-gray300) !important; +} + +.project-name a { + color: var(--color-blurple); + text-decoration: none; + font-weight: 500; + font-size: 1.1rem; +} + +.project-name a:hover { + text-decoration: underline; +} + +.vote-count { + text-align: center; + font-weight: 500; + color: var(--color-gray200); + font-size: 1.1rem; +} + +.total-votes { + text-align: center; + background: var(--color-gray400); + font-weight: 600; + color: var(--color-gray100); + font-size: 1.1rem; +} + +.total-votes strong { + color: var(--color-blurple); + font-size: 1.2rem; +} + +/* Column width adjustments for better desktop experience */ +.vote-table .project-name { + min-width: 250px; /* Project names get more space */ +} + +.vote-table .vote-count { + min-width: 120px; /* Vote counts get comfortable width */ + text-align: center; +} + +.vote-table .total-votes { + min-width: 140px; /* Total column gets more space */ + text-align: center; + background: var(--color-gray400); + font-weight: 600; + color: var(--color-gray100); +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .vote-table th, + .vote-table td { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; + } + + .vote-table-container h3 { + font-size: 1.25rem; + } +} diff --git a/src/components/VoteTable.js b/src/components/VoteTable.js new file mode 100644 index 0000000..5081f05 --- /dev/null +++ b/src/components/VoteTable.js @@ -0,0 +1,119 @@ +import React, {useState} from 'react'; +import PropTypes from 'prop-types'; + +const VoteTable = ({data, awardCategories, projects, year}) => { + const [sortConfig, setSortConfig] = useState({column: 'total', direction: 'desc'}); + + // Transform data for table display + const tableData = Object.keys(projects).map((projectKey) => { + const project = projects[projectKey]; + const row = {projectKey, project, total: 0}; + + // Add vote counts for each category + Object.keys(awardCategories).forEach((categoryKey) => { + const categoryVotes = data[categoryKey] || {}; + const voteCount = categoryVotes[projectKey] || 0; + row[categoryKey] = voteCount; + row.total += voteCount; + }); + + return row; + }); + + // Sort function + const sortData = (data, config) => { + return [...data].sort((a, b) => { + let aValue = config.column === 'project' ? a.project.name : a[config.column]; + let bValue = config.column === 'project' ? b.project.name : b[config.column]; + + if (config.column === 'project') { + return config.direction === 'asc' + ? aValue.localeCompare(bValue) + : bValue.localeCompare(aValue); + } + + if (config.direction === 'asc') { + return aValue - bValue; + } + return bValue - aValue; + }); + }; + + // Handle column sorting + const handleSort = (column) => { + setSortConfig((prevConfig) => ({ + column, + direction: + prevConfig.column === column && prevConfig.direction === 'asc' ? 'desc' : 'asc', + })); + }; + + // Get sort indicator + const getSortIndicator = (column) => { + if (sortConfig.column !== column) return '↕'; + return sortConfig.direction === 'asc' ? '↑' : '↓'; + }; + + const sortedData = sortData(tableData, sortConfig); + + return ( +
+

Vote Summary Table

+
+ + + + + {Object.keys(awardCategories).map((categoryKey) => ( + + ))} + + + + + {sortedData.map(({projectKey, project, total, ...categoryVotes}) => ( + + + {Object.keys(awardCategories).map((categoryKey) => ( + + ))} + + + ))} + +
handleSort('project')}> + Project Name {getSortIndicator('project')} + handleSort(categoryKey)} + > + {awardCategories[categoryKey].name} {getSortIndicator(categoryKey)} + handleSort('total')}> + Total Votes {getSortIndicator('total')} +
+ + {project.name} + + + {categoryVotes[categoryKey] || 0} + + {total} +
+
+
+ ); +}; + +VoteTable.propTypes = { + data: PropTypes.object.isRequired, + awardCategories: PropTypes.object.isRequired, + projects: PropTypes.object.isRequired, + year: PropTypes.string.isRequired, +}; + +export default VoteTable; diff --git a/src/pages/ManageVotes.js b/src/pages/ManageVotes.js index d469108..6e9a02e 100644 --- a/src/pages/ManageVotes.js +++ b/src/pages/ManageVotes.js @@ -6,6 +6,8 @@ import {compose} from 'redux'; import {firebaseConnect, isLoaded, pathToJS, dataToJS} from 'react-redux-firebase'; import {mapObject, orderedPopulatedDataToJS} from '../helpers'; +import VoteTable from '../components/VoteTable'; +import '../components/VoteTable.css'; class ManageAwardCategories extends Component { static propTypes = { @@ -48,6 +50,12 @@ class ManageAwardCategories extends Component { return (
+ {Object.keys(votesByProjectAndCategory).map((categoryKey) => { let votesByProject = votesByProjectAndCategory[categoryKey]; @@ -67,7 +75,14 @@ class ManageAwardCategories extends Component { let project = projects[projectKey]; return (
  • - {project.name} • {votesByProject[projectKey]} + + {project.name} + {' '} + • {votesByProject[projectKey]}
  • ); })} From dba699f9cb83d69d538daad64c2e49284c1499c5 Mon Sep 17 00:00:00 2001 From: John Manhart Date: Sat, 23 Aug 2025 19:27:22 -0700 Subject: [PATCH 2/6] Adding in some more anlytics and table styles --- src/components/VoteAnalytics.css | 78 ++++++++++++++++++++++++++++++++ src/components/VoteAnalytics.js | 71 +++++++++++++++++++++++++++++ src/components/VoteTable.css | 31 +++++++++++++ src/components/VoteTable.js | 24 ++++++++++ src/pages/ManageVotes.js | 15 ++++++ 5 files changed, 219 insertions(+) create mode 100644 src/components/VoteAnalytics.css create mode 100644 src/components/VoteAnalytics.js diff --git a/src/components/VoteAnalytics.css b/src/components/VoteAnalytics.css new file mode 100644 index 0000000..ed953b0 --- /dev/null +++ b/src/components/VoteAnalytics.css @@ -0,0 +1,78 @@ +.vote-analytics-container { + margin-bottom: 2rem; +} + +.vote-analytics-container h3 { + margin-bottom: 1.5rem; + color: var(--color-gray100, #1d1127); + font-size: 1.75rem; + font-weight: 600; +} + +.analytics-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-bottom: 1rem; +} + +.analytics-card { + background: white; + border: 1px solid var(--color-gray400); + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + transition: box-shadow 0.2s ease, transform 0.05s ease; +} + +.analytics-card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transform: translateY(-1px); +} + +.analytics-value { + font-size: 2.5rem; + font-weight: 700; + color: var(--color-blurple); + margin-bottom: 0.5rem; + line-height: 1; +} + +.analytics-label { + font-size: 1rem; + font-weight: 600; + color: var(--color-gray100); + line-height: 1.3; +} + +.analytics-subtext { + font-size: 0.85rem; + font-weight: 400; + color: var(--color-gray300); + margin-top: 0.25rem; + opacity: 0.8; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .analytics-grid { + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + } + + .analytics-card { + padding: 1.25rem; + } + + .analytics-value { + font-size: 2rem; + } + + .analytics-label { + font-size: 0.9rem; + } + + .analytics-subtext { + font-size: 0.8rem; + } +} diff --git a/src/components/VoteAnalytics.js b/src/components/VoteAnalytics.js new file mode 100644 index 0000000..844a1cd --- /dev/null +++ b/src/components/VoteAnalytics.js @@ -0,0 +1,71 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const VoteAnalytics = ({data, awardCategories, projects, userCount}) => { + // Calculate total votes cast across all categories + const totalVotesCast = Object.values(data).reduce((total, categoryVotes) => { + return total + Object.values(categoryVotes).reduce((sum, votes) => sum + votes, 0); + }, 0); + + // Calculate total possible votes (each user gets 5 votes) + const totalPossibleVotes = userCount * 5; + + // Calculate participation percentage + const participationPercentage = + totalPossibleVotes > 0 ? ((totalVotesCast / totalPossibleVotes) * 100).toFixed(1) : 0; + + // Calculate votes per category + const votesPerCategory = Object.keys(awardCategories) + .map((categoryKey) => { + const categoryVotes = data[categoryKey] || {}; + const total = Object.values(categoryVotes).reduce((sum, votes) => sum + votes, 0); + return { + name: awardCategories[categoryKey].name, + total, + key: categoryKey, + }; + }) + .sort((a, b) => b.total - a.total); + + // Find most active category + const mostActiveCategory = votesPerCategory[0]; + + return ( +
    +

    Vote Analytics Dashboard

    +
    +
    +
    {totalVotesCast}
    +
    Total Votes Cast
    +
    + +
    +
    {participationPercentage}%
    +
    + Participation Rate +
    + {totalVotesCast} / {totalPossibleVotes} possible votes +
    +
    +
    + +
    +
    {mostActiveCategory?.total || 0}
    +
    + Most Active Category +
    {mostActiveCategory?.name || 'N/A'}
    +
    +
    +
    +
    + ); +}; + +VoteAnalytics.propTypes = { + data: PropTypes.object.isRequired, + awardCategories: PropTypes.object.isRequired, + projects: PropTypes.object.isRequired, + userCount: PropTypes.number.isRequired, +}; + +export default VoteAnalytics; diff --git a/src/components/VoteTable.css b/src/components/VoteTable.css index 1444858..359f944 100644 --- a/src/components/VoteTable.css +++ b/src/components/VoteTable.css @@ -99,6 +99,37 @@ font-size: 1.2rem; } +/* Category Totals Row Styling */ +.category-totals-row { + background: var(--color-gray400); + border-top: 2px solid var(--color-blurple); + border-bottom: 2px solid var(--color-blurple); +} + +.category-totals-row td { + padding: 1rem 1.5rem; + font-weight: 600; + color: var(--color-gray100); + text-align: center; +} + +.category-totals-label { + text-align: left !important; + color: var(--color-gray100) !important; + font-size: 1rem; +} + +.category-total { + font-size: 1.2rem; + color: var(--color-blurple) !important; +} + +.grand-total { + background: var(--color-blurple) !important; + color: white !important; + font-size: 1.3rem; +} + /* Column width adjustments for better desktop experience */ .vote-table .project-name { min-width: 250px; /* Project names get more space */ diff --git a/src/components/VoteTable.js b/src/components/VoteTable.js index 5081f05..3e1e36b 100644 --- a/src/components/VoteTable.js +++ b/src/components/VoteTable.js @@ -54,6 +54,15 @@ const VoteTable = ({data, awardCategories, projects, year}) => { return sortConfig.direction === 'asc' ? '↑' : '↓'; }; + // Calculate category totals for summary row + const categoryTotals = Object.keys(awardCategories).map((categoryKey) => { + const categoryVotes = data[categoryKey] || {}; + return Object.values(categoryVotes).reduce((sum, votes) => sum + votes, 0); + }); + + // Calculate grand total + const grandTotal = categoryTotals.reduce((sum, total) => sum + total, 0); + const sortedData = sortData(tableData, sortConfig); return ( @@ -80,6 +89,21 @@ const VoteTable = ({data, awardCategories, projects, year}) => { + + + + CATEGORY TOTALS + + {categoryTotals.map((total, index) => ( + + {total} + + ))} + + {grandTotal} + + + {sortedData.map(({projectKey, project, total, ...categoryVotes}) => ( diff --git a/src/pages/ManageVotes.js b/src/pages/ManageVotes.js index 6e9a02e..120cd1e 100644 --- a/src/pages/ManageVotes.js +++ b/src/pages/ManageVotes.js @@ -8,6 +8,8 @@ import {firebaseConnect, isLoaded, pathToJS, dataToJS} from 'react-redux-firebas import {mapObject, orderedPopulatedDataToJS} from '../helpers'; import VoteTable from '../components/VoteTable'; import '../components/VoteTable.css'; +import VoteAnalytics from '../components/VoteAnalytics'; +import '../components/VoteAnalytics.css'; class ManageAwardCategories extends Component { static propTypes = { @@ -50,6 +52,12 @@ class ManageAwardCategories extends Component { return (
    + ({ auth: pathToJS(firebase, 'auth'), awardCategories: dataToJS(firebase, 'awardCategories'), projects: dataToJS(firebase, 'projects'), voteList: orderedPopulatedDataToJS(firebase, 'voteList', keyPopulates), + userList: dataToJS(firebase, 'userList'), })) )(ManageAwardCategories); From 3ded44b8d3172cd301d3e296e29c90d66fa16ac5 Mon Sep 17 00:00:00 2001 From: John Manhart Date: Sat, 23 Aug 2025 19:56:32 -0700 Subject: [PATCH 3/6] Adding in some more styling to the table --- src/components/VoteTable.css | 50 ++++++++++++- src/components/VoteTable.js | 137 +++++++++++++++++++++++++---------- src/index.css | 29 ++++++++ src/pages/App.css | 6 ++ src/pages/ManageVotes.js | 2 +- 5 files changed, 184 insertions(+), 40 deletions(-) diff --git a/src/components/VoteTable.css b/src/components/VoteTable.css index 359f944..05a742f 100644 --- a/src/components/VoteTable.css +++ b/src/components/VoteTable.css @@ -1,5 +1,14 @@ .vote-table-container { margin-bottom: 2rem; + width: 95%; + margin-left: auto; + margin-right: auto; +} + +/* Admin pages - make table wider */ +.admin-page .vote-table-container { + width: 95%; + max-width: none; } .vote-table-container h3 { @@ -51,7 +60,19 @@ } .vote-table tbody tr:hover { - background: var(--color-gray400); + background-color: #f8f9fa; +} + +/* Top 5 rows styling - visually distinguish top performers */ +.vote-table tbody tr:nth-child(-n + 5) { + background-color: #f0f8ff !important; /* Light blue background */ + border-left: 4px solid #007bff !important; /* Blue left border */ + font-weight: 500; /* Slightly bolder text */ +} + +/* Ensure hover effect still works on top 5 rows */ +.vote-table tbody tr:nth-child(-n + 5):hover { + background-color: #e3f2fd !important; /* Slightly darker blue on hover */ } .vote-table tbody tr:last-child td { @@ -160,3 +181,30 @@ font-size: 1.25rem; } } + +.vote-table-header { + cursor: pointer; + user-select: none; + transition: background-color 0.2s ease; +} + +.vote-table-header:hover { + background-color: #e9ecef; +} + +/* Active category column styling */ +.vote-table-header.active-category { + background-color: #007bff !important; + color: white !important; + position: relative; +} + +.vote-table-header.active-category::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background-color: #0056b3; +} diff --git a/src/components/VoteTable.js b/src/components/VoteTable.js index 3e1e36b..d88df13 100644 --- a/src/components/VoteTable.js +++ b/src/components/VoteTable.js @@ -2,7 +2,16 @@ import React, {useState} from 'react'; import PropTypes from 'prop-types'; const VoteTable = ({data, awardCategories, projects, year}) => { - const [sortConfig, setSortConfig] = useState({column: 'total', direction: 'desc'}); + const [sortConfig, setSortConfig] = useState({ + key: null, + direction: 'desc', + }); + + // Set the first category as default active + const [activeCategory, setActiveCategory] = useState(() => { + const firstCategoryKey = Object.keys(awardCategories)[0]; + return firstCategoryKey || null; + }); // Transform data for table display const tableData = Object.keys(projects).map((projectKey) => { @@ -40,18 +49,71 @@ const VoteTable = ({data, awardCategories, projects, year}) => { }; // Handle column sorting - const handleSort = (column) => { - setSortConfig((prevConfig) => ({ - column, - direction: - prevConfig.column === column && prevConfig.direction === 'asc' ? 'desc' : 'asc', - })); + const handleSort = (key) => { + // If clicking the same column, toggle between showing all vs top 5 + if (activeCategory === key) { + setActiveCategory(null); + setSortConfig({key: null, direction: 'desc'}); + return; + } + + // Set new active category and show top 5 for that category + setActiveCategory(key); + setSortConfig({ + key, + direction: 'desc', + }); + }; + + // Filter data based on active category + const getFilteredData = () => { + // Always show all projects + const allProjects = Object.keys(projects).map((projectKey) => { + const project = projects[projectKey]; + const row = {projectKey, projectName: project.name}; + + // Calculate total votes for this project + let totalVotes = 0; + Object.keys(awardCategories).forEach((categoryKey) => { + const votes = data[categoryKey]?.[projectKey] || 0; + row[categoryKey] = votes; + totalVotes += votes; + }); + + row.totalVotes = totalVotes; + return row; + }); + + return allProjects; + }; + + const getSortedData = () => { + let sortableData = getFilteredData(); + + if (activeCategory) { + // Sort by the active category (highest to lowest) + sortableData.sort((a, b) => { + return (b[activeCategory] || 0) - (a[activeCategory] || 0); + }); + } + + return sortableData; }; // Get sort indicator - const getSortIndicator = (column) => { - if (sortConfig.column !== column) return '↕'; - return sortConfig.direction === 'asc' ? '↑' : '↓'; + const getSortIndicator = (key) => { + if (activeCategory === key) { + return ' ↓'; // Show down arrow for active category (sorted highest to lowest) + } + return ''; + }; + + const getHeaderClassName = (key) => { + let className = 'vote-table-header'; + if (activeCategory === key) { + className += ' active-category'; + } + return className; }; // Calculate category totals for summary row @@ -63,67 +125,66 @@ const VoteTable = ({data, awardCategories, projects, year}) => { // Calculate grand total const grandTotal = categoryTotals.reduce((sum, total) => sum + total, 0); - const sortedData = sortData(tableData, sortConfig); + const sortedData = getSortedData(); return (
    -

    Vote Summary Table

    +

    Vote Summary by Category

    - + {Object.keys(awardCategories).map((categoryKey) => ( ))} - + + {/* Category totals row */} - - {categoryTotals.map((total, index) => ( - - ))} - + ); + })} + - - - {sortedData.map(({projectKey, project, total, ...categoryVotes}) => ( - + {sortedData.map((row) => ( + {Object.keys(awardCategories).map((categoryKey) => ( ))} - + ))} diff --git a/src/index.css b/src/index.css index 8653f3b..1f99df4 100644 --- a/src/index.css +++ b/src/index.css @@ -399,3 +399,32 @@ input { .Project-details-summary { overflow: hidden; } + +/* Responsive adjustments */ +@media (max-width: 768px) { + .vote-table th, + .vote-table td { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; + } + + .vote-table-container h3 { + font-size: 1.25rem; + } +} + +/* Admin page full width - simple and reliable */ +.admin-page { + width: 100vw !important; + max-width: 100vw !important; + margin-left: calc(-50vw + 50%) !important; + margin-right: calc(-50vw + 50%) !important; + padding: 0 32px !important; + box-sizing: border-box !important; +} + +/* Ensure all admin page content uses full width */ +.admin-page > * { + max-width: none !important; + width: 100% !important; +} diff --git a/src/pages/App.css b/src/pages/App.css index 03ef1f6..151e976 100644 --- a/src/pages/App.css +++ b/src/pages/App.css @@ -4,6 +4,12 @@ min-height: 100vh; } +/* Admin pages full width override - simplified */ +.admin-page { + width: 100% !important; + max-width: none !important; +} + .App-header { padding: 20px 0; margin: 0; diff --git a/src/pages/ManageVotes.js b/src/pages/ManageVotes.js index 120cd1e..70a4784 100644 --- a/src/pages/ManageVotes.js +++ b/src/pages/ManageVotes.js @@ -51,7 +51,7 @@ class ManageAwardCategories extends Component { }); return ( -
    +
    Date: Sat, 23 Aug 2025 20:13:13 -0700 Subject: [PATCH 4/6] Adding in more table styles --- src/components/VoteTable.css | 124 +++++++++++++++++++++-------------- src/components/VoteTable.js | 47 +++++++++++-- 2 files changed, 114 insertions(+), 57 deletions(-) diff --git a/src/components/VoteTable.css b/src/components/VoteTable.css index 05a742f..f2d3acf 100644 --- a/src/components/VoteTable.css +++ b/src/components/VoteTable.css @@ -63,81 +63,79 @@ background-color: #f8f9fa; } -/* Top 5 rows styling - visually distinguish top performers */ -.vote-table tbody tr:nth-child(-n + 5) { +/* Top 5 rows styling - rows 2-6 (category totals + top 5 projects) */ +.vote-table tbody tr:nth-child(n + 2):nth-child(-n + 6) { background-color: #f0f8ff !important; /* Light blue background */ - border-left: 4px solid #007bff !important; /* Blue left border */ font-weight: 500; /* Slightly bolder text */ } -/* Ensure hover effect still works on top 5 rows */ -.vote-table tbody tr:nth-child(-n + 5):hover { - background-color: #e3f2fd !important; /* Slightly darker blue on hover */ +/* Top 5 project names - make them bolder for additional distinction */ +.vote-table tbody tr:nth-child(n + 2):nth-child(-n + 6) .project-name { + font-weight: 700 !important; /* Bold project names for top 5 */ } -.vote-table tbody tr:last-child td { - border-bottom: none; +/* Top 5 project names - make them bolder for additional distinction */ +.vote-table tbody tr:nth-child(n + 2):nth-child(-n + 6) .project-name a { + font-weight: 700 !important; /* Bold project names for top 5 */ + color: #007bff !important; /* Blue color to match the theme */ } -.sortable-header { - cursor: pointer; - user-select: none; - transition: background-color 0.2s ease; -} - -.sortable-header:hover { - background: var(--color-gray300) !important; +/* Ensure hover effect still works on top 5 rows (rows 2-6) */ +.vote-table tbody tr:nth-child(n + 2):nth-child(-n + 6):hover { + background-color: #e3f2fd !important; /* Slightly darker blue on hover */ } -.project-name a { - color: var(--color-blurple); - text-decoration: none; - font-weight: 500; - font-size: 1.1rem; +/* Thicker divider between 5th and 6th highest project rows */ +/* Category totals is row 1, then projects start at row 2, so divider is after row 6 (6th project) */ +.vote-table tbody tr:nth-child(7) { + border-top: 3px solid #dee2e6 !important; /* Thicker top border */ } -.project-name a:hover { - text-decoration: underline; +/* Remove any conflicting borders */ +.vote-table tbody tr:nth-child(6) { + border-top: none !important; } -.vote-count { +/* Rank column styling */ +.rank-column { + width: 60px; + min-width: 60px; text-align: center; - font-weight: 500; - color: var(--color-gray200); - font-size: 1.1rem; + font-weight: bold; + color: #007bff; + background-color: #f8f9fa; + border-right: 2px solid #dee2e6; } -.total-votes { - text-align: center; - background: var(--color-gray400); - font-weight: 600; - color: var(--color-gray100); - font-size: 1.1rem; +/* Top 5 rows - blue left border in rank column (rows 2-6: category totals + top 5 projects) */ +.vote-table tbody tr:nth-child(n + 2):nth-child(-n + 6) .rank-column { + border-left: 4px solid #007bff !important; + background-color: #f0f8ff !important; + color: #007bff !important; /* Blue numbers for top 5 */ } -.total-votes strong { - color: var(--color-blurple); - font-size: 1.2rem; +/* Rows 6+ - gray numbers for lower rankings */ +.vote-table tbody tr:nth-child(n + 7) .rank-column { + color: #6c757d !important; /* Gray numbers for 6th place and below */ } -/* Category Totals Row Styling */ -.category-totals-row { - background: var(--color-gray400); - border-top: 2px solid var(--color-blurple); - border-bottom: 2px solid var(--color-blurple); +/* Category totals row - no left border */ +.category-totals-row .rank-column { + border-left: none !important; + background-color: #f8f9fa !important; } -.category-totals-row td { - padding: 1rem 1.5rem; - font-weight: 600; - color: var(--color-gray100); - text-align: center; +/* Category totals row styling - remove blue borders */ +.category-totals-row { + background-color: #f8f9fa !important; + border-top: 2px solid #dee2e6 !important; + border-bottom: 2px solid #dee2e6 !important; } -.category-totals-label { - text-align: left !important; - color: var(--color-gray100) !important; - font-size: 1rem; +.category-totals-row .rank-column { + border-left: none !important; + background-color: #f8f9fa !important; + color: #495057 !important; /* Dark gray for totals */ } .category-total { @@ -167,6 +165,15 @@ background: var(--color-gray400); font-weight: 600; color: var(--color-gray100); + font-size: 1.1rem; +} + +/* Totals column - narrower width to fit the label */ +.vote-table th:last-child, +.vote-table td:last-child { + width: 80px; + min-width: 80px; + max-width: 80px; } /* Responsive adjustments */ @@ -208,3 +215,20 @@ height: 3px; background-color: #0056b3; } + +/* Blue border around active category column using data attributes */ +.vote-table tbody tr td[data-category-active='true'] { + border-left: 2px solid #007bff !important; + border-right: 2px solid #007bff !important; +} + +/* Add top and bottom borders to header for complete column box effect */ +.vote-table-header.active-category { + border-top: 2px solid #007bff !important; + border-bottom: 2px solid #007bff !important; +} + +/* Center-align category headers to match vote count columns */ +.vote-table-header { + text-align: center !important; +} diff --git a/src/components/VoteTable.js b/src/components/VoteTable.js index d88df13..f98faa3 100644 --- a/src/components/VoteTable.js +++ b/src/components/VoteTable.js @@ -91,8 +91,11 @@ const VoteTable = ({data, awardCategories, projects, year}) => { let sortableData = getFilteredData(); if (activeCategory) { - // Sort by the active category (highest to lowest) + // Sort by the active category or total votes (highest to lowest) sortableData.sort((a, b) => { + if (activeCategory === 'totalVotes') { + return (b.totalVotes || 0) - (a.totalVotes || 0); + } return (b[activeCategory] || 0) - (a[activeCategory] || 0); }); } @@ -134,6 +137,7 @@ const VoteTable = ({data, awardCategories, projects, year}) => {
    handleSort('project')}> - Project Name {getSortIndicator('project')} - Project handleSort(categoryKey)} + className={getHeaderClassName(categoryKey)} > - {awardCategories[categoryKey].name} {getSortIndicator(categoryKey)} + {awardCategories[categoryKey].name} handleSort('total')}> - Total Votes {getSortIndicator('total')} - Total Votes
    - CATEGORY TOTALS + + Category Totals - {total} - + {Object.keys(awardCategories).map((categoryKey) => { + const categoryVotes = Object.values(data[categoryKey] || {}).reduce( + (sum, votes) => sum + votes, + 0 + ); + return ( + + {categoryVotes} + {grandTotal}
    - {project.name} + {row.projectName} - {categoryVotes[categoryKey] || 0} + {row[categoryKey] || 0} - {total} - {row.totalVotes}
    + {Object.keys(awardCategories).map((categoryKey) => ( ))} - + {/* Category totals row */} + {/* Empty cell for rank column */} @@ -159,17 +169,27 @@ const VoteTable = ({data, awardCategories, projects, year}) => { 0 ); return ( - ); })} - - {sortedData.map((row) => ( + {sortedData.map((row, index) => ( + {Object.keys(awardCategories).map((categoryKey) => ( - ))} - + ))} From d425f74439fa31f625e3ed0eafd24c4c8c63376e Mon Sep 17 00:00:00 2001 From: John Manhart Date: Sat, 23 Aug 2025 20:23:10 -0700 Subject: [PATCH 5/6] Adding in some more table styles --- src/components/VoteTable.css | 16 ++++++++++++++++ src/components/VoteTable.js | 15 +++++++++++++-- src/pages/ManageVotes.js | 7 +++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/components/VoteTable.css b/src/components/VoteTable.css index f2d3acf..1d738bc 100644 --- a/src/components/VoteTable.css +++ b/src/components/VoteTable.css @@ -176,6 +176,22 @@ max-width: 80px; } +/* Group column styling */ +.vote-table th:nth-child(3), +.vote-table td:nth-child(3) { + width: 120px; + min-width: 120px; + max-width: 120px; + text-align: center; + padding: 0.75rem 0.5rem; +} + +.group-name { + font-size: 0.9rem; + color: #6c757d; + font-weight: 500; +} + /* Responsive adjustments */ @media (max-width: 768px) { .vote-table th, diff --git a/src/components/VoteTable.js b/src/components/VoteTable.js index f98faa3..47c4db5 100644 --- a/src/components/VoteTable.js +++ b/src/components/VoteTable.js @@ -1,7 +1,7 @@ import React, {useState} from 'react'; import PropTypes from 'prop-types'; -const VoteTable = ({data, awardCategories, projects, year}) => { +const VoteTable = ({data, awardCategories, projects, groups, year}) => { const [sortConfig, setSortConfig] = useState({ key: null, direction: 'desc', @@ -70,7 +70,14 @@ const VoteTable = ({data, awardCategories, projects, year}) => { // Always show all projects const allProjects = Object.keys(projects).map((projectKey) => { const project = projects[projectKey]; - const row = {projectKey, projectName: project.name}; + const row = { + projectKey, + projectName: project.name, + groupName: + project.group && groups && groups[project.group] + ? groups[project.group].name + : 'No Group', + }; // Calculate total votes for this project let totalVotes = 0; @@ -139,6 +146,7 @@ const VoteTable = ({data, awardCategories, projects, year}) => { + {Object.keys(awardCategories).map((categoryKey) => ( + {/* Empty cell for group column */} {Object.keys(awardCategories).map((categoryKey) => { const categoryVotes = Object.values(data[categoryKey] || {}).reduce( (sum, votes) => sum + votes, @@ -199,6 +208,7 @@ const VoteTable = ({data, awardCategories, projects, year}) => { {row.projectName} + {Object.keys(awardCategories).map((categoryKey) => (
    Rank Project { {awardCategories[categoryKey].name} Total Votes handleSort('totalVotes')} + className={getHeaderClassName('totalVotes')} + > + Totals +
    Category Totals + {categoryVotes} + {grandTotal}
    {index + 1} { + {row[categoryKey] || 0} {row.totalVotes} + {row.totalVotes} +
    Rank ProjectGroup { Category Totals {row.groupName} {Object.keys(votesByProjectAndCategory).map((categoryKey) => { @@ -119,6 +120,11 @@ export default compose( storeAs: 'projects', }, {path: `/years/${params.year}/votes`, populates: keyPopulates, storeAs: 'voteList'}, + { + path: `/years/${params.year}/groups`, + populates: keyPopulates, + storeAs: 'groups', + }, { path: `/users`, queryParams: ['orderByValue=displayName'], @@ -131,6 +137,7 @@ export default compose( awardCategories: dataToJS(firebase, 'awardCategories'), projects: dataToJS(firebase, 'projects'), voteList: orderedPopulatedDataToJS(firebase, 'voteList', keyPopulates), + groups: dataToJS(firebase, 'groups'), userList: dataToJS(firebase, 'userList'), })) )(ManageAwardCategories); From ae2960feea0920b25dbb13dd5386f82e867159ef Mon Sep 17 00:00:00 2001 From: Lazar Nikolov Date: Sun, 24 Aug 2025 19:55:32 -0400 Subject: [PATCH 6/6] super minor adjustments --- src/components/VoteTable.css | 15 ++------------- src/index.css | 21 ++++----------------- src/pages/App.css | 6 ------ 3 files changed, 6 insertions(+), 36 deletions(-) diff --git a/src/components/VoteTable.css b/src/components/VoteTable.css index 1d738bc..c36c406 100644 --- a/src/components/VoteTable.css +++ b/src/components/VoteTable.css @@ -1,13 +1,11 @@ .vote-table-container { margin-bottom: 2rem; - width: 95%; margin-left: auto; margin-right: auto; } /* Admin pages - make table wider */ .admin-page .vote-table-container { - width: 95%; max-width: none; } @@ -23,13 +21,12 @@ border-radius: 8px; border: 1px solid var(--color-gray400); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - min-width: 1200px; /* Ensure minimum comfortable width on desktop */ + max-width: 100%; } .vote-table { width: 100%; - min-width: 1000px; /* Match wrapper minimum width */ - border-collapse: collapse; + border-collapse: separate; background: white; font-size: 1.1rem; } @@ -51,14 +48,6 @@ z-index: 10; } -.vote-table th:first-child { - border-top-left-radius: 8px; -} - -.vote-table th:last-child { - border-top-right-radius: 8px; -} - .vote-table tbody tr:hover { background-color: #f8f9fa; } diff --git a/src/index.css b/src/index.css index 1f99df4..7226392 100644 --- a/src/index.css +++ b/src/index.css @@ -405,26 +405,13 @@ input { .vote-table th, .vote-table td { padding: 0.5rem 0.75rem; - font-size: 0.8rem; - } - - .vote-table-container h3 { - font-size: 1.25rem; } } /* Admin page full width - simple and reliable */ .admin-page { - width: 100vw !important; - max-width: 100vw !important; - margin-left: calc(-50vw + 50%) !important; - margin-right: calc(-50vw + 50%) !important; - padding: 0 32px !important; - box-sizing: border-box !important; -} - -/* Ensure all admin page content uses full width */ -.admin-page > * { - max-width: none !important; - width: 100% !important; + width: 98vw; + padding: 0px 12px; + margin-left: calc(-49vw + 50%); + box-sizing: border-box; } diff --git a/src/pages/App.css b/src/pages/App.css index 151e976..03ef1f6 100644 --- a/src/pages/App.css +++ b/src/pages/App.css @@ -4,12 +4,6 @@ min-height: 100vh; } -/* Admin pages full width override - simplified */ -.admin-page { - width: 100% !important; - max-width: none !important; -} - .App-header { padding: 20px 0; margin: 0;