Skip to content

Commit

Permalink
Merge pull request #8 from ellielle/boss-tab
Browse files Browse the repository at this point in the history
  • Loading branch information
ellielle authored Jun 7, 2024
2 parents b05e18d + f694c89 commit aaadd57
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 96 deletions.
76 changes: 0 additions & 76 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"context"
"errors"
"log"
"time"

Expand Down Expand Up @@ -44,81 +43,6 @@ func (a *App) startup(ctx context.Context) {
runtime.EventsEmit(ctx, "domready")
}

// GetArchmagesList returns the data from the archmage leaderboard
func (a *App) ArchmagesList() []bootdevapi.Archmage {
list, err := bootdevapi.Archmages(a.cache)
if err != nil {
log.Fatal(err)
}

return list
}

// GlobalStats returns the general global stats from the leaderboard
func (a *App) GlobalStats() bootdevapi.GlobalStats {
stats, err := bootdevapi.GetGeneralStats(a.cache)
if err != nil {
log.Fatal(err)
}

return stats
}

// TopDailyLearners returns the top 30 users based on exp earned
func (a *App) TopDailyLearners() []bootdevapi.LeaderboardUser {
list, err := bootdevapi.GetDailyStats(a.cache)
if err != nil {
log.Fatal(err)
}

return list
}

// TopCommunity returns the top 30 members of the discord community,
// based on a variety of factors such as activity
func (a *App) TopCommunity() []bootdevapi.Archon {
list, err := bootdevapi.GetDiscordLeaderboard(a.cache)
if err != nil {
log.Fatal(err)
}

return list
}

// LoginUserWithOTP takes the user's one-time password and exchanges it
// for an access_token and refresh_token from Boot.Dev
func (a *App) LoginUserWithOTP(OTP string) (bool, error) {
tokens, err := login.ExchangeOTPForToken(OTP)
if err != nil {
return false, errors.New("error exchanging OTP for Token")
}
if tokens.AccessToken == "" {
return false, errors.New("empty token after exchanging with OTP")
}

err = login.SaveTokens(tokens)
if err != nil {
return false, err
}
// set token in App struct so it can be used for
// user-specific queries
a.tokens = *tokens

return true, nil
}

func (a *App) LoginUserWithToken() (bool, error) {
// Don't waste calls and assume the 1 hour token is invalid
tokens, err := login.RefreshToken()
if err != nil {
return false, err
}

a.tokens = *tokens

return true, nil
}

