= (props) => {
}} />
)
@@ -286,25 +164,6 @@ const MissedDayCard: React.FC<{dateStr: string}> = (props) => {
)
}
-const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
-const dateCursor = new Date() // Start on today
-const startDate = new Date('2021-05-03T04:00:00Z') // First day of our study group! 🥳
-
-// Generate the date range we're interested in
-type TDay = {
- dayNo: number
- dateStr: string
-}
-const dateRange = [] as TDay[]
-while (dateCursor.getTime() >= startDate.getTime()) {
- dateRange.push({
- dayNo: dateCursor.getDay(),
- dateStr: dateCursor.toLocaleDateString()
- })
- dateCursor.setTime(dateCursor.getTime() - 86400 * 1000) // Step one day backwards until we get to the start date
-}
-// console.log('$$ dateRange:', dateRange)
-
const FLine = styled.div`
width: 100%;
border: 0;
@@ -368,40 +227,85 @@ const FFooter = styled.div`
const FStudyGroup = styled.div`
height: 100%;
- min-height: 100vh;
- margin: 0;
+ margin: 0 0 60px 0;
padding: 0;
`
+const FMonthTitle = styled.span`
+ font-size: 18px;
+ color: white;
+ background: #161b22;
+ border: 1px #30363d solid;
+ padding: 3px 3px 5px 3px;
+ margin: 10px;
+`
+
+const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
+
type TStudyGroup = {
commit: Commit
userProgressDb: UserProgressDb
+ initialDate: Date
}
+
+let globalInitialLoad = true // Hack. 7/8/21
+let globalUpDb: UserProgressDb
+
const StudyGroup: React.FC
= (props) => {
- // console.log('>> render Study Group')
- // const [upDb, setUpDb] = useState(props.userProgressDb)
- const [value, setValue] = useState(0)
+ const [upDb, setUpDb] = useState(props.userProgressDb)
+ const [value, setValue] = useState(new Date())
+ const [curDate, setCurDate] = useState(props.initialDate)
+ const [members, setMembers] = useState>([])
+ // console.log('>> curDate - initial load:', curDate)
+
+ // Set local instance to global instance
+ globalUpDb = upDb
+
+ // Load cards
+ useEffect(() => {
+ if (curDate !== props.initialDate) {
+ console.log('>> Changing date periods', curDate)
+ Promise.resolve(getUpDb(curDate)).then(rs => setUpDb(rs))
+ }
+ history.pushState({}, '', '/studygroup/?d=' + util.getYearMonth(curDate))
+ window.parent.postMessage(window.location.href, '*')
+ }, [curDate])
+ // This should NOT run on initial load. 7/8/21
function updateDashboard (payload: any) {
- console.log('>> updateDashboard: ' + value)
- if (payload.kind === 'issue') {
- runUpdateIssueFlow(payload)
- } else if (payload.kind === 'issue_comment') {
- runUpdateIssueCommentFlow(payload)
+ if (globalInitialLoad) {
+ console.log('>> Initial load detected. Skipping dashboard refresh')
+ globalInitialLoad = false // Flip the one-time flag. Hack. 7/8/21
+ } else {
+ console.log('>> updateDashboard:', payload.dt.toDate().toISOString(), payload.issue.number, payload.issue.title, payload.kind)
+ if (payload.kind === 'issue') {
+ runUpdateIssueFlow(payload)
+ } else if (payload.kind === 'issue_comment') {
+ runUpdateIssueCommentFlow(payload)
+ }
+ setValue(new Date()) // Force update hack to repaint React GUI. 5/31/21
}
- setValue(oldVal => oldVal + 1) // Force update hack to repaint React GUI. 5/31/21
}
- // Bug with GitHub? Comment count seems buggy. 6/1/21
function runUpdateIssueCommentFlow (payload:any) {
- console.log('>> TODO: runUpdateIssueCommentFlow:', payload.action, payload.kind)
- // Currently broken - need to reach out to GitHub for guidance. 6/2/21
- // const dateStr = (new Date(payload.issue.created_at)).toLocaleDateString()
- // if (payload.action === 'created') {
- // upDb.getUser(payload.issue.user.login)!.getCard(dateStr)!.comments++
- // } else if (payload.action === 'deleted') {
- // upDb.getUser(payload.issue.user.login)!.getCard(dateStr)!.comments--
- // }
+ // For now, until GitHub fixes the `comments number payload` bug, just
+ // query the GitHub REST API to refresh the entire GUI to get all up-to-date comment counts. 7/8/21
+ console.log('\t>> Run: UpdateIssueCommentFlow:', payload.action, payload.kind, payload.issue.number, payload.issue.user.login)
+ if (payload.action === 'created') {
+ globalUpDb.getUser(payload.issue.user.login)?.incrementComments(payload.issue.number)
+ } else if (payload.action === 'deleted') {
+ globalUpDb.getUser(payload.issue.user.login)?.decrementComments(payload.issue.number)
+ }
+
+ // Experimental; remove this later. Keeping here for future reference. 7/8/21
+ // // getNumberOfCommentsPerCard(payload.issue.number)
+ // // This is a total hack; we wait 61 seconds for the GitHub-side to propogate the change before fetching from them again. 7/8/21
+ // const d = new Date()
+ // console.log('>>>>>> Begin timer to query GitHub:', d)
+ // setTimeout(() => {
+ // console.log('>>>>>> Now querying GitHub - 61 secs later...', d)
+ // Promise.resolve(getUpDb(curDate)).then(rs => setUpDb(rs))
+ // }, 61000)
}
function runUpdateIssueFlow (payload:any) {
@@ -434,90 +338,131 @@ const StudyGroup: React.FC = (props) => {
upDb.getUser(cardInput.userHandle)!.setCard(cardInput)
}
- // Listen for latest updates from Firestore
+ // Listen for latest member stats updates from Firestore
+ useEffect(() => {
+ const unsubscribe = firebase.firestore().collection('studyMembers')
+ .onSnapshot((querySnapshot) => {
+ const members = [] as Array
+ querySnapshot.forEach((doc) => {
+ const m = doc.data()
+ members.push({
+ userFullname: m.Fullname,
+ userHandle: m.Handle,
+ startDateStr: m.StartDate,
+ uid: m.Uid,
+ repo: m.Repo,
+ active: m.Active,
+ streakCurrent: {
+ days: m.StreakCurrent.Days,
+ startDate: m.StreakCurrent.StartDate,
+ endDate: m.StreakCurrent.EndDate
+ },
+ streakMax: {
+ days: m.StreakMax.Days,
+ startDate: m.StreakMax.StartDate,
+ endDate: m.StreakMax.EndDate
+ },
+ recordCount: m.RecordCount,
+ daysJoined: m.DaysJoined,
+ lastCard: {
+ date: m.LastCard.Date,
+ number: m.LastCard.Number
+ },
+ record: new Map(Object.entries(m.Record))
+ })
+ })
+ // console.log('\t>> member[0].record', members[0].record)
+ setMembers(members.sort((a, b) => Date.parse(a.startDateStr) - Date.parse(b.startDateStr)))
+ console.log('>> member stats update received:', (new Date()).toISOString())
+ })
+ return () => unsubscribe()
+ }, [])
+
+ // Listen for latest card updates from Firestore
useEffect(() => {
const unsubscribe = firebase.firestore().collection('ghUpdates').doc('latestUpdate')
.onSnapshot((doc) => {
- // console.log('Data update received: ', doc.data())
+ // console.log('>> doc.data():', doc.data())
updateDashboard(doc.data())
})
return () => unsubscribe()
}, [])
+ function navPrevMonth () {
+ setCurDate(util.getPrevMonth(curDate))
+ }
+
+ function navNextMonth () {
+ setCurDate(util.getNextMonth(curDate))
+ }
+
+ const startDate = new Date('2021-05-03T04:00:00Z')
+ const dateRange = util.getDateRangeOneMonth(curDate)
+
return (
- Roadmap/Specs
- Raw Data
- Repo
- Members
- History
- Changelog
- CodeNewbie
+ Roadmap/Specs
+ Raw Data
+ Repo
+ Members
+ Screenshots
+ Changelog
+ CodeNewbie
+ Tasks
+ New Card
- Study Group 00:
- {
- studyMembers.map((member: StudyMember, i: number) => {
- const handle = member.userHandle
- const streak = upDb.getUser(handle)?.CurrentStreak
- const missedDays = upDb.getUser(handle)?.MissedDays
- const rs = []
- const content =
-
- Member #{i}:
{handle} |
- Streak: {streak} consecutive days |
- Start: {(new Date(member.startDateStr)).toDateString()} |
- Missed Days: {missedDays}
-
- if (member.active) {
- rs.push(content)
- } else {
- rs.push({content})
- }
- return (rs)
- })
- }
+ Study Group 00 | {value.toISOString()}
+
+
+
+
+
+ {util.printMonthYear(curDate)}
+
+
- Progress:
-
-
+
+
+
.
.
{
- dateRange.map((day: TDay, i: number) => {
- dateCursor.setDate(dateCursor.getDate() - 1)
- return (
-
-
- {days[day.dayNo]}
- {day.dateStr.replace('/2021', '/21')}
-
- {day.dayNo === 0 &&
-
- }
-
- )
- })
+ dateRange.map((day: util.TDay, i: number) =>
+
+
+ {days[day.dayNo]}
+ {day.dateStr.replace('/2021', '/21')}
+
+ {day.dayNo === 0 &&
+
+ }
+
+ )
}
-
+
{
- studyMembers.map((m: StudyMember, i: number) =>
- m.active
- ?
+ members.map((m: StudyMember, i: number) =>
+ m.active && Date.parse(m.startDateStr) <= util.getMonthLastDay(curDate).getTime()
+ ?
{
- dateRange.map((day: TDay, i: number) => renderCard(upDb, m, day, i))
+ dateRange.map((day: util.TDay, i: number) => renderCard(upDb, m, day, i))
}
-
+
:
)
}
-
+
🔖 {props.commit.version} | {props.commit.message}
👷♂️ {props.commit.built.toString()}
@@ -526,9 +471,9 @@ const StudyGroup: React.FC = (props) => {
)
}
-function renderCard (upDb:UserProgressDb, m:StudyMember, day: TDay, i: number) {
+function renderCard (upDb:UserProgressDb, m:StudyMember, day: util.TDay, i: number) {
const rs = []
- const card = upDb.getUser(m.userHandle)!.getCard(day.dateStr)
+ const card = upDb.getUser(m.userHandle)!.getCardByDate(day.dateStr)
if (card) {
rs.push()
@@ -538,7 +483,7 @@ function renderCard (upDb:UserProgressDb, m:StudyMember, day: TDay, i: number) {
rs.push()
}
- if (day.dayNo === 0 && Date.parse(day.dateStr) > Date.parse(m.startDateStr)) {
+ if (day.dayNo === 0 && Date.parse(day.dateStr) >= Date.parse(m.startDateStr)) {
rs.push(
)
diff --git a/src/assets/css/design01.css b/src/assets/css/design01.css
new file mode 100644
index 0000000..f9b9231
--- /dev/null
+++ b/src/assets/css/design01.css
@@ -0,0 +1,49 @@
+@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap');
+
+* {
+ font-family: 'IBM Plex Mono', monospace;
+ font-size: 14px;
+ line-height: 21px;
+}
+
+body {
+ background-color: #0d1117;
+ color: white;
+ margin: 0;
+ padding: 0;
+}
+
+a:link {
+ color: #2f87f8;
+ text-decoration: none;
+}
+
+a:visited {
+ color: #2f87f8;
+ text-decoration: none;
+}
+
+a:hover {
+ color: #2f87f8;
+ text-decoration: underline;
+}
+
+a:active {
+ color: red;
+}
+
+.octicon {
+ fill: #a5b5bb;
+ margin-top: 2px;
+ margin-bottom: -3px;
+ padding-right: 6px;
+ /* background: pink; */
+}
+
+.commentNo {
+ font-weight: 600;
+ font-size: 13px;
+ font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
+ color: #a5b5bb;
+}
\ No newline at end of file
diff --git a/src/assets/css/design02.css b/src/assets/css/design02.css
new file mode 100644
index 0000000..5802c72
--- /dev/null
+++ b/src/assets/css/design02.css
@@ -0,0 +1,43 @@
+.board {
+ position: absolute;
+ display: flex;
+ left: 43px;
+ top: 29px;
+}
+
+a:link {
+ color: white;
+ font-weight: 300;
+ text-decoration: none;
+}
+
+a:visited {
+ color: white;
+ text-decoration: none;
+}
+
+a:hover {
+ color: yellow;
+ font-weight: 400;
+ /* text-decoration: underline; */
+ /* background-color: yellow; */
+}
+
+a:active {
+ color: red;
+}
+
+.octicon {
+ fill: #a5b5bb;
+ margin-top: 2px;
+ margin-bottom: -3px;
+ padding-right: 6px;
+ /* background: pink; */
+}
+
+.commentNo {
+ font-weight: 600;
+ font-size: 13px;
+ font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
+ color: #a5b5bb;
+}
\ No newline at end of file
diff --git a/src/assets/css/landscape.css b/src/assets/css/landscape.css
new file mode 100644
index 0000000..5ebe969
--- /dev/null
+++ b/src/assets/css/landscape.css
@@ -0,0 +1,115 @@
+@import url('https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@300;400&display=swap');
+
+:root {
+ --darkblue: #152c3e;
+ --lightblue: #37618a;
+ --greenprimary: #879759;
+ --greenshadow: #648459;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ font-family: 'Chakra Petch', sans-serif;
+ font-style: normal;
+ font-weight: 400;
+ font-size: 12px;
+ line-height: 29px;
+ /* color: blue; */
+}
+
+@media (max-width: 960px) {
+ body { zoom: 0.7; }
+}
+
+html, body { height: 100%; }
+
+@keyframes bounce {
+ from {
+ transform: translateY(0px);
+ } to {
+ transform: translateY(-4px);
+ }
+}
+
+@keyframes sway {
+ from {
+ transform: translateX(0px);
+ } to {
+ transform: translateX(-12px);
+ }
+}
+
+body {
+ min-height: 100%;
+ background: -webkit-linear-gradient(top, var(--darkblue), var(--lightblue));
+ position: relative;
+ overflow-x: hidden;
+ overflow-y: hidden;
+}
+
+#mountains {
+ z-index: 2;
+ position: absolute;
+ bottom: 20%;
+ margin-bottom: -16px;
+ left: 10%;
+}
+
+#cloud1 {
+ z-index: 2;
+ position: absolute;
+ top: 20%;
+ right: 4%;
+ animation: bounce 2s ease-in-out 0s infinite alternate;
+}
+
+#cloud2 {
+ z-index: 1;
+ position: absolute;
+ top: 22%;
+ right: 16%;
+ animation: bounce 3.5s ease-in-out 2s infinite alternate;
+}
+
+.trees {
+ position: absolute;
+ width: 100%;
+ height: 160px;
+ z-index: 4;
+ left: -43px; /* Hack-- fix later. 8/6/21 */
+ bottom: 20%;
+ margin-bottom: -100px;
+ animation: fader 10s linear forwards;
+}
+
+@keyframes fader {
+ 0% { opacity: 0; }
+ 20% { opacity: 1; }
+ 80% { opacity: 1; }
+ 100% { opacity: 0; }
+}
+
+#ground {
+ z-index: 1;
+ width: 100%;
+ height: 20%;
+ position: absolute;
+ bottom: 0;
+ background: var(--greenprimary);
+}
+
+#hills {
+ /* background: rgba(255,0,0,0.3); */
+ position: absolute;
+ left: 10%;
+ margin-left: -120px;
+ margin-bottom: -86px;
+ bottom: 20%;
+ z-index: 3;
+}
+
+#stars {
+ width: 100%;
+ height: 100%;
+}
\ No newline at end of file
diff --git a/src/data/changelog.json b/src/data/changelog.json
index 5de4519..38d3dca 100644
--- a/src/data/changelog.json
+++ b/src/data/changelog.json
@@ -1,4 +1,154 @@
[
+ {
+ "version": "1.9.9",
+ "message": "♻ Update `Previewer` to `v1`/`v2` \u0026 disable console logging",
+ "built": "Fri, 06 Aug 2021 05:08:54 EDT"
+ },
+ {
+ "version": "1.9.8",
+ "message": "✨ Finish integration of dynamic `CSS tree art` generation",
+ "built": "Fri, 06 Aug 2021 03:31:04 EDT"
+ },
+ {
+ "version": "1.9.7",
+ "message": "✨ Begin integration of CSS trees to be dynamic",
+ "built": "Thu, 05 Aug 2021 20:33:03 EDT"
+ },
+ {
+ "version": "1.9.6",
+ "message": "✨ Add attribution for CSS background art",
+ "built": "Thu, 05 Aug 2021 15:58:05 EDT"
+ },
+ {
+ "version": "1.9.5",
+ "message": "✨ Add footer",
+ "built": "Thu, 05 Aug 2021 14:03:05 EDT"
+ },
+ {
+ "version": "1.9.4",
+ "message": "♻ Refactor `MemberBoard` into separate component",
+ "built": "Tue, 03 Aug 2021 23:42:11 EDT"
+ },
+ {
+ "version": "1.9.3",
+ "message": "♻ Refactor `card` styled components to inherit from base class",
+ "built": "Tue, 03 Aug 2021 23:26:39 EDT"
+ },
+ {
+ "version": "1.9.2",
+ "message": "✨ Display past seven days worth of cards in GUI v2",
+ "built": "Sat, 31 Jul 2021 22:56:13 EDT"
+ },
+ {
+ "version": "1.9.1",
+ "message": "✨ Start `dashboard` GUI redesign that is beautified with CSS art",
+ "built": "Fri, 30 Jul 2021 02:12:08 EDT"
+ },
+ {
+ "version": "1.9.0",
+ "message": "🍑 Begin `sprint-jambul` (Thu - 7/29/21)",
+ "built": "Thu, 29 Jul 2021 20:46:42 EDT"
+ },
+ {
+ "version": "1.8.19",
+ "message": "♻ Refactor models + `LastCard` fields in `StudyMember`",
+ "built": "Tue, 20 Jul 2021 22:44:32 EDT"
+ },
+ {
+ "version": "1.8.18",
+ "message": "♻ Refactor `dashboard` to fetch `studyMembers` from Firestore",
+ "built": "Tue, 20 Jul 2021 01:16:00 EDT"
+ },
+ {
+ "version": "1.8.17",
+ "message": "🧙♂️ Add Jassa to the study group!",
+ "built": "Sun, 18 Jul 2021 18:03:54 EDT"
+ },
+ {
+ "version": "1.8.16",
+ "message": "💄🐞 Tweak `week demarcation` logic in `MembersPane` \u0026 fix `target='_top'` bug in links",
+ "built": "Sun, 18 Jul 2021 14:39:16 EDT"
+ },
+ {
+ "version": "1.8.15",
+ "message": "✨ Revamp `MembersPane` to better visually show `Missed Days` and `streaks` metrics",
+ "built": "Thu, 15 Jul 2021 22:15:45 EDT"
+ },
+ {
+ "version": "1.8.14",
+ "message": "🐞 Fix error that was being thrown if comment was +/- to a card not currently in-view",
+ "built": "Thu, 08 Jul 2021 22:27:09 EDT"
+ },
+ {
+ "version": "1.8.13",
+ "message": "🔊 Add more debug logging to the `+/- comment count` logic",
+ "built": "Thu, 08 Jul 2021 21:23:26 EDT"
+ },
+ {
+ "version": "1.8.12",
+ "message": "✨ Add `octicon comments` icon \u0026 update the GUI in real-time on every new comment +/-",
+ "built": "Thu, 08 Jul 2021 16:50:49 EDT"
+ },
+ {
+ "version": "1.8.11",
+ "message": "✨ Support `pass-thru` `GET {params}` in `Previewer` top-level url",
+ "built": "Wed, 07 Jul 2021 17:24:35 EDT"
+ },
+ {
+ "version": "1.8.10",
+ "message": "🐞 Fix bug: Clicking on top navbar links now opens them in `target='_top'`",
+ "built": "Wed, 07 Jul 2021 07:40:30 EDT"
+ },
+ {
+ "version": "1.8.9",
+ "message": "🐞 Fix bug: Clicking on cards now opens them in `target='_top'`",
+ "built": "Wed, 07 Jul 2021 02:26:35 EDT"
+ },
+ {
+ "version": "1.8.8",
+ "message": "✨ Change url to `/studydash` \u0026 add support for `Previewer Frame`",
+ "built": "Tue, 06 Jul 2021 22:37:58 EDT"
+ },
+ {
+ "version": "1.8.7",
+ "message": "✨ Support `?d=YYYY-MM` GET {param} in url",
+ "built": "Tue, 06 Jul 2021 17:05:54 EDT"
+ },
+ {
+ "version": "1.8.6",
+ "message": "✨ Update `MembersPane` to show latest submitted card number",
+ "built": "Mon, 05 Jul 2021 21:52:54 EDT"
+ },
+ {
+ "version": "1.8.5",
+ "message": "💄 Beautify `MembersPane` to display `Current Streak` details on newline",
+ "built": "Sat, 03 Jul 2021 22:54:32 EDT"
+ },
+ {
+ "version": "1.8.4",
+ "message": "✨ Add `{StartDate} to {EndDate}` title when hovering over `Streak Numbers`",
+ "built": "Thu, 01 Jul 2021 20:41:52 EDT"
+ },
+ {
+ "version": "1.8.3",
+ "message": "✨ Impl live `Current/Max Streak` on `dashboard GUI`",
+ "built": "Thu, 01 Jul 2021 16:07:17 EDT"
+ },
+ {
+ "version": "1.8.2",
+ "message": "💄 Beautify `pagination controls` on `dashboard GUI`",
+ "built": "Thu, 24 Jun 2021 21:41:24 EDT"
+ },
+ {
+ "version": "1.8.1",
+ "message": "✨ Add `pagination-by-month` to `dashboard GUI`",
+ "built": "Thu, 24 Jun 2021 18:05:57 EDT"
+ },
+ {
+ "version": "1.8.0",
+ "message": "🥭 Begin `sprint-imbe` (Sun - 6/20/21)",
+ "built": "Sun, 20 Jun 2021 16:17:17 EDT"
+ },
{
"version": "1.7.4",
"message": "📦 Release `sprint-hala` 🍍 ➡ PROD: `v1.7.4-hala`",
diff --git a/src/lib/util.ts b/src/lib/util.ts
index 66802dc..d5147d6 100644
--- a/src/lib/util.ts
+++ b/src/lib/util.ts
@@ -49,3 +49,94 @@ export class AutoId {
return autoId
}
}
+
+// Eg. "August 2021"
+export function printMonthYear (d: Date): string {
+ const options = {
+ year: 'numeric',
+ month: 'long'
+ }
+ return new Intl.DateTimeFormat('en-US', options as any).format(d)
+}
+
+export function getNextMonth (d: Date): Date {
+ const nd = new Date(d.getFullYear(), d.getMonth() + 1, d.getDate()) // Step forward one month
+ // console.log('>> getNextMonth:', d.toDateString(), nd.toDateString())
+ return nd
+}
+
+export function getPrevMonth (d: Date): Date {
+ const nd = new Date(d.getFullYear(), d.getMonth() - 1, 1) // Step backward one month; start on first day
+ // console.log('>> getPrevMonth:', d.toDateString(), nd.toDateString())
+ return nd
+}
+
+export function getPeriod (d: Date): string {
+ return `${d.getFullYear()}-` + `${d.getMonth() + 1}`.padStart(2, '0')
+}
+
+export function getMonthLastDay (d: Date): Date {
+ const nd = new Date(d.getFullYear(), d.getMonth() + 1, 0)
+ return nd
+}
+
+export type TDay = {
+ dayNo: number
+ dateStr: string
+}
+
+// Return past eight days, ending on endDate
+export function getDateRangeOneWeek (endDate: Date): TDay[] {
+ const dateRange = [] as TDay[]
+ const startDate = new Date()
+ startDate.setTime(endDate.getTime() - 86400 * 1000 * 8) // Eight days
+
+ const today = new Date()
+ let dateCursor: Date
+ if (today.getMonth() === endDate.getMonth()) {
+ dateCursor = new Date(endDate.getFullYear(), endDate.getMonth(), today.getDate()) // Start on today
+ } else {
+ dateCursor = new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0) // Start on the last day of the month
+ }
+
+ while (dateCursor.getTime() >= startDate.getTime()) {
+ dateRange.push({
+ dayNo: dateCursor.getDay(),
+ dateStr: dateCursor.toLocaleDateString()
+ })
+ dateCursor.setTime(dateCursor.getTime() - 86400 * 1000) // Step one day backwards until we get to the start date
+ }
+ return dateRange
+}
+
+export function getDateRangeOneMonth (endDate: Date): TDay[] {
+ const today = new Date()
+ let dateCursor: Date
+ if (today.getMonth() === endDate.getMonth()) {
+ dateCursor = new Date(endDate.getFullYear(), endDate.getMonth(), today.getDate()) // Start on today
+ } else {
+ dateCursor = new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0) // Start on the last day of the month
+ }
+
+ const dateRange = [] as TDay[]
+ const startDate = new Date(endDate.getFullYear(), endDate.getMonth(), 1) // Stop on the first day of the month at midnight ET
+ // Eg. Tue Jun 01 2021 00:00:00 GMT-0400 (Eastern Daylight Time)
+ while (dateCursor.getTime() >= startDate.getTime()) {
+ dateRange.push({
+ dayNo: dateCursor.getDay(),
+ dateStr: dateCursor.toLocaleDateString()
+ })
+ dateCursor.setTime(dateCursor.getTime() - 86400 * 1000) // Step one day backwards until we get to the start date
+ }
+ return dateRange
+}
+
+// Returns "YYYY-MM-DD" => Eg. "2021-07-15"
+export function getYearMonthDay (d: Date): string {
+ return `${d.getFullYear()}-` + `${d.getMonth() + 1}`.padStart(2, '0') + '-' + `${d.getDate()}`.padStart(2, '0')
+}
+
+// Returns "YYYY-MM" => Eg. "2021-07"
+export function getYearMonth (d: Date): string {
+ return `${d.getFullYear()}-` + `${d.getMonth() + 1}`.padStart(2, '0')
+}
diff --git a/src/models/MStudyMember.ts b/src/models/MStudyMember.ts
new file mode 100644
index 0000000..8063433
--- /dev/null
+++ b/src/models/MStudyMember.ts
@@ -0,0 +1,14 @@
+/**
+ * An abbreviated model of StudyMember.
+ * Only contains the core fields.
+ */
+type MStudyMember = {
+ userFullname: string
+ userHandle: string
+ startDateStr: string
+ uid: string
+ repo: string
+ active: boolean
+}
+
+export default MStudyMember
diff --git a/src/models/StudyMember.ts b/src/models/StudyMember.ts
new file mode 100644
index 0000000..2252b27
--- /dev/null
+++ b/src/models/StudyMember.ts
@@ -0,0 +1,25 @@
+export type Streak = {
+ days: number,
+ startDate: string,
+ endDate: string
+}
+
+export type Card = {
+ date: string,
+ number: number
+}
+
+export type StudyMember = {
+ userFullname: string
+ userHandle: string
+ startDateStr: string
+ uid: string
+ repo: string
+ active: boolean,
+ streakCurrent: Streak,
+ streakMax: Streak,
+ recordCount: number,
+ daysJoined: number,
+ lastCard: Card,
+ record: Map
+}
diff --git a/src/models/UserProgress.ts b/src/models/UserProgress.ts
index 9c9f0e4..d32b55b 100644
--- a/src/models/UserProgress.ts
+++ b/src/models/UserProgress.ts
@@ -50,7 +50,8 @@ class UserProgress {
userFullname: string
userHandle: string
startDate: Date
- _cards = new Map()
+ _cardsByDate = new Map() // Index key: Eg. '7/8/2021'
+ _cardsByNo = new Map() // Index key: Eg. 185
_streakCurrent = 0
_missedDays = 0
@@ -62,46 +63,30 @@ class UserProgress {
setCard (cardInput: TCardInput) {
const card = new Card(cardInput)
- this._cards.set(card.createdStr, card)
+ this._cardsByDate.set(card.createdStr, card)
+ this._cardsByNo.set(card.number, card)
}
- getCard (dateStr: string) {
- return this._cards.get(dateStr)
+ getCardByDate (dateStr: string) {
+ return this._cardsByDate.get(dateStr)
}
- calculateStreak () {
- // console.log('>> Calculate streak for:', this.userHandle)
- const dateCursor = new Date() // Start on today
- // Generate the date range we're interested in
- const dateRange = [] as string[]
- while (dateCursor.getTime() >= this.startDate.getTime()) {
- dateRange.push(dateCursor.toLocaleDateString())
- dateCursor.setTime(dateCursor.getTime() - 86400 * 1000) // Step one day backwards until we get to the start date
- }
- // console.log('>> dateRange:', this.startDate.getDate(), dateRange)
- for (const dateStr of dateRange) {
- // console.log('>> dateStr:', dateStr)
- if (this._cards.has(dateStr)) {
- this._streakCurrent++
- } else {
- const today = new Date()
- if (today.toLocaleDateString() !== dateStr) { // Don't do anything if user hasn't yet contributed today
- // console.log('>> Resetting streak!')
- this._streakCurrent = 0 // Reset the streak
- this._missedDays++
- }
- }
- }
+ getCardByNo (cardNo: number) {
+ return this._cardsByNo.get(cardNo)
}
- get CurrentStreak () {
- this._streakCurrent = this._missedDays = 0
- this.calculateStreak()
- return this._streakCurrent
+ incrementComments (cardNo: number) {
+ if (this._cardsByNo.has(cardNo)) {
+ this._cardsByNo.get(cardNo)!.comments++
+ // console.log('\t\t>> Comments count++:', this._cardsByNo.get(cardNo)!.comments)
+ }
}
- get MissedDays () {
- return this._missedDays
+ decrementComments (cardNo: number) {
+ if (this._cardsByNo.has(cardNo)) {
+ this._cardsByNo.get(cardNo)!.comments--
+ // console.log('\t\t>> Comments count--:', this._cardsByNo.get(cardNo)!.comments)
+ }
}
}
diff --git a/src/services/FirestoreApi.ts b/src/services/FirestoreApi.ts
index 224524a..4a9af5d 100644
--- a/src/services/FirestoreApi.ts
+++ b/src/services/FirestoreApi.ts
@@ -2,6 +2,27 @@ import firebase from 'firebase/app'
import { ILog, TPassage, TVote } from '../widgets/Shared'
import { AutoId } from '../lib/util'
import MMenuItem from '../models/MMenuItem'
+import MStudyMember from '../models/MStudyMember'
+
+/**
+ * @param void
+ * @returns a promise of studyMembers[]
+ */
+export async function getStudyMembers (): Promise> {
+ const qs = await firebase.firestore().collection('studyMembers').get()
+ const studyMembers = qs.docs.map((doc: any) => {
+ const o = doc.data()
+ return {
+ userFullname: o.Fullname,
+ userHandle: o.Handle,
+ startDateStr: o.StartDate,
+ uid: o.Uid,
+ repo: o.Repo,
+ active: o.Active
+ }
+ })
+ return studyMembers
+}
/**
* Queries firestore to get menu items.
diff --git a/src/services/GithubApi.ts b/src/services/GithubApi.ts
new file mode 100644
index 0000000..2e8a4df
--- /dev/null
+++ b/src/services/GithubApi.ts
@@ -0,0 +1,118 @@
+import { UserProgressDb, Tag } from '../models/UserProgress'
+import * as util from '../lib/util'
+import { getStudyMembers } from './FirestoreApi'
+
+export const uriAllCards = 'https://api.github.com/repos/studydash/cards/issues?milestone=1&sort=created&direction=desc&per_page=100'
+// const uriAllCards0 = 'https://api.github.com/repos/studydash/cards/issues?milestone=1&sort=created&direction=desc&per_page=100&creator=r002'
+// const uriAllCards1 = 'https://api.github.com/repos/studydash/cards/issues?milestone=1&sort=created&direction=desc&per_page=100&creator=anitabe404'
+// const uriAllCards2 = 'https://api.github.com/repos/studydash/cards/issues?milestone=1&sort=created&direction=desc&per_page=100&creator=shazahuang'
+
+// const dayCodes = ['U', 'M', 'T', 'W', 'H', 'F', 'S']
+
+// Render a member's record for the past 12 weeks * 7 days = 84 times
+export function RenderRecord (record: Map, startDate: string): string {
+ const dateCursor = new Date()
+ let s = ''
+ for (let i = 0; i < 7 * 12; i++) {
+ // Demarcate weeks on "Saturday | Sunday"
+ const weekBar = dateCursor.getDay() === 0 ? '|' : '' // Is it Sunday? If so, prefix with '|' to start a new week
+ if (record.has(util.getYearMonthDay(dateCursor))) {
+ s = weekBar + '*' + s
+ // s = dayCodes[dateCursor.getDay()] + s
+ } else {
+ if (i === 0) { // It is today
+ s = weekBar + '_' + s
+ } else {
+ s = weekBar + '❌' + s // a 'missed' day
+ }
+ }
+ dateCursor.setTime(dateCursor.getTime() - 86400 * 1000) // 86400 seconds in 24 hours
+
+ if (dateCursor.getTime() < Date.parse(startDate)) {
+ break
+ }
+ }
+ return s.replace(/^\|/g, '') // Trim any leading '|' from final output
+}
+
+export const tagMap = new Map()
+tagMap.set('movie trailer', {
+ name: 'movie trailer',
+ icon: '🎬'
+})
+tagMap.set('tv show', {
+ name: 'tv show',
+ icon: '📺'
+})
+tagMap.set('youtube', {
+ name: 'youtube',
+ icon: '▶'
+})
+tagMap.set('reading', {
+ name: 'reading',
+ icon: '📖'
+})
+tagMap.set('life', {
+ name: 'life',
+ icon: '🌳'
+})
+tagMap.set('travel', {
+ name: 'travel',
+ icon: '🛸'
+})
+tagMap.set('podcast notes', {
+ name: 'podcast notes',
+ icon: '🎙'
+})
+
+// Experimental; remove this later. Keeping here for future reference. 7/8/21
+// export async function getNumberOfCommentsPerCard (cardNo: number) {
+// const url = `https://api.github.com/repos/studydash/cards/issues/${cardNo}/comments`
+// const fetchTimeline = await fetch(url, {
+// // headers: {
+// // Accept: 'application/vnd.github.mockingbird-preview+json'
+// // }
+// })
+// const rs = await fetchTimeline.json()
+// console.log('>>rs', rs)
+// }
+
+// eslint-disable-next-line
+export async function getUpDb (d: Date): Promise {
+ const upDb = new UserProgressDb()
+ const members = await getStudyMembers()
+
+ for (const member of members) {
+ upDb.addUser(member)
+ }
+
+ // // If we are within the first week of a new month, also fetch cards from last month as well.
+ // const fetchCards0 = await fetch(uriAllCards + '&labels=' + util.getPeriod(util.getPrevMonth(d)))
+ // const fetchCards = await fetch(uriAllCards + '&labels=' + util.getPeriod(d))
+ // const allCards0 = await fetchCards0.json()
+ // const allCards = await fetchCards.json()
+
+ const fetchCards = await fetch(uriAllCards)
+ const allCards = await fetchCards.json()
+
+ for (const item of allCards) {
+ const tags = [] as Tag[]
+ for (const label of item.labels) {
+ if (tagMap.has(label.name)) {
+ tags.push(tagMap.get(label.name)!) // Why is assertion required here? 5/20/21
+ }
+ }
+
+ const cardInput = {
+ title: item.title,
+ userHandle: item.user.login,
+ number: item.number,
+ createdAt: item.created_at,
+ updatedAt: item.updated_at,
+ tags: tags,
+ comments: item.comments
+ }
+ upDb.getUser(cardInput.userHandle)!.setCard(cardInput)
+ }
+ return upDb
+}
diff --git a/src/widgets/Attribution.tsx b/src/widgets/Attribution.tsx
new file mode 100644
index 0000000..6dcbdff
--- /dev/null
+++ b/src/widgets/Attribution.tsx
@@ -0,0 +1,34 @@
+import React from 'react'
+import styled from 'styled-components'
+
+const FAttribution = styled.div`
+ position: absolute;
+ color: black;
+ background-color: #a2a1b1;
+ top: 0;
+ right: 0;
+ margin-top: 29px;
+ margin-right: 43px;
+ z-index: 99;
+ font-size: 14px;
+ line-height: 150%;
+ width: fit-content;
+ height: 28px;
+ padding-left: 10px;
+ padding-right: 10px;
+ box-sizing: border-box;
+ box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
+ white-space: nowrap;
+`
+
+const F14Link = styled.span`
+ font-size: 14px;
+`
+
+export const Attribution: React.VFC = () => {
+ return (
+
+ CSS Background Art by Luke Reid
+
+ )
+}
diff --git a/src/widgets/Footer.tsx b/src/widgets/Footer.tsx
new file mode 100644
index 0000000..6d237cc
--- /dev/null
+++ b/src/widgets/Footer.tsx
@@ -0,0 +1,43 @@
+import changelogUri from '../data/changelog.json'
+import React, { useEffect, useState } from 'react'
+import styled from 'styled-components'
+
+const FFooter = styled.div`
+ text-align: right;
+ position: absolute;
+ color: black;
+ bottom: 0;
+ right: 0;
+ margin-bottom: 12px;
+ margin-right: 20px;
+ z-index: 99;
+ font-size: 14px;
+ line-height: 150%;
+`
+
+type Commit = {
+ version: string
+ message: string
+ built: Date
+}
+
+export const Footer: React.VFC = () => {
+ const [commit, setCommit] = useState()
+
+ useEffect(() => {
+ fetch(changelogUri).then(rs => rs.json().then(payload => {
+ setCommit({
+ version: payload[0].version,
+ message: payload[0].message,
+ built: payload[0].built
+ })
+ }))
+ }, [])
+
+ return (
+
+ 🔖 {commit?.version} | {commit?.message}
+ 👷♂️ {commit?.built ? commit.built.toString() : 'NA'}
+
+ )
+}
diff --git a/src/widgets/Layout.tsx b/src/widgets/Layout.tsx
new file mode 100644
index 0000000..5350e2c
--- /dev/null
+++ b/src/widgets/Layout.tsx
@@ -0,0 +1,26 @@
+import styled from 'styled-components'
+
+export const FHorizontal = styled.div`
+ display: flex;
+ flex-direction: row;
+`
+
+// Temp hack. TODO: Refactor this later. 8/6/21
+export const FHorizontal2 = styled.div`
+ display: flex;
+ flex-direction: row;
+ width: 100vw;
+ height: 100vh;
+`
+
+export const FVertical = styled.div`
+ display: flex;
+ flex-direction: column;
+`
+
+export const Board = styled.div`
+ position: absolute;
+ display: flex;
+ left: 43px;
+ top: 29px;
+`
diff --git a/src/widgets/MemberBoard.tsx b/src/widgets/MemberBoard.tsx
new file mode 100644
index 0000000..54cdd39
--- /dev/null
+++ b/src/widgets/MemberBoard.tsx
@@ -0,0 +1,263 @@
+import React, { useState, useEffect } from 'react'
+import styled from 'styled-components'
+import '../providers/AuthContext'
+import firebase from 'firebase/app'
+import { StudyMember } from '../models/StudyMember'
+import { UserProgressDb, Tag } from '../models/UserProgress'
+import { getUpDb } from '../services/GithubApi'
+import * as util from '../lib/util'
+import * as l from '../widgets/Layout'
+import { SceneViz } from './SceneViz'
+
+const FCard = styled.div`
+ width: 205px;
+ height: 28px;
+
+ display: flex;
+ align-items: center;
+
+ font-family: 'Chakra Petch', sans-serif;
+ font-style: normal;
+ font-weight: 400;
+ /* line-height: 29px; */
+ color: #FFFFFF;
+ margin-right: 11px;
+ margin-bottom: 11px;
+ box-sizing: border-box;
+ box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
+`
+
+const FEmpty = styled(FCard)`
+ width: 34px;
+ height: 28px;
+ background: none;
+ box-shadow: none;
+`
+
+const FDay = styled(FCard)`
+ width: 34px;
+ height: 28px;
+ justify-content: center;
+
+ font-family: 'Chakra Petch', sans-serif;
+ font-style: normal;
+ font-weight: 300;
+ line-height: 11.7px;
+ color: #FFFFFF;
+
+ font-size: 9px;
+ background: #632242;
+`
+
+const FMemberCard = styled(FCard)`
+ background: #632242;
+ font-size: 15px;
+ padding-left: 7px;
+ padding-top: 3px;
+`
+
+const FEntryCard = styled(FCard)`
+ background: #274867;
+ font-size: 12px;
+ padding-left: 7px;
+`
+
+const FMissedCard = styled(FCard)`
+ background: #632242;
+ font-size: 12px;
+ padding-left: 7px;
+`
+
+const FPendingCard = styled(FCard)`
+ background: #597252;
+ font-size: 12px;
+ padding-left: 7px;
+`
+
+const MissedCard: React.VFC = () => {
+ return (
+
+ No card today! 😢😦🙁
+
+ )
+}
+
+const PendingCard: React.VFC = () => {
+ return (
+
+ Today's card pending! 😀😁😄
+
+ )
+}
+
+const F15Link = styled.span`
+ font-size: 15px;
+`
+
+type TMemberCard = {
+ name: string
+ userHandle: string
+ uid: string
+}
+const MemberCard: React.FC = (props) => {
+ return (
+
+ {props.userHandle}
+
+ )
+}
+
+type TEntryCard = {
+ title: string
+ userHandle: string
+ number: number
+ created: Date
+ updated: Date
+ tags: Tag[]
+ comments: number
+ repo: string
+}
+
+const EntryCard: React.FC = (props) => {
+ const title = props.title.length > 33 ? props.title.substr(0, 30) + '...' : props.title
+ return (
+
+ {title}
+
+ )
+}
+
+const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
+
+export const MemberBoard: React.VFC = () => {
+ const [members, setMembers] = useState>([])
+ const [curDate] = useState(new Date())
+ const [upDb, setUpDb] = useState(new UserProgressDb())
+
+ // Load cards
+ useEffect(() => {
+ console.log('>> Changing date periods', curDate)
+ Promise.resolve(getUpDb(curDate)).then(rs => setUpDb(rs))
+ history.pushState({}, '', '/studydash/?d=' + util.getYearMonth(curDate))
+ window.parent.postMessage(window.location.href, '*')
+ }, [curDate])
+
+ // Listen for latest member stats updates from Firestore
+ useEffect(() => {
+ const unsubscribe = firebase.firestore().collection('studyMembers')
+ .onSnapshot((querySnapshot) => {
+ const members = [] as Array
+ querySnapshot.forEach((doc) => {
+ const m = doc.data()
+ members.push({
+ userFullname: m.Fullname,
+ userHandle: m.Handle,
+ startDateStr: m.StartDate,
+ uid: m.Uid,
+ repo: m.Repo,
+ active: m.Active,
+ streakCurrent: {
+ days: m.StreakCurrent.Days,
+ startDate: m.StreakCurrent.StartDate,
+ endDate: m.StreakCurrent.EndDate
+ },
+ streakMax: {
+ days: m.StreakMax.Days,
+ startDate: m.StreakMax.StartDate,
+ endDate: m.StreakMax.EndDate
+ },
+ recordCount: m.RecordCount,
+ daysJoined: m.DaysJoined,
+ lastCard: {
+ date: m.LastCard.Date,
+ number: m.LastCard.Number
+ },
+ record: new Map(Object.entries(m.Record))
+ })
+ })
+ // console.log('\t>> member[0].record', members[0].record)
+ setMembers(members.sort((a, b) => Date.parse(a.startDateStr) - Date.parse(b.startDateStr)))
+ console.log('>> member stats update received:', (new Date()).toISOString())
+ })
+ return () => unsubscribe()
+ }, [])
+
+ const dateRange = util.getDateRangeOneWeek(curDate)
+ // console.log('>> dateRange', dateRange)
+ return (
+
+
+
+
+
+ {
+ dateRange.map((day: util.TDay, i: number) =>
+
+ {days[day.dayNo].substring(0, 3)}
+ {day.dateStr.replace('/2021', '')}
+
+ )
+ }
+
+
+ {
+ members.map((m: StudyMember, i: number) =>
+ m.active && Date.parse(m.startDateStr) <= util.getMonthLastDay(curDate).getTime()
+ ?
+
+ {
+ dateRange.map((day: util.TDay, i: number) => renderCard(upDb, m, day, i))
+ }
+
+ :
+ )
+ }
+
+
+ )
+}
+
+/**
+ * Extract number of submitted cards in the past eight day per each member
+ */
+function extractDensities (upDb:UserProgressDb, members: Array, dateRange: Array): Array {
+ const rs = [] as Array
+
+ members.forEach((m: StudyMember) => {
+ if (m.active) {
+ let cardCount = 0
+ dateRange.forEach((day: util.TDay) => {
+ const card = upDb.getUser(m.userHandle)?.getCardByDate(day.dateStr) ?? null
+ if (card) {
+ cardCount++
+ }
+ })
+ rs.push(cardCount)
+ }
+ })
+ // console.log('>> rs', rs)
+ return rs
+}
+
+function renderCard (upDb:UserProgressDb, m:StudyMember, day: util.TDay, i: number): JSX.Element {
+ const today = new Date()
+ const rs = []
+ const card = upDb.getUser(m.userHandle)?.getCardByDate(day.dateStr) ?? null
+ if (card) {
+ rs.push()
+ } else if (today.toLocaleDateString() === day.dateStr) {
+ rs.push()
+ } else if (Date.parse(day.dateStr) >= Date.parse(m.startDateStr)) {
+ rs.push()
+ } else {
+ console.log('>> render empty card!')
+ // rs.push() // No longer needed? 7/31/21
+ }
+
+ return (
+
+ {rs}
+
+ )
+}
diff --git a/src/widgets/MembersPane.tsx b/src/widgets/MembersPane.tsx
new file mode 100644
index 0000000..a0c7233
--- /dev/null
+++ b/src/widgets/MembersPane.tsx
@@ -0,0 +1,67 @@
+import React from 'react'
+import { StudyMember, Streak } from '../models/StudyMember'
+import { RenderRecord } from '../services/GithubApi'
+import styled from 'styled-components'
+
+const FColor = styled.span`
+ color: orange;
+`
+
+const FGreen = styled.span`
+ color: #5ff708;
+`
+
+const FOrange = styled.span`
+ color: #ec7240;
+`
+
+const FStreak: React.FC = (streak) => {
+ return (
+ streak.days !== 0
+ ?
+ {streak.days}
+
+ : {streak.days}
+ )
+}
+
+type TMembersPane = {
+ members: Array
+}
+const MembersPane: React.FC = (props) => {
+ return (
+
+ {
+ props.members.map((member: StudyMember, i: number) => {
+ const handle = member.userHandle
+ const rs = []
+ const content =
+
+
-
+ 🧙♂️
+ Member #{i}: {handle} |
+ Start: {(new Date(member.startDateStr)).toDateString()} |
+ Max Streak: |
+ Record: {member.recordCount}/{member.daysJoined} |
+ Current Streak: days |
+ Latest Card: #{member.lastCard.number} ({member.lastCard.date})
+
+
-
+ {RenderRecord(member.record, member.startDateStr)}
+
+
+ if (member.active) {
+ rs.push(content)
+ }
+ // else {
+ // rs.push({content})
+ // }
+ return (rs)
+ })
+ }
+
+ )
+}
+
+export default MembersPane
diff --git a/src/widgets/SceneViz.tsx b/src/widgets/SceneViz.tsx
new file mode 100644
index 0000000..95805db
--- /dev/null
+++ b/src/widgets/SceneViz.tsx
@@ -0,0 +1,79 @@
+import React, { useEffect, useState } from 'react'
+
+type TSceneViz = {
+ densityIndices: Array
+}
+
+export const SceneViz: React.FC = (props) => {
+ const [index, setIndex] = useState(0)
+
+ function tick () {
+ if (props.densityIndices.length > 0) {
+ setIndex(oldIndex => {
+ const newIndex = (oldIndex + 1) % props.densityIndices.length
+ // console.log('\t>> densities/newIndex set:', props.densityIndices, newIndex)
+ return newIndex
+ })
+ }
+ }
+
+ useEffect(() => {
+ const interval = setInterval(() => tick(), 1000 * 10) // Update every 10 seconds. Hack: Sync with `.trees` animation in `landscape.css` 🤦♂️
+ return () => {
+ clearInterval(interval)
+ }
+ }, [])
+
+ const trees = []
+ for (let i = 1; i < props.densityIndices[index]; i++) {
+ const x = getRandomInt(-315, 640)
+ const y = getRandomInt(-30, -15)
+ const scale = getRandomFloat(0.5, 1.5)
+ trees.push()
+ }
+
+ return (
+
+ {props.densityIndices[index] > 0
+ ?
+ : <>> // No trees are painted!
+ }
+
+ )
+}
+
+function getRandomInt (min: number, max: number) {
+ min = Math.ceil(min)
+ max = Math.floor(max)
+ return Math.floor(Math.random() * (max - min + 1)) + min
+}
+
+function getRandomFloat (min: number, max: number) {
+ return Math.random() * (max - min) + min
+}
diff --git a/webpack.common.js b/webpack.common.js
index 3b7d812..9c38726 100644
--- a/webpack.common.js
+++ b/webpack.common.js
@@ -5,7 +5,8 @@ module.exports = {
entry: {
app: './src/index.tsx',
wattpad: './src/wattpad.tsx',
- StudyGroup: './src/StudyGroup.tsx'
+ StudyGroup: './src/StudyGroup.tsx',
+ StudyDash: './src/StudyDash.tsx'
},
plugins: [
new HtmlWebpackPlugin({
@@ -17,17 +18,38 @@ module.exports = {
}),
new HtmlWebpackPlugin({
title: 'Wattpad POC',
- filename: 'wattpad.html',
+ filename: './wattpad/index.html',
template: './public/wattpad.html',
favicon: './public/favicon.ico',
chunks: ['wattpad']
}),
new HtmlWebpackPlugin({
- title: 'Study Group 00',
- filename: 'study-group.html',
+ title: 'StudyGroup',
+ filename: './studygroup/index.html',
template: './public/study-group.html',
favicon: './public/favicon.ico',
chunks: ['StudyGroup']
+ }),
+ new HtmlWebpackPlugin({
+ title: 'StudyDash',
+ filename: './studydash/index.html',
+ template: './public/studydash.html',
+ favicon: './public/favicon.ico',
+ chunks: ['StudyDash']
+ }),
+ new HtmlWebpackPlugin({
+ title: 'Preview v1',
+ filename: './v1/index.html',
+ template: './public/v1/index.html',
+ favicon: './public/favicon.ico',
+ chunks: []
+ }),
+ new HtmlWebpackPlugin({
+ title: 'Preview v2',
+ filename: './v2/index.html',
+ template: './public/v2/index.html',
+ favicon: './public/favicon.ico',
+ chunks: []
})
],
module: {