Skip to content

Commit

Permalink
Merge pull request #212 from doug-stormtree/leaderboard
Browse files Browse the repository at this point in the history
Add 1000 Rain Gardens Leaderboard
  • Loading branch information
doug-stormtree authored Nov 22, 2024
2 parents e030123 + e0e0163 commit 1570a5d
Show file tree
Hide file tree
Showing 9 changed files with 707 additions and 7 deletions.
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@fontsource-variable/inter": "^5.0.17",
"@fontsource-variable/noto-serif": "^5.0.5",
"@fontsource-variable/urbanist": "^5.0.21",
"@fontsource/luckiest-guy": "^5.1.0",
"@fontsource/noto-serif-jp": "^5.0.11",
"@fontsource/poppins": "^5.0.13",
"@fontsource/raleway": "^5.0.8",
Expand Down
291 changes: 291 additions & 0 deletions public/Leaderboard.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
105 changes: 105 additions & 0 deletions public/LeaderboardBanner.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 7 additions & 4 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ import { latLng } from 'leaflet';
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
import { getDatabase } from "firebase/database";
// Questions
import Questions, { useActiveQuestionStore } from './data/QuestionStore';
import { PlacesAutocomplete } from './components/PlacesAutocomplete'; // eslint-disable-line no-unused-vars
import Sandbox from './components/Sandbox';
import QuestionCardBar from './components/QuestionCardBar';
import HomePage from './components/HomePage';
import ContentInitiativeContainer from './components/ContentInitiativeContainer';
Expand All @@ -34,6 +34,7 @@ import TutorialPopup from './components/TutorialPopup';
import AboutPage from './components/AboutPage';
import MobileQuestionMenu from './components/MobileQuestionMenu';
import MobileQuestionDock from './components/MobileQuestionDock';
import LeaderboardAdmin from './components/LeaderboardAdmin';

