diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..86de467 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Noelle L'Amour + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 0709391..7a5b4bf 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,6 @@ Access tokens are only valid for 60 minutes. Once invalid, the app will attempt - [x] View other mages' profiles on leaderboard by click - [ ] Fancier and more formatted stats - [ ] Buff timers -- [ ] More(?) ## Live Development @@ -74,6 +73,6 @@ func main() { To build a redistributable, production mode package, use `wails build`. -## Why? +## Motivation I'm a fan of monitoring software, making it look nice, and making it easy to get the information you want at a glance. I thought having a buddy app to the [Boot.dev](https://boot.dev) curriculum that could give a student more sense of community, while also monitoring your own progress, would be fun. diff --git a/app.go b/app.go index 2201970..97dbbc6 100644 --- a/app.go +++ b/app.go @@ -49,7 +49,7 @@ func (a *App) startup(ctx context.Context) { func (a *App) UserData() (bootdevapi.UserData, error) { userData, err := bootdevapi.UserInfo(a.cache, a.tokens.AccessToken) if err != nil { - return bootdevapi.UserData{}, nil + return bootdevapi.UserData{}, err } return userData, nil diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index cd3a4dc..9d80f66 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -10,19 +10,21 @@ UserData().then((result) => ($User.userData = result)); } - // FIXME: doesn't run update function - - onMount(() => { + // FIXME: i'm bad at svelte reactivity + onMount(async () => { // Attempt to log the user on mount by refreshing their // access token LoginUserWithToken().then((result) => ($User.isLoggedIn = result)); - UserData().then((result) => ($User.userData = result)); + UserData() + .then((result) => ($User.userData = result)) + .catch((e) => console.log(e)); + // Update user data every 60 seconds const refreshInterval = setInterval(() => { if ($User.isLoggedIn) { updateUserData(); } - }, 60000); + }, 30000); return () => { clearInterval(refreshInterval); @@ -31,13 +33,16 @@
- {#if $User.isLoggedIn} + {#if $User.isLoggedIn && $User.userData.Handle !== ""} + {:else if $User.isLoggedIn} +

Loading data...

+

Data not loading? Please restart the app!

{:else}
{/if} diff --git a/frontend/src/assets/images/armor.png b/frontend/src/assets/images/armor.png new file mode 100644 index 0000000..cc569c2 Binary files /dev/null and b/frontend/src/assets/images/armor.png differ diff --git a/frontend/src/assets/images/flame.png b/frontend/src/assets/images/flame.png new file mode 100644 index 0000000..00d9246 Binary files /dev/null and b/frontend/src/assets/images/flame.png differ diff --git a/frontend/src/assets/images/salmon.webp b/frontend/src/assets/images/salmon.webp new file mode 100644 index 0000000..5ed4d90 Binary files /dev/null and b/frontend/src/assets/images/salmon.webp differ diff --git a/frontend/src/assets/images/seerstone.webp b/frontend/src/assets/images/seerstone.webp new file mode 100644 index 0000000..1ebdee9 Binary files /dev/null and b/frontend/src/assets/images/seerstone.webp differ diff --git a/frontend/src/assets/images/xppotion.webp b/frontend/src/assets/images/xppotion.webp new file mode 100644 index 0000000..0e0b941 Binary files /dev/null and b/frontend/src/assets/images/xppotion.webp differ diff --git a/frontend/src/components/Login.svelte b/frontend/src/components/Login.svelte index 6138e4a..dea299d 100644 --- a/frontend/src/components/Login.svelte +++ b/frontend/src/components/Login.svelte @@ -1,6 +1,6 @@ diff --git a/frontend/src/components/UI/Inventory.svelte b/frontend/src/components/UI/Inventory.svelte new file mode 100644 index 0000000..e776c1f --- /dev/null +++ b/frontend/src/components/UI/Inventory.svelte @@ -0,0 +1,88 @@ + + +
+
+
{timer !== null ? timer : "00:00"}
+
+ XP Potions + {$User.xpPotion} +
+
+ Seer Stone + {$User.seerStones} +
+
+ Baked Salmon + {$User.bakedSalmon} +
+
+ XP Potions + {$User.frozenFlame} +
+
+
diff --git a/frontend/src/components/UI/Tabs.svelte b/frontend/src/components/UI/Tabs.svelte index 122baab..6a7df3a 100644 --- a/frontend/src/components/UI/Tabs.svelte +++ b/frontend/src/components/UI/Tabs.svelte @@ -6,6 +6,7 @@ import Avatar from "../UI/Avatar.svelte"; import Stats from "../content/Stats.svelte"; import Courses from "../content/Courses.svelte"; + import Inventory from "./Inventory.svelte"; import { Tab, TabGroup } from "@skeletonlabs/skeleton"; import { User } from "../../stores/user"; import { LogoutUser, CloseApp } from "../../../wailsjs/go/main/App.js"; @@ -60,6 +61,13 @@ {/if} + + + +
+ +
+
{#if $User.isLoggedIn} diff --git a/frontend/src/components/UI/Timers.svelte b/frontend/src/components/UI/Timers.svelte new file mode 100644 index 0000000..49f5fcd --- /dev/null +++ b/frontend/src/components/UI/Timers.svelte @@ -0,0 +1,2 @@ + diff --git a/frontend/src/components/content/Courses.svelte b/frontend/src/components/content/Courses.svelte index 50b0a96..a21da1c 100644 --- a/frontend/src/components/content/Courses.svelte +++ b/frontend/src/components/content/Courses.svelte @@ -92,8 +92,6 @@ $: if (Object.keys(progress).length > 0) { init = true; - - console.log("progress: ", progress.Progress); } // handles launching URLs outside of the app diff --git a/frontend/src/stores/user.js b/frontend/src/stores/user.js index ddf182f..e15f983 100644 --- a/frontend/src/stores/user.js +++ b/frontend/src/stores/user.js @@ -1,50 +1,58 @@ import { writable } from "svelte/store"; /** - * @typedef {Object} UserData - * @property {string} DiscordUserHandle - * @property {string | null} SyncedGoogleID - * @property {number} SyncedGithubID - * @property {Date} ManualProSubExpiresAt + * @typedef {Object} UserData + * @property {string} DiscordUserHandle + * @property {string | null} SyncedGoogleID + * @property {number} SyncedGithubID + * @property {Date} ManualProSubExpiresAt * @property {Date | null} LifetimeProSubCreatedAt - * @property {Date} MembershipExpiresAt - * @property {string} Email - * @property {string} Currency - * @property {number} Xp - * @property {number} Level - * @property {number} XPForLevel - * @property {number} XPTotalForLevel - * @property {string} Role - * @property {number} Gems - * @property {number} Armor - * @property {Date} CreatedAt - * @property {Date} UpdatedAt - * @property {string} FirstName - * @property {string} LastName - * @property {string} Handle - * @property {string} Bio - * @property {string} JobTitle - * @property {string} Location - * @property {string} City - * @property {string} Country - * @property {string} GithubHandle - * @property {string} WebsiteURL - * @property {string} ProfileImageURL - * @property {boolean} IsSubscribed - * @property {boolean} GithubSynced - * + * @property {Date} MembershipExpiresAt + * @property {string} Email + * @property {string} Currency + * @property {number} Xp + * @property {number} Level + * @property {number} XPForLevel + * @property {number} XPTotalForLevel + * @property {string} Role + * @property {number} Gems + * @property {number} Armor + * @property {Date} CreatedAt + * @property {Date} UpdatedAt + * @property {string} FirstName + * @property {string} LastName + * @property {string} Handle + * @property {string} Bio + * @property {string} JobTitle + * @property {string} Location + * @property {string} City + * @property {string} Country + * @property {string} GithubHandle + * @property {string} WebsiteURL + * @property {string} ProfileImageURL + * @property {boolean} IsSubscribed + * @property {boolean} GithubSynced + * */ /** * @typedef {Object} User * @property {boolean} isArchmage * @property {boolean} isLoggedIn + * @property {number} seerStones + * @property {number} bakedSalmon + * @property {number} xpPotion + * @property {Date|null} xpTimer + * @property {number} frozenFlame * @property {UserData} userData */ export const User = writable({ isArchmage: false, isLoggedIn: false, - userData: {} -}) - - + seerStones: 0, + bakedSalmon: 0, + xpPotion: 0, + xpTimer: null, + frozenFlame: 0, + userData: {}, +}); diff --git a/internal/bootdevapi/api_remap_structs.go b/internal/bootdevapi/api_remap_structs.go index cdbc854..f09e77a 100644 --- a/internal/bootdevapi/api_remap_structs.go +++ b/internal/bootdevapi/api_remap_structs.go @@ -199,3 +199,30 @@ type ProgressDetail struct { NumMax int `json:"NumMax"` LastViewedLessonUUID string `json:"LastViewedLessonUUID"` } + +// List of potions user has in effect / unused +type PotionList struct { + ActiveXPPotions []XPPotion `json:"ActiveXPPotions"` + NumUnusedXPPotion int `json:"NumUnusedXPPotion"` +} + +// XP Potion information +type XPPotion struct { + ID string `json:"ID"` + CreatedAt time.Time `json:"CreatedAt"` + UpdatedAt time.Time `json:"UpdatedAt"` + UserUUID string `json:"UserUUID"` + ExpiresAt time.Time `json:"ExpiresAt"` + UsedAt time.Time `json:"UsedAt"` + SoldAt any `json:"SoldAt"` +} + +// Frozen flame data. Included since it seems it will +// be used in the future +type FrozenFlame struct { + UUID string `json:"UUID"` + UserUUID string `json:"UserUUID"` + CreatedAt time.Time `json:"CreatedAt"` + UsedAt any `json:"UsedAt"` + SoldAt any `json:"SoldAt"` +} diff --git a/inventory.go b/inventory.go new file mode 100644 index 0000000..01b6690 --- /dev/null +++ b/inventory.go @@ -0,0 +1,118 @@ +package main + +import ( + "encoding/json" + "io" + "net/http" + "strconv" + + "github.com/ellielle/bootdev-buddy/internal/bootdevapi" +) + +const SALMON_URL = "https://api.boot.dev/v1/user_baked_salmon" + +const STONES_URL = "https://api.boot.dev/v1/user_seer_stones" + +const XP_URL = "https://api.boot.dev/v1/user_xp_potion" + +const FLAME_URL = "https://api.boot.dev/v1/user_frozen_flame" + +// SeerStones gets the user's current amount of Seer Stones +func (a *App) SeerStones() (int, error) { + req, err := http.NewRequest("GET", STONES_URL, nil) + if err != nil { + return 0, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+a.tokens.AccessToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + stones, err := io.ReadAll(resp.Body) + if err != nil { + return 0, err + } + + num, err := strconv.Atoi(string(stones)) + + return num, err +} + +func (a *App) BakedSalmon() (int, error) { + req, err := http.NewRequest("GET", SALMON_URL, nil) + if err != nil { + return 0, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+a.tokens.AccessToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + salmon, err := io.ReadAll(resp.Body) + if err != nil { + return 0, err + } + + num, err := strconv.Atoi(string(salmon)) + return num, err +} + +func (a *App) XPPotion() (bootdevapi.PotionList, error) { + var potions bootdevapi.PotionList + + req, err := http.NewRequest("GET", XP_URL, nil) + if err != nil { + return bootdevapi.PotionList{}, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+a.tokens.AccessToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return bootdevapi.PotionList{}, err + } + defer resp.Body.Close() + + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&potions) + if err != nil { + return bootdevapi.PotionList{}, err + } + + return potions, nil +} + +func (a *App) FrozenFlames() (int, error) { + req, err := http.NewRequest("GET", FLAME_URL, nil) + if err != nil { + return 0, err + } + + var frozenFlames []bootdevapi.FrozenFlame + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+a.tokens.AccessToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&frozenFlames) + + // only need the length of the FrozenFlames slice + return len(frozenFlames), err +}