// UserData sends an authenticated request to gather the user's
// data for display in the app.
func (a *App) UserData() (bootdevapi.UserData, error) {
Expand Down
21 changes: 12 additions & 9 deletions frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,33 @@
import Tabs from "./components/UI/Tabs.svelte";
import { User } from "./stores/user.js";
let userPromise = UserData().then((result) => ($User.userData = result));
$: loggedIn = $User.isLoggedIn;
onMount(() => {
// Attempt to log the user on mount by refreshing their
// access token
// FIXME: signing in isn't reactive and doesn't change pages
// the issue seems to be frontend related. Data is fine on backend
// but comes empty to frontend
// * Sign in works fine from file though
LoginUserWithToken().then((result) => ($User.isLoggedIn = result));
UserData().then((result) => ($User.userData = result));
console.log("userdata", $User.userData);
});
</script>

<main>
<!-- insert Tab component. The rest of the content is rendered via Tabs -->
{#await userPromise}
<p>Loading...</p>
{:then _}
{#if loggedIn}
<Tabs />
{/if}
{#if !$User.isLoggedIn || typeof $User.isLoggedIn != "boolean"}
<div class="container-buddy">
<div class="menu-container">
<!-- show user login button if automatic sign in fails -->
{#if !$User.isLoggedIn || typeof $User.isLoggedIn != "boolean"}
<Login bind:loggedIn={$User.isLoggedIn} />
{/if}
<Login loggedIn={$User.isLoggedIn} />
</div>
</div>
{/await}
{/if}
</main>

<style>
Expand Down
21 changes: 15 additions & 6 deletions frontend/src/components/Login.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@
// Empty field initially for the one-time password
let otpField = "";
let error = "";
// loginUser takes a user's OTP, trades it for an access token which
// is saved for futher use, and marks the user as logged in
function loginUser() {
LoginUserWithOTP(otpField).then((result) => (loggedIn = result));
LoginUserWithOTP(otpField)
.then((result) => (loggedIn = result))
.catch(() => (error = "Invalid or expired OTP"));
}
</script>

<main>
<section>
<div class="menu-item btn-login">
<p>
You aren't currently logged in! You will only have limited
functionality.
</p>
<p>You aren't currently logged in!</p>
<p>
Please
<a
Expand All @@ -43,18 +44,26 @@
type="text"
placeholder="Boot.Dev CLI Code"
bind:value={otpField}
style="color:black"
style="color: black"
/>
<button
class="text-primary-500 border rounded px-2 py-1.5"
on:click={loginUser}>Sign in</button
>
</div>
</section>
{#if error !== ""}
<section>
<div class="error">{error}</div>
</section>
{/if}
</main>

<style>
.menu-item > p {
margin-bottom: 0.5rem;
}
.error {
color: red;
}
</style>
21 changes: 18 additions & 3 deletions frontend/src/components/content/BossBattle.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,25 @@
</script>

<main>
<p>Boss Battles!</p>
{#await promise}
<p>Loading...</p>
{:then b}
<p>{b.Event}</p>
{:then battle}
{#if new Date(battle.Event.ExpiresAt) > new Date(Date.now()) || new Date(battle.Event.DefeatedAt) < new Date(Date.now())}
<a
href="https://www.boot.dev/lore/{battle.Event.Boss.LoreSlug}"
class="text-primary-500"
target="_blank"
>
{battle.Event.Boss.Name}
</a>
has been defeated!
<p>You gained {battle.XPUser} XP during the fight.</p>
<p>
The final blow was dealt on {new Date(
battle.Event.DefeatedAt,
).toLocaleDateString()}.
</p>
<!-- TODO: Add active boss battle stats -->
{/if}
{/await}
</main>
4 changes: 3 additions & 1 deletion frontend/src/components/content/General.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<script>
import { User } from "../../stores/user";
$: username = $User.userData.DiscordUserHandle;
</script>

<main>
Welcome to Boot.dev Buddy, {$User.userData.DiscordUserHandle}!
{console.log($User)}
Welcome to Boot.dev Buddy, {username}!
</main>
1 change: 1 addition & 0 deletions internal/bootdevapi/bootdevapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func TestMain(t *testing.T) {
"achievements": {input: "achievements", want: "https://api.boot.dev/v1/users/achievements"},
"feed": {input: "feed", want: "https://api.boot.dev/v1/lesson_completion_feed"},
"progress": {input: "progress", want: "https://api.boot.dev/v1/courses_progress"},
"boss": {input: "boss", want: "https://api.boot.dev/v1/boss_events_progress"},
}

for name, tc := range tests {
Expand Down
1 change: 0 additions & 1 deletion internal/bootdevapi/daily.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
// DailyStats returns a slice of the top 30 learners
// from Boot.Dev's leaderboard page.
// As this won't change often, the refresh time on this can be low.
// TODO: The list will be paginated to only show 10
func GetDailyStats(c cache.Cache) ([]LeaderboardUser, error) {
// daily stats leaderboard URL
dailyStatsLB, err := BootDevAPIMap("daily")
Expand Down
41 changes: 41 additions & 0 deletions login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package main

import (
"errors"

"github.com/ellielle/bootdev-buddy/internal/login"
)

// LoginUserWithOTP takes the user's one-time password and exchanges it
// for an access_token and refresh_token from Boot.Dev
func (a *App) LoginUserWithOTP(OTP string) (bool, error) {
tokens, err := login.ExchangeOTPForToken(OTP)
if err != nil {
return false, errors.New("error exchanging OTP for Token")
}
if tokens.AccessToken == "" {
return false, errors.New("empty token after exchanging with OTP")
}

err = login.SaveTokens(tokens)
if err != nil {
return false, err
}
// set token in App struct so it can be used for
// user-specific queries
a.tokens = *tokens

return true, nil
}

func (a *App) LoginUserWithToken() (bool, error) {
// Don't waste calls and assume the 1 hour token is invalid
tokens, err := login.RefreshToken()
if err != nil {
return false, err
}

a.tokens = *tokens

return true, nil
}
48 changes: 48 additions & 0 deletions stats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package main

import (
"log"

"github.com/ellielle/bootdev-buddy/internal/bootdevapi"
)

// GetArchmagesList returns the data from the archmage leaderboard
func (a *App) ArchmagesList() []bootdevapi.Archmage {
list, err := bootdevapi.Archmages(a.cache)
if err != nil {
log.Fatal(err)
}

return list
}

// GlobalStats returns the general global stats from the leaderboard
func (a *App) GlobalStats() bootdevapi.GlobalStats {
stats, err := bootdevapi.GetGeneralStats(a.cache)
if err != nil {
log.Fatal(err)
}

return stats
}

// TopDailyLearners returns the top 30 users based on exp earned
func (a *App) TopDailyLearners() []bootdevapi.LeaderboardUser {
list, err := bootdevapi.GetDailyStats(a.cache)
if err != nil {
log.Fatal(err)
}

return list
}

// TopCommunity returns the top 30 members of the discord community,
// based on a variety of factors such as activity
func (a *App) TopCommunity() []bootdevapi.Archon {
list, err := bootdevapi.GetDiscordLeaderboard(a.cache)
if err != nil {
log.Fatal(err)
}

return list
}

0 comments on commit aaadd57

Please sign in to comment.