// Your web app's Firebase configuration
const firebaseConfig = {
Expand All @@ -44,12 +45,14 @@ const firebaseConfig = {
messagingSenderId: "475584087043",
appId: "1:475584087043:web:b7f77d9656f9721da37a36",
measurementId: "G-F052FD5Y1T",
databaseURL: "https://rush-webapp-default-rtdb.firebaseio.com"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
if (app === null) {console.log("Firebase did not initialize.")};
const analytics = getAnalytics(app); // eslint-disable-line no-unused-vars
const database = getDatabase(app); // eslint-disable-line no-unused-vars

function App() {
return (
Expand Down Expand Up @@ -77,8 +80,8 @@ function App() {
element={<WebMap />}
/>
<Route
path="/sandbox"
element={<Sandbox />}
path="/lbadmin"
element={<LeaderboardAdmin db={database} />}
/>
</Routes>
</Router>
Expand Down Expand Up @@ -184,7 +187,7 @@ function WebMap() {
isMobile={isMobile}
/>
<MapBasemap />
<MapData />
<MapData db={database} />
</MapView>
<TutorialPopup
isMobile={isMobile}
Expand Down
113 changes: 113 additions & 0 deletions src/components/Leaderboard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
Box,
Flex,
useDisclosure,
} from '@chakra-ui/react';
import { ref, onValue } from 'firebase/database';

const lbContent = {
topRainmaker: {
name: 'None',
score: '0',
},
topClass: {
name: 'None',
score: '0',
},
topSchool: {
name: 'None',
score: '0',
},
}

const textFieldOpts = {
maxWidth:'114px',
textOverflow:'ellipsis',
whiteSpace:'nowrap',
overflow:'hidden',
}

export default function Leaderboard({ db, initContent = lbContent }) {
const [ content, setContent ] = useState(initContent);

// store Rain Garden count separately to fetch from OGM
const [ total, setTotal ] = useState('0')
const getTotalRainGardens = useMemo(() => async () => {
// Fetch count of Rain Gardens from OGM
fetch('https://greenmap.org/api-v1/maps/63e6939eabcc260100514352/meta')
.then((response) => response.json())
.then((json) => {
setTotal(json.publicFeatures.toString())
})
}, [])

useEffect(() => {
getTotalRainGardens()
}, [getTotalRainGardens])

useEffect(() => {
if (db === undefined) return;

// Listen for DB changes
const lbRef = ref(db, 'leaderboard/');
onValue(lbRef, (snapshot) => {
const data = snapshot.val()
setContent(data)
})
}, [setContent, db])

return (
<LeaderboardComponent content={{total: { name: 'Total', score: total }, ...content}} />
)
}

export function LeaderboardComponent({ content }) {
const { isOpen, onOpen, onClose } = useDisclosure({defaultIsOpen: true})

return isOpen ? (
<Flex
backgroundImage="url('/Leaderboard.svg')"
width='200px'
height='344px'
direction='column'
// positioning
paddingX='1.75rem'
paddingTop='4.5rem'
//paddingTop='11.75rem'
gap='1.3rem'
// text styles
fontFamily='var(--chakra-fonts-leaderboard)'
fontWeight='400'
color='white'
textShadow='-1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000'
fontSize='1rem'
// interaction
onClick={onClose}
>
<Flex direction='row' justifyContent='space-between' marginBottom='0.8rem'>
<Box textAlign='left' {...textFieldOpts}>{content.topRainmaker.name}</Box>
<Box textAlign='right'>{content.topRainmaker.score.substring(0,4)}</Box>
</Flex>
<Flex direction='row' justifyContent='space-between' marginBottom='0.8rem'>
<Box textAlign='left' {...textFieldOpts}>{content.topClass.name}</Box>
<Box textAlign='right'>{content.topClass.score.substring(0,4)}</Box>
</Flex>
<Flex direction='row' justifyContent='space-between'>
<Box textAlign='left' {...textFieldOpts}>{content.topSchool.name}</Box>
<Box textAlign='right'>{content.topSchool.score.substring(0,4)}</Box>
</Flex>
<Flex direction='row' justifyContent='space-between'>
<Box textAlign='left' {...textFieldOpts}>{content.total.name}</Box>
<Box textAlign='right'>{content.total.score.substring(0,4)}</Box>
</Flex>
</Flex>
) : (
<Box
backgroundImage="url('/LeaderboardBanner.svg')"
width='200px'
height='52px'
onClick={onOpen}
/>
)
}
173 changes: 173 additions & 0 deletions src/components/LeaderboardAdmin.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import React, { useEffect, useState } from 'react';
import {
Button,
Center,
Flex,
FormControl,
FormLabel,
Input,
InputGroup,
InputRightElement,
SimpleGrid,
Text,
} from '@chakra-ui/react';
import { produce } from 'immer';
import { LeaderboardComponent } from './Leaderboard';
import { set, ref } from 'firebase/database';

const lbContent = {
topRainmaker: {
name: 'Test',
score: '0',
},
topClass: {
name: 'Test',
score: '0',
},
topSchool: {
name: 'Test',
score: '0',
},
}

const isNameInvalid = (name) => {
return (name.length > 20 || name.length <= 0)
}

const isScoreInvalid = (score) => {
return (score.length > 4 || score.length <= 0 || isNaN(score))
}

const validateForm = (content) => {
return content.topRainmaker.name.length < 20 &&
content.topClass.name.length < 20 &&
content.topSchool.name.length < 20 &&
!isScoreInvalid(content.topRainmaker.score) &&
!isScoreInvalid(content.topClass.score) &&
!isScoreInvalid(content.topSchool.score)
}

export default function LeaderboardAdmin({ db }) {
const [content, setContent] = useState(lbContent);
const [show, setShow] = useState(false)
const handleClick = () => setShow(!show)
const [isValid, setIsValid] = useState(false)
const [message, setMessage] = useState('')

useEffect(() => {
setIsValid(validateForm(content))
}, [content, setIsValid])

const submitForm = () => {
set(ref(db, 'leaderboard/'), content)
.then(setMessage('Upload successful.'))
.catch((error) => {
setMessage(error.message)
})
}

return (
<Center height='100svh'>
<Flex direction='column' gap='1rem'>
<LeaderboardComponent content={{ total: { name: 'Total', score: '0' }, ...content}}/>
<SimpleGrid columns={2} gap='1rem'>
<FormControl isRequired>
<FormLabel>Top Rainmaker</FormLabel>
<Input
placeholder='Top Rainmaker name...'
onChange={(e) => setContent(produce(content, (draftState) => {
draftState.topRainmaker.name = e.target.value
}))}
isInvalid={isNameInvalid(content.topRainmaker.name)}
/>
</FormControl>
<FormControl isRequired>
<FormLabel>Score</FormLabel>
<Input
placeholder='score...'
htmlSize={4}
width='auto'
onChange={(e) => setContent(produce(content, (draftState) => {
draftState.topRainmaker.score = e.target.value
}))}
isInvalid={isScoreInvalid(content.topRainmaker.score)}
/>
</FormControl>

<FormControl isRequired>
<FormLabel>Top Class</FormLabel>
<Input
placeholder='Top class name...'
onChange={(e) => setContent(produce(content, (draftState) => {
draftState.topClass.name = e.target.value
}))}
isInvalid={isNameInvalid(content.topClass.name)}
/>
</FormControl>
<FormControl isRequired>
<FormLabel>Score</FormLabel>
<Input
placeholder='score...'
htmlSize={4}
width='auto'
onChange={(e) => setContent(produce(content, (draftState) => {
draftState.topClass.score = e.target.value
}))}
isInvalid={isScoreInvalid(content.topClass.score)}
/>
</FormControl>

<FormControl isRequired>
<FormLabel>Top School</FormLabel>
<Input
placeholder='Top school name...'
onChange={(e) => setContent(produce(content, (draftState) => {
draftState.topSchool.name = e.target.value
}))}
isInvalid={isNameInvalid(content.topSchool.name)}
/>
</FormControl>
<FormControl isRequired>
<FormLabel>Score</FormLabel>
<Input
placeholder='score...'
htmlSize={4}
width='auto'
onChange={(e) => setContent(produce(content, (draftState) => {
draftState.topSchool.score = e.target.value
}))}
isInvalid={isScoreInvalid(content.topSchool.score)}
/>
</FormControl>
</SimpleGrid>

<FormControl isRequired>
<FormLabel>Admin Password</FormLabel>
<InputGroup size='md'>
<Input
pr='4.5rem'
type={show ? 'text' : 'password'}
placeholder='Enter password'
onChange={(e) => setContent(produce(content, (draftState) => {
draftState.password = e.target.value
}))}
/>
<InputRightElement width='4.5rem'>
<Button h='1.75rem' size='sm' onClick={handleClick}>
{show ? 'Hide' : 'Show'}
</Button>
</InputRightElement>
</InputGroup>
</FormControl>
<Button
onClick={submitForm}
colorScheme='green'
disabled={!isValid}
>
Submit
</Button>
<Text>{message}</Text>
</Flex>
</Center>
)
}
Loading

0 comments on commit 1570a5d

Please sign in to comment.