Browse Source

a bunch of work

master
Stephanie Gredell 1 year ago
parent
commit
af855e1da5
  1. 9
      assets/css/style.css
  2. 194
      auth/sessions.go
  3. 1
      db/db.go
  4. 1
      main.go
  5. 81
      models/goal.go
  6. 84
      pages/goals.go
  7. 30
      pages/home.go
  8. 20
      pages/welcome.go
  9. 148
      templates/goal.html
  10. 23
      templates/goals.html
  11. 13
      templates/header.html
  12. 116
      templates/upload-goal.html
  13. 2
      templates/welcome.html

9
assets/css/style.css

@ -28,7 +28,16 @@ body { @@ -28,7 +28,16 @@ body {
font-family: "Helvetica Neue", "Helvetica", serif;
margin: 0;
align-self: center;
}
.title-link {
font-size: 1.75rem;
color: #000;
text-decoration: none;
}
.title-link:hover {
color: #666;
}
.login-container {

194
auth/sessions.go

@ -3,205 +3,33 @@ package auth @@ -3,205 +3,33 @@ package auth
import (
"encoding/json"
"fmt"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/gorilla/securecookie"
"github.com/markbates/goth"
"log"
"net/url"
"sponsorahacker/config"
"sponsorahacker/db"
"strconv"
"time"
)
type SessionStore interface {
CreateSession(*gin.Context) error
GetSession(string) (Session, error)
DeleteSession(string) error
}
type SessionManager struct {
DB *db.DBClient
}
type Session struct {
sessionId string
sessionData string
createdOn string
modifiedOn string
expiresOn string
}
type User struct {
NickName string `json:"NickName"`
Provider string `json:"Provider"`
Provider_userid string `json:"UserID"`
Sid string `json:"Sid"`
}
var secureCookie *securecookie.SecureCookie
func NewSessionManager(db *db.DBClient) SessionManager {
return SessionManager{DB: db}
}
func (s *SessionManager) CreateSession(user goth.User, c *gin.Context) error {
sessionData, err := json.Marshal(user)
if err != nil {
fmt.Printf("error marshalling user: %v", err)
}
auth := Session{
sessionData: string(sessionData),
createdOn: time.Now().Format("20060102150405"),
modifiedOn: time.Now().Format("20060102150405"),
expiresOn: user.ExpiresAt.Format("20060102150405"),
}
result, err := s.DB.Exec(`
INSERT INTO sessions (sessionData, createdOn, modifiedOn, expiresOn)
VALUES (?, ?, ?, ?);`, auth.sessionData, auth.createdOn, auth.modifiedOn, auth.expiresOn)
if result == nil {
fmt.Printf("error getting result from database while creating the session: %v", err)
return err
}
spuser, err := GetUserFromSession(auth)
err = s.SaveUserIfNotExist(spuser)
if err != nil {
log.Printf("error creating session: %v", err)
return err
}
sessionId, err := result.LastInsertId()
if err != nil {
fmt.Printf("error getting session id from database while creating the session: %v", err)
}
hash := []byte(config.GetEnvVar("COOKIE_HASH"))
block := []byte(config.GetEnvVar("COOKIE_BLOCK"))
secureCookie = securecookie.New(hash, block)
cookieValue := map[string]string{
"sessionId": strconv.Itoa(int(sessionId)),
}
encoded, err := secureCookie.Encode("_session", cookieValue)
if err != nil {
fmt.Printf("error encoding cookie value: %v", err)
return err
}
c.SetCookie("_session", encoded, 3600, "/", "localhost", false, true)
return nil
}
func (s *SessionManager) GetSession(session sessions.Session) (Session, error) {
// query for one row
result, err := s.DB.Query(`SELECT sessionData FROM sessions WHERE sessionId=$1 LIMIT 1`, session.ID())
func HydrateUser(userJson string) *User {
user := User{}
// if err, then return an empty struct
if err != nil {
return Session{}, err
if err := json.Unmarshal([]byte(userJson), &user); err != nil {
fmt.Println("serialization in hydrating user error = ", err)
return nil
}
// else go through the results and create a Session
for result.Next() {
var s Session
// unless there is an error, of course, then return an empty struct
if err := result.StructScan(&s); err != nil {
return Session{}, err
}
return s, nil
}
// if we get nothing, well, we go nothing
return Session{}, nil
return &user
}
func GetUserFromSession(session Session) (User, error) {
var user User
fmt.Println(session.sessionData)
err := json.Unmarshal([]byte(session.sessionData), &user)
func (u *User) GetSid() int {
strSid, err := strconv.Atoi(u.Sid)
if err != nil {
fmt.Println("error unmarshalling session data for user", err)
return User{}, err
fmt.Println("sid is invalid")
}
return user, nil
}
func (s *SessionManager) SaveUserIfNotExist(user User) error {
rows, err := s.DB.Query(`SELECT * FROM users WHERE provider_userid = ? AND provider = ? LIMIT 1`, user.Provider_userid, user.Provider)
if err != nil {
fmt.Printf("error getting user from database: %v", err)
return err
}
exists := rows.Next()
if !exists {
_, err = s.DB.Exec(`
INSERT INTO users (provider_userid, provider, username)
VALUES (?, ?, ?);`, user.Provider_userid, user.Provider, user.NickName)
if err != nil {
fmt.Printf("error inserting user: %v", err)
return err
}
}
return nil
}
func (s *SessionManager) DeleteSession(c *gin.Context) error {
if cookie, err := c.Request.Cookie("_session"); err == nil {
value := make(map[string]string)
cookieValue, _ := url.QueryUnescape(cookie.Value)
hash := []byte(config.GetEnvVar("COOKIE_HASH"))
block := []byte(config.GetEnvVar("COOKIE_BLOCK"))
secureCookie := securecookie.New(hash, block)
err = secureCookie.Decode("_session", cookieValue, &value)
if err != nil {
fmt.Printf("error decoding cookie value: %v", err)
return err
}
sessionId := value["sessionId"]
sessionIdInt, err := strconv.Atoi(sessionId)
if err != nil {
fmt.Printf("error converting sessionId to int: %v", err)
return err
}
_, err = s.DB.Exec(`DELETE FROM sessions WHERE ID = ?`, sessionIdInt)
if err != nil {
return err
}
if err != nil {
return err
}
}
c.SetCookie("_session", "", -1, "/", "localhost", false, true)
return nil
return strSid
}

1
db/db.go

@ -34,6 +34,7 @@ func (db *DBClient) Query(query string, args ...interface{}) (*sqlx.Rows, error) @@ -34,6 +34,7 @@ func (db *DBClient) Query(query string, args ...interface{}) (*sqlx.Rows, error)
}
func (db *DBClient) Exec(query string, args ...interface{}) (sql.Result, error) {
fmt.Println("running query")
return db.db.Exec(query, args...)
}

1
main.go

@ -44,6 +44,7 @@ func main() { @@ -44,6 +44,7 @@ func main() {
r.GET("/login", pages.Login)
r.GET("/welcome", pages.Welcome)
r.GET("/goals", pages.Goals)
r.GET("/goals/:goalId", pages.Goal)
// post routes
r.POST("/goals", pages.CreateGoal)
err = r.Run(":8080")

81
models/goal.go

@ -0,0 +1,81 @@ @@ -0,0 +1,81 @@
package models
import (
"fmt"
"log"
"time"
"sponsorahacker/db"
)
type Goal struct {
id int
Name string `form:"item-name" db:"name" binding:"required"`
FundingAmount float64 `form:"funding-amount" db:"funding_amount" binding:"required,numeric"`
Description string `form:"item-description" db:"description" binding:"required"`
LearnMoreURL string `form:"item-url" db:"learn_more_url"` // Optional field
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
CreatedBy int `db:"created_by"`
}
func (g *Goal) CreateGoal() error {
dbClient, err := db.NewDbClient()
if err != nil {
log.Fatalf("Could not connect to database: %v", err)
return err
}
_, err = dbClient.Exec(`INSERT INTO goals (name, description, learn_more_url, funding_amount, created_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?);`, g.Name, g.Description, g.LearnMoreURL, g.FundingAmount, g.CreatedBy, g.CreatedAt, g.UpdatedAt)
if err != nil {
log.Fatalf("Could not create goal: %v", err)
}
return nil
}
func GetGoals(user int) ([]Goal, error) {
var goals []Goal
dbClient, err := db.NewDbClient()
if err != nil {
log.Fatalf("Could not connect to database: %v", err)
}
result, err := dbClient.Query(`SELECT name, description, learn_more_url, funding_amount, created_by, created_at, updated_at FROM goals where created_by = ?`, user)
if err != nil {
log.Fatalf("Could not get goals: %v", err)
}
for result.Next() {
var goal Goal
err = result.StructScan(&goal)
fmt.Println(goal.Name)
if err != nil {
log.Fatalf("Could not read goal: %v", err)
return nil, err
}
goals = append(goals, goal)
}
return goals, nil
}
func GetGoal(id int) (*Goal, error) {
dbClient, err := db.NewDbClient()
result, err := dbClient.Query(`SELECT name, description, learn_more_url, funding_amount, created_by, created_at, updated_at FROM goals where id = ?`, id)
if err != nil {
log.Fatalf("Could not read goal: %v", err)
return nil, err
}
var goal Goal
err = result.StructScan(&goal)
return &goal, nil
}

84
pages/goals.go

@ -3,26 +3,86 @@ package pages @@ -3,26 +3,86 @@ package pages
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/markbates/goth/gothic"
"log"
"net/http"
"net/url"
"sponsorahacker/auth"
"sponsorahacker/models"
"sponsorahacker/utils"
"strconv"
"strings"
"time"
)
func Goals(c *gin.Context) {
isLoggedIn := utils.CheckIfLoggedIn(c)
user, err := gothic.GetFromSession("user", c.Request)
if err != nil {
fmt.Println("error finding session: ", err)
c.Redirect(http.StatusTemporaryRedirect, "/login")
return
}
userModel := auth.HydrateUser(user)
goals, err := models.GetGoals(userModel.GetSid())
if err != nil {
fmt.Println("error getting goals: ", err)
}
fmt.Println("sid: ", userModel.GetSid())
fmt.Println("goals:", goals)
c.HTML(http.StatusOK, "goals.html", gin.H{
"title": "Sponsor A Hacker",
"isLoggedIn": isLoggedIn,
"isLoggedIn": true,
"user": userModel.NickName,
"goals": goals,
})
}
func Goal(c *gin.Context) {
user, err := gothic.GetFromSession("user", c.Request)
if err != nil {
fmt.Println("error finding session: ", err)
c.Redirect(http.StatusTemporaryRedirect, "/login")
return
}
goalId, err := strconv.Atoi(c.Param("goalId"))
if err != nil {
log.Println("error parsing goal id: ", err)
}
userModel := auth.HydrateUser(user)
goal, err := models.GetGoal(goalId)
if err != nil {
fmt.Println("error getting goals: ", err)
}
c.HTML(http.StatusOK, "goal.html", gin.H{
"title": "Sponsor A Hacker",
"isLoggedIn": true,
"user": userModel.NickName,
"goal": goal,
})
}
func CreateGoal(c *gin.Context) {
isLoggedIn := utils.CheckIfLoggedIn(c)
user, err := gothic.GetFromSession("user", c.Request)
if err != nil {
fmt.Println("error finding session: ", err)
c.AbortWithStatus(http.StatusForbidden)
return
}
userModel := auth.HydrateUser(user)
trim := strings.TrimSpace
parseFloat := strconv.ParseFloat
parseUrl := url.Parse
@ -36,13 +96,13 @@ func CreateGoal(c *gin.Context) { @@ -36,13 +96,13 @@ func CreateGoal(c *gin.Context) {
description := trim(c.PostForm("item-description"))
learnMoreURL := trim(c.PostForm("item-url"))
_, err := parseUrl(learnMoreURL)
_, err = parseUrl(learnMoreURL)
if isEmpty(name) && isEmpty(fundingAmountStr) && isEmpty(description) && isEmpty(learnMoreURL) && err != nil {
fieldError := fmt.Errorf("missing a required field. Please check and make sure all fields are filled out")
c.HTML(http.StatusNotAcceptable, "goals.html", gin.H{
"title": "Sponsor A Hacker",
"isLoggedIn": isLoggedIn,
"isLoggedIn": true,
"error": fieldError,
})
}
@ -53,7 +113,7 @@ func CreateGoal(c *gin.Context) { @@ -53,7 +113,7 @@ func CreateGoal(c *gin.Context) {
fieldError := fmt.Errorf("invalid funding amount")
c.HTML(http.StatusInternalServerError, "goals.html", gin.H{
"title": "Sponsor A Hacker",
"isLoggedIn": isLoggedIn,
"isLoggedIn": true,
"error": fieldError,
})
}
@ -61,12 +121,18 @@ func CreateGoal(c *gin.Context) { @@ -61,12 +121,18 @@ func CreateGoal(c *gin.Context) {
createdDate := time.Now()
updatedDate := time.Now()
if err != nil {
c.HTML(http.StatusInternalServerError, "goals.html", gin.H{})
}
fmt.Println("sid is : ", userModel.GetSid())
goal := models.Goal{
Name: name,
FundingAmount: fundingAmount,
Description: description,
LearnMoreURL: learnMoreURL,
CreatedAt: createdDate,
CreatedBy: userModel.GetSid(),
UpdatedAt: updatedDate,
}
@ -76,13 +142,13 @@ func CreateGoal(c *gin.Context) { @@ -76,13 +142,13 @@ func CreateGoal(c *gin.Context) {
fmt.Printf("Error inserting goal: %v", err)
c.HTML(http.StatusInternalServerError, "goals.html", gin.H{
"title": "Sponsor A Hacker",
"isLoggedIn": isLoggedIn,
"isLoggedIn": true,
"error": err,
})
}
c.HTML(http.StatusOK, "goals.html", gin.H{
"title": "Sponsor A Hacker",
"isLoggedIn": isLoggedIn,
"isLoggedIn": true,
})
}

30
pages/home.go

@ -3,29 +3,29 @@ package pages @@ -3,29 +3,29 @@ package pages
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"log"
"github.com/markbates/goth/gothic"
"net/http"
"sponsorahacker/db"
"sponsorahacker/utils"
"sponsorahacker/auth"
)
func Home(c *gin.Context) {
user, err := gothic.GetFromSession("user", c.Request)
envErr := godotenv.Load()
if envErr != nil {
log.Fatal("Error loading .env file")
if err != nil {
fmt.Println("error checking login", err)
}
isLoggedIn := utils.CheckIfLoggedIn(c)
fmt.Println("isLoggedIn:", isLoggedIn)
_, err := db.NewDbClient()
auth.HydrateUser(user)
if err != nil {
log.Fatal(err)
fmt.Println("error checking login", err)
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "Sponsor a Hacker",
"isLoggedIn": false,
})
return
}
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "Sponsor a Hacker",
"isLoggedIn": isLoggedIn,
})
c.Redirect(http.StatusTemporaryRedirect, "/welcome")
}

20
pages/welcome.go

@ -3,15 +3,27 @@ package pages @@ -3,15 +3,27 @@ package pages
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/markbates/goth/gothic"
"net/http"
"sponsorahacker/utils"
"sponsorahacker/auth"
)
func Welcome(c *gin.Context) {
isLoggedIn := utils.CheckIfLoggedIn(c)
fmt.Println("isLoggedIn:", isLoggedIn)
user, err := gothic.GetFromSession("user", c.Request)
fmt.Println(user)
if err != nil {
fmt.Println("error finding session: ", err)
c.Redirect(http.StatusTemporaryRedirect, "/login")
return
}
userModel := auth.HydrateUser(user)
fmt.Println(userModel.NickName)
c.HTML(http.StatusOK, "welcome.html", gin.H{
"title": "Sponsor A Hacker",
"isLoggedIn": isLoggedIn,
"isLoggedIn": true,
"user": userModel.NickName,
})
}

148
templates/goal.html

@ -0,0 +1,148 @@ @@ -0,0 +1,148 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="/assets/css/style.css" />
<title>Goal Page</title>
<style>
/* General Styles */
body {
font-family: Arial, sans-serif;
background-color: #F4F1EB; /* Light cream background */
margin: 0;
padding: 0;
color: #2B2D42; /* Dark gray text */
}
header {
background-color: #457B9D; /* Muted blue header */
color: #FFFFFF;
padding: 20px;
text-align: center;
}
main {
max-width: 800px;
margin: 20px auto;
padding: 20px;
background-color: #FFFFFF; /* White card */
border: 1px solid #A3C1AD; /* Muted green border */
border-radius: 10px;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
}
h1, h2 {
color: #457B9D; /* Muted blue text */
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.5rem;
margin-top: 20px;
}
p {
margin: 10px 0;
line-height: 1.6;
}
.goal-details {
margin-bottom: 20px;
}
.goal-progress {
margin: 20px 0;
}
.progress-bar {
background-color: #A3C1AD; /* Muted green */
height: 20px;
width: 70%; /* Example progress, replace dynamically */
max-width: 100%;
border-radius: 5px;
}
.progress-container {
background-color: #F4F1EB; /* Light cream */
border: 1px solid #A3C1AD; /* Muted green */
border-radius: 5px;
height: 20px;
overflow: hidden;
margin-bottom: 10px;
}
.actions {
display: flex;
justify-content: space-between;
gap: 10px;
margin-top: 20px;
}
.actions button {
background-color: #A3C1AD; /* Muted green button */
color: #FFFFFF;
border: none;
padding: 10px 20px;
border-radius: 5px;
font-size: 1rem;
cursor: pointer;
}
.actions button:hover {
background-color: #87B09C; /* Slightly darker muted green */
}
.link {
text-align: center;
margin-top: 20px;
}
.link a {
color: #457B9D; /* Muted blue */
text-decoration: none;
font-weight: bold;
}
.link a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
{{template "pageheader" . }}
<main>
<!-- Goal Details Section -->
<section class="goal-details">
<h2>Goal: Learn Full-Stack Development</h2>
<p>Description: A comprehensive course to master full-stack web development. This goal includes learning front-end and back-end technologies to build robust applications.</p>
<p>Funding Needed: <strong>$500</strong></p>
<p>Funding Received: <strong>$350</strong></p>
</section>
<!-- Progress Bar Section -->
<section class="goal-progress">
<h2>Funding Progress</h2>
<div class="progress-container">
<div class="progress-bar"></div>
</div>
<p>70% funded</p>
</section>
<!-- Actions Section -->
<section class="actions">
<button>Donate</button>
<button>Share</button>
</section>
<!-- External Link Section -->
<div class="link">
<a href="#">Learn more about this goal</a>
</div>
</main>
</body>
</html>

23
templates/goals.html

@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
<div class="goals-container">
<header class="goals-page-header">
<h1 class="goals-page-header-title">Manage Your Goals</h1>
<h1 class="goals-page-header-title">Ready to set some goals, {{ .user }}?</h1>
<p class="goals-page-header-content">Add new goals or track progress on your current items.</p>
</header>
@ -43,20 +43,15 @@ @@ -43,20 +43,15 @@
<section>
<h2 class="add-goals-title">Your Current Goals</h2>
<div class="goal-items">
<div class="goal-item">
<div class="item-info">
<h3 class="item-info-title">Full-Stack Development Certification</h3>
<p class="item-info-content">Progress: <span class="goal-progress">75%</span> funded</p>
{{ range .goals }}
<div class="goal-item">
<div class="item-info">
<h3 class="item-info-title">{{ .Name }}</h3>
<p>{{ .Description }}</p>
<p class="item-info-content">Progress: <span class="goal-progress">75%</span> funded</p>
</div>
</div>
<button class="btn danger-btn">Remove</button>
</div>
<div class="goal-item">
<div class="item-info">
<h3 class="item-info-title">Data Science Course</h3>
<p class="item-info-content">Progress: <span class="goal-progress">40%</span> funded</p>
</div>
<button class="btn danger-btn">Remove</button>
</div>
{{ end }}
</div>
</section>
</div>

13
templates/header.html

@ -1,14 +1,17 @@ @@ -1,14 +1,17 @@
{{define "pageheader"}}
<div class="header">
<img src="assets/images/logo.jpg" class="logo"/>
<h1 class="title">
Sponsor A Hacker
</h1>
<img src="/assets/images/logo.jpg" class="logo"/>
{{if eq .isLoggedIn false}}
<h1 class="title">
<a href="/" class="title-link">Sponsor A Hacker</a>
</h1>
<a href="/login" class="login-banner-link">Login</a>
{{ else }}
<a href="auth/logout/twitch" class="login-banner-link">Logout</a>
<h1 class="title">
<a href="/welcome" class="title-link">Sponsor A Hacker</a>
</h1>
<a href="/auth/logout/twitch" class="login-banner-link">Logout</a>
{{ end }}
</div>

116
templates/upload-goal.html

@ -0,0 +1,116 @@ @@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Submit Hackathon Project</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #F4F1EB; /* Light cream background */
color: #2B2D42; /* Dark gray text */
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.form-container {
background-color: #FFFFFF; /* White form background */
border: 2px solid #457B9D; /* Muted blue border */
border-radius: 10px;
padding: 20px;
width: 400px;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
}
.form-container h2 {
text-align: center;
color: #457B9D; /* Muted blue heading */
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
font-weight: bold;
margin-bottom: 5px;
}
.form-group input[type="text"],
.form-group textarea,
.form-group input[type="file"] {
width: 100%;
padding: 10px;
font-size: 1rem;
border: 1px solid #A3C1AD; /* Muted green border */
border-radius: 5px;
box-sizing: border-box;
}
.form-group textarea {
height: 100px;
resize: vertical;
}
.form-group input[type="file"] {
padding: 5px;
}
.form-group input[type="file"]::file-selector-button {
background-color: #457B9D; /* Muted blue button */
color: #FFFFFF; /* White text */
border: none;
border-radius: 5px;
padding: 5px 10px;
cursor: pointer;
font-size: 0.9rem;
}
.form-group input[type="file"]::file-selector-button:hover {
background-color: #356081; /* Slightly darker blue */
}
.submit-btn {
display: block;
width: 100%;
background-color: #A3C1AD; /* Muted green button */
color: #FFFFFF;
border: none;
border-radius: 5px;
padding: 10px;
font-size: 1rem;
cursor: pointer;
text-align: center;
}
.submit-btn:hover {
background-color: #87B09C; /* Slightly darker green */
}
</style>
</head>
<body>
<div class="form-container">
<h2>Submit Your Hack</h2>
<form action="/submit-hack" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="hack-name">Hack Name</label>
<input type="text" id="hack-name" name="hack-name" placeholder="Enter your hack name" required>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" name="description" placeholder="Describe your hack" required></textarea>
</div>
<div class="form-group">
<label for="docker-file">Upload Docker Image</label>
<input type="file" id="docker-file" name="docker-file" accept=".tar,.tar.gz" required>
</div>
<button type="submit" class="submit-btn">Submit Hack</button>
</form>
</div>
</body>
</html>

2
templates/welcome.html

@ -12,7 +12,7 @@ @@ -12,7 +12,7 @@
<div class="dashboard-container">
<div class="dashboard-container">
<div class="welcome">
<h1 class="welcome-title">Welcome, Codegirl007!</h1>
<h1 class="welcome-title">Welcome, {{ .user }}!</h1>
<p class="tagline">Find your path, fund futures, or inspire a community today.</p>
</div>

Loading…
Cancel
Save