diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..7f8a808 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,26 @@ +jobs: + deploy: + name: Deploy Website + runs-on: ubuntu-latest + steps: + - name: Remote deploy + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SSH_HOST }} + key: ${{ secrets.SSH_KEY }} + port: ${{ secrets.SSH_PORT }} + username: ${{ secrets.SSH_USER }} + script: | + source ~/.bash_profile + cd ~/projects/codejam2024/ + git reset --hard HEAD || true + git pull origin main + ./build.sh + sudo systemctl restart codejam-staging.service + +name: Deploy + +on: + push: + branches: + - main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9cba972 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,23 @@ +jobs: + deploy: + name: Deploy Prod Release + runs-on: ubuntu-latest + steps: + - name: Remote deploy + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SSH_HOST }} + key: ${{ secrets.SSH_KEY }} + port: ${{ secrets.SSH_PORT }} + username: ${{ secrets.SSH_USER }} + script: | + source ~/.bash_profile + sudo systemctl stop codejam-prod.service + sudo cp /opt/codejam/staging/codejam.io /opt/codejam/prod/ + sudo systemctl start codejam-prod.service + +name: Release + +on: + release: + types: [published] diff --git a/.gitignore b/.gitignore index d4ae893..2e564db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,18 @@ .idea/ *.iml +node_modules + # VSCODE .vscode/ # Config files... config.toml + +# extra files to ignore +codejam.io.exe +brainstorm.txt +gradientchange.svelte + +# docker related files +docker-compose.override.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8c91fde --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM library/node:20-alpine3.20 as frontend_builder + +WORKDIR /app + +COPY ./html/package.json /app +COPY ./html/package-lock.json /app + +RUN npm install + +COPY ./html /app + +RUN npm run build + +RUN mkdir /app/dist/static/ +RUN cp --recursive /app/static/. /app/dist/static/ + +FROM library/golang:1.22.3-alpine3.20 + +WORKDIR /app + +COPY ./backend/app/go.mod /app +COPY ./backend/app/go.sum /app + +RUN go mod download + +COPY ./backend/app/ /app +COPY --from=frontend_builder /app/dist/ /app/server/static_files/ + +ENTRYPOINT [ "go", "run", "./main.go" ] diff --git a/README.md b/README.md index 9f9713f..2f6917a 100644 --- a/README.md +++ b/README.md @@ -1 +1,42 @@ -# CodeJam Site \ No newline at end of file +# CodeJam Site + +## docker compose +### Deploying + +1. Pull all images + +```bash +docker compose pull database redis +``` + +2. Build the docker image: `docker compose build server` + +```bash +docker compose build server +``` + +3. Copy the example configuration file from `./backend/app/config.example.toml` to `./backend/app/config.toml` +4. Edit the configuration file + 1. The database user will be "postgres" + 2. The database host will be "database" (as the name of the service in the `docker-compose.yaml` file) + 3. The redis host will be "redis" (as the name of the service in the `docker-compose.yaml` file) +5. Start the PostgreSQL and Redis containers + +```bash +docker compose up -d database redis +``` + +6. Start the docker container through docker compose + +```bash +docker compose up -d server +``` + +7. Visit website at [http://localhost:8080](http://localhost:8080) + +### Providing more overrides + +1. Unignore the `docker-compose.override.yaml` in the `.gitignore` file +2. Make changes +3. Stage the updated `docker-compose.override.yaml` file +4. Ignore the `docker-compose.override.yaml` file in the `.gitignore` file diff --git a/backend/app/database/events.go b/backend/app/database/events.go index 398d7f4..ad3f84e 100644 --- a/backend/app/database/events.go +++ b/backend/app/database/events.go @@ -1,6 +1,8 @@ package database -import "github.com/jackc/pgx/v5/pgtype" +import ( + "github.com/jackc/pgx/v5/pgtype" +) type DBEvent struct { Id pgtype.UUID `db:"id"` @@ -8,26 +10,49 @@ type DBEvent struct { Title string `db:"title"` Description string `db:"description"` Rules string `db:"rules"` - OrganizerUserId pgtype.UUID `db:"organizer_user_id"` + Timeline string `db:"timeline"` + OrganizerUserId pgtype.UUID `db:"organizer_user_id" json:"-"` MaxTeams int `db:"max_teams"` StartsAt pgtype.Timestamp `db:"starts_at"` EndsAt pgtype.Timestamp `db:"ends_at"` - CreatedOn pgtype.Timestamp `db:"created_on"` + CreatedOn pgtype.Timestamp `db:"created_on" json:"-"` +} + +type DBEventStatus struct { + Id int `db:"id"` + Code string `db:"code"` + Title string `db:"title"` + Description string `db:"description"` + Sort int `db:"sort"` +} + +type DBEventStatusCode struct { + Code string `db:"code"` +} + +func GetEventStatusCode(eventId pgtype.UUID) (string, error) { + result, err := GetRow[DBEventStatusCode]( + `SELECT code + FROM events + INNER JOIN statuses ON (events.status_id = statuses.id) + WHERE events.id = $1`, + eventId) + return result.Code, err } func CreateEvent(organizerUserId pgtype.UUID) (DBEvent, error) { var event DBEvent event, err := GetRow[DBEvent]( `INSERT INTO events - (status_id, title, description, rules, organizer_user_id) - VALUES - ((SELECT id FROM statuses WHERE code = 'PLANNING'), + (status_id, title, description, rules, organizer_user_id) + VALUES + ((SELECT id FROM statuses WHERE code = 'PLANNING'), '', '', '', $1) RETURNING * - `, + `, organizerUserId) return event, err } @@ -44,18 +69,35 @@ func GetEvents() ([]DBEvent, error) { return result, err } +// GetActiveEvent will return what is assumed to be a single active event. +func GetActiveEvent() (DBEvent, error) { + result, err := GetRow[DBEvent]( + `SELECT * FROM events + WHERE status_id IN ( + SELECT id FROM statuses WHERE code NOT IN ('PLANNING', 'ENDED') + ) + LIMIT 1`) + return result, err +} + func UpdateEvent(event DBEvent) (DBEvent, error) { event, err := GetRow[DBEvent]( `UPDATE events SET status_id=$2, title=$3, - description=$4, - rules=$5, - max_teams=$6, - starts_at=$7, - ends_at=$8 + timeline=$4, + description=$5, + rules=$6, + max_teams=$7, + starts_at=$8, + ends_at=$9 WHERE id=$1 RETURNING *`, - event.Id, event.StatusId, event.Title, event.Description, event.Rules, event.MaxTeams, event.StartsAt, event.EndsAt) + event.Id, event.StatusId, event.Title, event.Timeline, event.Description, event.Rules, event.MaxTeams, event.StartsAt, event.EndsAt) return event, err } + +func GetStatuses() ([]DBEventStatus, error) { + statuses, err := GetRows[DBEventStatus](`SELECT * FROM statuses ORDER BY sort`) + return statuses, err +} diff --git a/backend/app/database/migrations.go b/backend/app/database/migrations.go new file mode 100644 index 0000000..6ac4658 --- /dev/null +++ b/backend/app/database/migrations.go @@ -0,0 +1,39 @@ +package database + +import ( + "embed" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/pgx/v5" + "github.com/golang-migrate/migrate/v4/source/iofs" + "github.com/jackc/pgx/v5/stdlib" + "os" +) + +//go:embed migrations/* +var migrationsFS embed.FS + +func RunMigrations() { + logger.Info("Running DB Migrations...") + stdConn := stdlib.OpenDBFromPool(Pool) + + migrationConfig := pgx.Config{} + sqlDriver, err := pgx.WithInstance(stdConn, &migrationConfig) + if err != nil { + logger.Critical("Error setting up migrations DB connection: %v", err) + os.Exit(1) + } + + sourceDriver, err := iofs.New(migrationsFS, "migrations") + if err != nil { + logger.Critical("Error setting up migrations FS: %v", err) + os.Exit(1) + } + + m, err := migrate.NewWithInstance("embeddedFS", sourceDriver, "postgres", sqlDriver) + err = m.Up() + if err != nil && err != migrate.ErrNoChange { + logger.Critical("Migrations failure: %v", err) + os.Exit(1) + } + logger.Info("Migrations completed") +} diff --git a/backend/app/database/migrations/00000000_Initial.up.sql b/backend/app/database/migrations/00000000_Initial.up.sql new file mode 100644 index 0000000..6f84fec --- /dev/null +++ b/backend/app/database/migrations/00000000_Initial.up.sql @@ -0,0 +1,49 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE TABLE IF NOT EXISTS users ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + service_name TEXT NOT NULL, + service_user_id TEXT NOT NULL, + display_name TEXT NOT NULL DEFAULT '', + created_on TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE('utc')) +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_service_user ON users (service_name, service_user_id); + +CREATE TABLE IF NOT EXISTS events ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + status_id integer, + title TEXT NOT NULL, + description TEXT NOT NULL, + rules TEXT NOT NULL, + organizer_user_id UUID NOT NULL, + max_teams integer default -1, + starts_at TIMESTAMP WITH TIME ZONE, + ends_at TIMESTAMP WITH TIME ZONE, + created_on TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE('utc')) +); + +CREATE TABLE IF NOT EXISTS statuses ( + id SERIAL PRIMARY KEY, + code TEXT NOT NULL UNIQUE, + title TEXT NOT NULL, + description TEXT NOT NULL +); + +INSERT INTO statuses (id, code, title, description) VALUES (1, 'PLANNING', 'Planning', 'Event is not yet live. The organizer may edit and preview the site but normal users will just see a placeholder page') +ON CONFLICT (id) DO UPDATE set code = EXCLUDED.code, title = EXCLUDED.title, description = EXCLUDED.description; + +INSERT INTO statuses (id, code, title, description) VALUES (2, 'PUBLISHED', 'Published', 'Event is publicly visible.') +ON CONFLICT (id) DO UPDATE set code = EXCLUDED.code, title = EXCLUDED.title, description = EXCLUDED.description; + +INSERT INTO statuses (id, code, title, description) VALUES (3, 'STARTED', 'Started', 'The theme will be shown publicly and users can start coding. Submissions will be accepted during this time.') +ON CONFLICT (id) DO UPDATE set code = EXCLUDED.code, title = EXCLUDED.title, description = EXCLUDED.description; + +INSERT INTO statuses (id, code, title, description) VALUES (4, 'VOTING', 'Voting Open', 'Submissions are closed and voting is underway') +ON CONFLICT (id) DO UPDATE set code = EXCLUDED.code, title = EXCLUDED.title, description = EXCLUDED.description; + +INSERT INTO statuses (id, code, title, description) VALUES (5, 'ENDED', 'Ended', 'Competition is over. Results may still be viewed, but no more voting is allowed') +ON CONFLICT (id) DO UPDATE set code = EXCLUDED.code, title = EXCLUDED.title, description = EXCLUDED.description; + + + diff --git a/backend/app/database/migrations/00000001_event_adds.down.sql b/backend/app/database/migrations/00000001_event_adds.down.sql new file mode 100644 index 0000000..9974750 --- /dev/null +++ b/backend/app/database/migrations/00000001_event_adds.down.sql @@ -0,0 +1 @@ +ALTER TABLE events DROP COLUMN timeline; \ No newline at end of file diff --git a/backend/app/database/migrations/00000001_event_adds.up.sql b/backend/app/database/migrations/00000001_event_adds.up.sql new file mode 100644 index 0000000..b327424 --- /dev/null +++ b/backend/app/database/migrations/00000001_event_adds.up.sql @@ -0,0 +1 @@ +ALTER TABLE events ADD COLUMN timeline TEXT DEFAULT ''; diff --git a/backend/app/database/migrations/00000002_user_role.down.sql b/backend/app/database/migrations/00000002_user_role.down.sql new file mode 100644 index 0000000..013fe61 --- /dev/null +++ b/backend/app/database/migrations/00000002_user_role.down.sql @@ -0,0 +1 @@ +ALTER TABLE users DROP COLUMN IF EXISTS role; \ No newline at end of file diff --git a/backend/app/database/migrations/00000002_user_role.up.sql b/backend/app/database/migrations/00000002_user_role.up.sql new file mode 100644 index 0000000..0d9a26e --- /dev/null +++ b/backend/app/database/migrations/00000002_user_role.up.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN role TEXT DEFAULT ''; \ No newline at end of file diff --git a/backend/app/database/migrations/00000003_teams.up.sql b/backend/app/database/migrations/00000003_teams.up.sql new file mode 100644 index 0000000..7c239b5 --- /dev/null +++ b/backend/app/database/migrations/00000003_teams.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS teams ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + event_id UUID references events(id), + owner_user_id UUID references users(id) ON DELETE NO ACTION ON UPDATE NO ACTION, + name TEXT NOT NULL, + visibility TEXT, + timezone TEXT, + technologies TEXT, + availability TEXT NOT NULL, + description TEXT NOT NULL, + created_on TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE('utc')) +); \ No newline at end of file diff --git a/backend/app/database/migrations/00000004_team_members.up.sql b/backend/app/database/migrations/00000004_team_members.up.sql new file mode 100644 index 0000000..5429dbd --- /dev/null +++ b/backend/app/database/migrations/00000004_team_members.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS team_members ( + team_id UUID NOT NULL references teams(id) ON DELETE CASCADE, + user_id UUID NOT NULL references users(id) ON DELETE CASCADE, + team_role TEXT NOT NULL, + created_on TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE('utc')), + PRIMARY KEY (team_id, user_id) +); + +ALTER TABLE teams DROP COLUMN IF EXISTS owner_user_id; + diff --git a/backend/app/database/migrations/00000005_team_role.down.sql b/backend/app/database/migrations/00000005_team_role.down.sql new file mode 100644 index 0000000..6465999 --- /dev/null +++ b/backend/app/database/migrations/00000005_team_role.down.sql @@ -0,0 +1 @@ +ALTER TABLE team_members RENAME COLUMN role TO team_role; \ No newline at end of file diff --git a/backend/app/database/migrations/00000006_team_code.up.sql b/backend/app/database/migrations/00000006_team_code.up.sql new file mode 100644 index 0000000..438181e --- /dev/null +++ b/backend/app/database/migrations/00000006_team_code.up.sql @@ -0,0 +1 @@ +ALTER TABLE teams ADD COLUMN code TEXT; \ No newline at end of file diff --git a/backend/app/database/migrations/00000007_team_code.down.sql b/backend/app/database/migrations/00000007_team_code.down.sql new file mode 100644 index 0000000..1729988 --- /dev/null +++ b/backend/app/database/migrations/00000007_team_code.down.sql @@ -0,0 +1 @@ +ALTER TABLE teams RENAME code TO invite_code; \ No newline at end of file diff --git a/backend/app/database/migrations/00000008_team_code.down.sql b/backend/app/database/migrations/00000008_team_code.down.sql new file mode 100644 index 0000000..e688cfd --- /dev/null +++ b/backend/app/database/migrations/00000008_team_code.down.sql @@ -0,0 +1 @@ +ALTER TABLE teams DROP COLUMN code; \ No newline at end of file diff --git a/backend/app/database/migrations/00000009_avatars.up.sql b/backend/app/database/migrations/00000009_avatars.up.sql new file mode 100644 index 0000000..dda99f4 --- /dev/null +++ b/backend/app/database/migrations/00000009_avatars.up.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url TEXT; \ No newline at end of file diff --git a/backend/app/database/migrations/00000010_signup_status.down.sql b/backend/app/database/migrations/00000010_signup_status.down.sql new file mode 100644 index 0000000..0b44460 --- /dev/null +++ b/backend/app/database/migrations/00000010_signup_status.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE statuses DROP COLUMN sort; + +DELETE FROM statuses WHERE code in ('SIGNUP', 'COMPLETED'); \ No newline at end of file diff --git a/backend/app/database/migrations/00000010_signup_status.up.sql b/backend/app/database/migrations/00000010_signup_status.up.sql new file mode 100644 index 0000000..9dfae8b --- /dev/null +++ b/backend/app/database/migrations/00000010_signup_status.up.sql @@ -0,0 +1,12 @@ +ALTER TABLE statuses ADD COLUMN IF NOT EXISTS sort int; + +INSERT INTO statuses (id, code, title, description) VALUES (6, 'SIGNUP', 'Signups Accepted', 'Users may begin signing up and creating teams.'); +INSERT INTO statuses (id, code, title, description) VALUES (7, 'COMPLETED', 'Complete', 'Competition is over. Results may still be viewed, but not more voting is allowed'); + +UPDATE statuses SET sort = 1 WHERE code='PLANNING'; +UPDATE statuses SET sort = 2 WHERE code='PUBLISHED'; +UPDATE statuses SET sort = 3 WHERE code='SIGNUP'; +UPDATE statuses SET sort = 4 WHERE code='STARTED'; +UPDATE statuses SET sort = 5 WHERE code='ENDED'; +UPDATE statuses SET sort = 6 WHERE code='VOTING'; +UPDATE statuses SET sort = 7 WHERE code='COMPLETED'; diff --git a/backend/app/database/migrations/00000011_admin_users.up.sql b/backend/app/database/migrations/00000011_admin_users.up.sql new file mode 100644 index 0000000..9b334a7 --- /dev/null +++ b/backend/app/database/migrations/00000011_admin_users.up.sql @@ -0,0 +1,12 @@ +-- capture the original user so admins can see it without having to lookup a discord id +ALTER TABLE users ADD COLUMN IF NOT EXISTS service_user_name TEXT; + +-- allow for a user to be disabled or banned, or have some other sort of status that's TBD +ALTER TABLE users ADD COLUMN IF NOT EXISTS account_status TEXT DEFAULT 'ACTIVE'; + +-- allow for a user to be prevented from modifying their display name +ALTER TABLE users ADD COLUMN IF NOT EXISTS lock_display_name BOOLEAN DEFAULT FALSE; + +-- assumes no real users are in the system yet +UPDATE users SET service_user_name = display_name; + diff --git a/backend/app/database/postgres.go b/backend/app/database/postgres.go index 6b25020..330032a 100644 --- a/backend/app/database/postgres.go +++ b/backend/app/database/postgres.go @@ -81,3 +81,21 @@ func GetRows[T any](query string, args ...any) ([]T, error) { return result, nil } + +func Execute(query string, args ...any) error { + conn, err := Pool.Acquire(context.Background()) + if err != nil { + logger.Error("acquire conn error %v", err) + return err + } + defer conn.Release() + + _, err = conn.Query(context.Background(), + query, + args...) + if err != nil { + logger.Error("Query Execute Error: %v", err) + } + + return nil +} diff --git a/backend/app/database/structs.go b/backend/app/database/structs.go new file mode 100644 index 0000000..adc142b --- /dev/null +++ b/backend/app/database/structs.go @@ -0,0 +1,139 @@ +package database + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type DBTeam struct { + Id pgtype.UUID `db:"id"` + EventId pgtype.UUID `db:"event_id"` + Name string `db:"name"` + Visibility string `db:"visibility"` + Timezone string `db:"timezone"` + Technologies string `db:"technologies"` + Availability string `db:"availability"` + Description string `db:"description"` + CreatedOn pgtype.Timestamptz `db:"created_on"` + InviteCode string `db:"invite_code"` +} + +type CreateTeamMember struct { + UserId pgtype.UUID `db:"user_id"` + TeamId pgtype.UUID `db:"team_id"` + TeamRole string `db:"team_role"` +} + +// has all the user info & role to pass to be read client-side +type DBTeamMemberInfo struct { + DBUser // embed the DBUser fields into the struct + TeamRole string `db:"team_role"` +} + +type DBTeamMember struct { + TeamId pgtype.UUID `db:"team_id"` + UserId pgtype.UUID `db:"user_id"` + TeamRole string `db:"team_role"` + CreatedOn pgtype.Timestamp `db:"user_created_on"` +} + +type DBUserTeams struct { + DBTeam + DisplayName string `db:"display_name"` + TeamRole string `db:"team_role"` + AvatarId string `db:"avatar_id"` +} + +type DBTeamAndTeamMember struct { + Id pgtype.UUID `db:"id"` + EventId pgtype.UUID `db:"event_id"` + Name string `db:"name"` + Visibility string `db:"visibility"` + Timezone string `db:"timezone"` + Technologies string `db:"technologies"` + Availability string `db:"availability"` + Description string `db:"description"` + CreatedOn pgtype.Timestamptz `db:"team_created_on"` + InviteCode string `db:"invite_code"` + MembershipCreatedOn pgtype.Timestamptz `db:"membership_created_on"` + TeamId pgtype.UUID `db:"team_id"` + UserId pgtype.UUID `db:"user_id"` + TeamRole string `db:"team_role"` + DisplayName string `db:"display_name"` + AvatarId string `db:"avatar_id"` + ServiceUserId string `db:"service_user_id"` +} + +type UITeam struct { + Id pgtype.UUID `db:"id"` + EventId pgtype.UUID `db:"event_id"` + Name string `db:"name"` + Visibility string `db:"visibility"` + Timezone string `db:"timezone"` + Technologies string `db:"technologies"` + Availability string `db:"availability"` + Description string `db:"description"` + CreatedOn pgtype.Timestamptz `db:"team_created_on"` + InviteCode string `db:"invite_code"` +} + +type UITeamMember struct { + TeamId pgtype.UUID `db:"team_id"` + UserId pgtype.UUID `db:"user_id"` + MembershipCreatedOn pgtype.Timestamptz `db:"membership_created_on"` + TeamRole string `db:"team_role"` +} + +type TeamMember struct { + UITeamMember + DisplayName string `db:"display_name"` + AvatarId string `db:"avatar_id"` + ServiceUserId string `db:"service_user_id"` +} + +type TeamAndMembers struct { + UITeam + TeamMembers []TeamMember +} + +type UserTeamMember struct { + UserId pgtype.UUID `json:"UserId"` + DisplayName string `json:"DisplayName"` + TeamRole string `json:"TeamRole"` + AvatarId string `json:"AvatarId"` + ServiceUserId string `json:"ServiceUserId"` + UserCreatedOn pgtype.Timestamptz `json:"UserCreatedOn"` +} + +type UserTeam struct { + Id pgtype.UUID `json:"Id"` + EventId pgtype.UUID `json:"EventId"` + Name string `json:"Name"` + Description string `json:"Description"` + Visibility string `json:"Visibility"` + Technologies string `json:"Technologies"` + Availability string `json:"Availability"` + TeamCreatedOn pgtype.Timestamptz `json:"TeamCreatedOn"` + InviteCode string `json:"InviteCode"` + TeamMembers []UserTeamMember `json:"TeamMembers"` +} + +type UserTeamAndMembers struct { + Id pgtype.UUID `db:"id"` + EventId pgtype.UUID `db:"event_id"` + Name string `db:"name"` + Visibility string `db:"visibility"` + Timezone string `db:"timezone"` + Technologies string `db:"technologies"` + Availability string `db:"availability"` + Description string `db:"description"` + TeamCreatedOn pgtype.Timestamptz `db:"team_created_on"` + InviteCode string `db:"invite_code"` + TeamId pgtype.UUID `db:"team_id"` + UserId pgtype.UUID `db:"user_id"` + TeamRole string `db:"team_role"` + MembershipCreatedOn pgtype.Timestamptz `db:"membership_created_on"` + DisplayName string `db:"display_name"` + AvatarId string `db:"avatar_id"` + ServiceUserId string `db:"service_user_id"` + CurrentUserRole string `db:"current_user_role"` +} diff --git a/backend/app/database/teams.go b/backend/app/database/teams.go new file mode 100644 index 0000000..c42a806 --- /dev/null +++ b/backend/app/database/teams.go @@ -0,0 +1,319 @@ +package database + +import ( + "fmt" + "github.com/jackc/pgx/v5/pgtype" +) + +func CreateTeam(team DBTeam) (pgtype.UUID, error) { + team, err := GetRow[DBTeam]( + `INSERT INTO teams + (event_id, name, visibility, timezone, technologies, availability, description, invite_code) + VALUES + ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING id, event_id, name, visibility, timezone, technologies, availability, description, created_on, invite_code + `, + team.EventId, team.Name, team.Visibility, team.Timezone, team.Technologies, team.Availability, team.Description, team.InviteCode) + if err != nil { + fmt.Println("ERROR: failed to create team: ", err) + } + return team.Id, err +} + +// stepp 5: used to construct the GetTeamResponse struct in server/teams.go +func GetTeam(teamId pgtype.UUID) (DBTeam, error) { + team, err := GetRow[DBTeam]( + `SELECT + teams.id, + teams.event_id, + teams.name, + teams.visibility, + teams.timezone, + teams.technologies, + teams.availability, + teams.description, + teams.created_on, + teams.invite_code + FROM teams + WHERE teams.id = $1`, + teamId) + // `SELECT * FROM teams WHERE id = $1`, + // teamId) + + if err != nil { + logger.Error("===DB/GetTeam error: ", err) + return DBTeam{}, err + } + return team, nil +} + +func GetTeamByInvite(inviteCode string) (DBTeam, error) { + // "/invite/:invitecode" + team, err := GetRow[DBTeam]( + `SELECT + teams.id, + teams.event_id, + teams.name, + teams.visibility, + teams.timezone, + teams.technologies, + teams.availability, + teams.description, + teams.created_on, + teams.invite_code + FROM teams + WHERE teams.invite_code = $1`, + inviteCode) + if err != nil { + logger.Error("===DB/GetTeamByInvite error: ", err) + return DBTeam{}, err + } + return team, nil +} + +func MapToTeamAndMember(data []DBTeamAndTeamMember) []TeamAndMembers { + // instantiates array to store output, mapped by team id (uuid) for key + teamMap := make(map[pgtype.UUID]*TeamAndMembers) + for _, item := range data { + // Check if team already exists in the map. This map loopkup returns: + // 1) value associated with the key if it exsits + // 2) boolean indicating whether key was found in the map + team, ok := teamMap[item.TeamId] + if !ok { + // Create a new team + team = &TeamAndMembers{ + UITeam: UITeam{ + Id: item.Id, + EventId: item.EventId, + Name: item.Name, + Visibility: item.Visibility, + Timezone: item.Timezone, + Technologies: item.Technologies, + Availability: item.Availability, + Description: item.Description, + CreatedOn: item.CreatedOn, + InviteCode: item.InviteCode, + }, + TeamMembers: []TeamMember{}, + } + teamMap[item.TeamId] = team + } + // Add team member to TeamMembers slice + member := TeamMember{ + UITeamMember: UITeamMember{ + TeamId: item.TeamId, + UserId: item.UserId, + MembershipCreatedOn: item.MembershipCreatedOn, + TeamRole: item.TeamRole, + }, + DisplayName: item.DisplayName, + AvatarId: item.AvatarId, + ServiceUserId: item.ServiceUserId, + } + team.TeamMembers = append(team.TeamMembers, member) + } + // Convert map back to slice + var result []TeamAndMembers + for _, team := range teamMap { + result = append(result, *team) + } + fmt.Println(result) + return result +} + +func GetTeams() (*[]TeamAndMembers, error) { + teamAndMember, err := GetRows[DBTeamAndTeamMember]( // returns { team 1: { userA: {display_name: "momo"}}, team 1...} + `SELECT + t.id, + t.event_id, + t.name, + t.visibility, + t.timezone, + t.technologies, + t.availability, + t.description, + t.created_on AS team_created_on, + t.invite_code, + u.display_name, + u.avatar_id, + u.service_user_id, + tm.team_id, + tm.user_id, + tm.created_on AS membership_created_on, + tm.team_role + FROM teams t + INNER JOIN team_members tm ON (tm.team_id = t.id) + INNER JOIN users u ON (u.id = tm.user_id) + ORDER BY t.id + `, + ) + if err != nil { + return nil, err + } + for _, t := range teamAndMember { + fmt.Printf("%v\n", t) + } + UITeamAndMember := MapToTeamAndMember(teamAndMember) + + return &UITeamAndMember, err +} + +func MapToUserTeamAndMember(userTeams *[]UserTeamAndMembers) []UserTeam { + // initialize a json dictionary + teamMap := make(map[pgtype.UUID]*UserTeam) + + // loop through all the rows from the query. + // UserTeams aka []UserTeam is a single row from query. + for _, ut := range *userTeams { + // If team doesn't exist in map, create it + if _, exists := teamMap[ut.Id]; !exists { + teamMap[ut.Id] = &UserTeam{ + Id: ut.Id, + EventId: ut.EventId, + Name: ut.Name, + Description: ut.Description, + Visibility: ut.Visibility, + Technologies: ut.Technologies, + Availability: ut.Availability, + TeamCreatedOn: ut.TeamCreatedOn, + InviteCode: ut.InviteCode, + TeamMembers: []UserTeamMember{}, + } + } + + // create a member + TeamMember := UserTeamMember{ + UserId: ut.UserId, + DisplayName: ut.DisplayName, + TeamRole: ut.TeamRole, + AvatarId: ut.AvatarId, + ServiceUserId: ut.ServiceUserId, + UserCreatedOn: ut.MembershipCreatedOn, + } + + // add member to teamMap dictionary + teamMap[ut.Id].TeamMembers = append(teamMap[ut.Id].TeamMembers, TeamMember) + } + + // Convert map to slice + teams := make([]UserTeam, 0, len(teamMap)) + for _, team := range teamMap { + teams = append(teams, *team) + } + return teams +} + +func GetUserTeams(userId pgtype.UUID) (*[]UserTeam, error) { + result, err := GetRows[UserTeamAndMembers]( + `SELECT + t.id, + t.event_id, + t.name, + t.visibility, + t.timezone, + t.technologies, + t.availability, + t.description, + t.created_on AS team_created_on, + t.invite_code, + tm.team_id, + tm.user_id, + tm.team_role, + tm.created_on AS membership_created_on, + tmu.display_name, + tmu.avatar_id, + tmu.service_user_id, + mtm.team_role AS current_user_role + FROM team_members mtm + INNER JOIN teams t ON t.id = mtm.team_id + INNER JOIN team_members tm ON t.id = tm.team_id + INNER JOIN users tmu ON tmu.id = tm.user_id + WHERE mtm.user_id = $1 + ORDER BY team_created_on`, + userId) + if err != nil { + logger.Error("failed to retrieve team info for user: %v", userId, err) + } + + UIUserTeamAndMember := MapToUserTeamAndMember(&result) + return &UIUserTeamAndMember, err +} + +func UpdateTeam(team DBTeam) (DBTeam, error) { + updatedTeam, err := GetRow[DBTeam]( + `UPDATE teams + SET name=$2, + visibility=$3, + timezone=$4, + technologies=$5, + availability=$6, + description=$7 + WHERE id=$1 + RETURNING *`, + team.Id, team.Name, team.Visibility, team.Timezone, team.Technologies, team.Availability, team.Description) + if err != nil { + return DBTeam{}, fmt.Errorf("failed to update team with ID %v: %w", team.Id, err) + } + return updatedTeam, err +} + +type DBTeamInviteCode struct { + InviteCode string `db:"invite_code"` +} + +func GetTeamInviteCode(teamId pgtype.UUID) (inviteCode DBTeamInviteCode, err error) { + fmt.Println(teamId) + teamInviteCode, err := GetRow[DBTeamInviteCode]( + `SELECT teams.invite_code + FROM teams + WHERE teams.id = $1`, teamId) + if err != nil { + logger.Error("failed to retrieve invite code for teamId %v: %w", teamId, err) + } + return teamInviteCode, err +} + +// fields: userid, teamid, role +// called at server/teams.go createTeam & when someone clicks "join team" +// DONT MESS WITH BELOW. IT WORKS. +func AddTeamMember(userId pgtype.UUID, teamUUID pgtype.UUID, role string) (userID pgtype.UUID, err error) { + // userId prints something like: {[22 162 173 240 222 76 79 42 174 62 196 207 243 22 25 78] true} + + teamMember, err := GetRow[CreateTeamMember]( + `INSERT INTO team_members + (user_id, team_id, team_role) + VALUES ($1, $2, $3) + RETURNING user_id, team_id, team_role`, userId, teamUUID, role) + if err != nil { + fmt.Println(err) + return userId, err + } + return teamMember.UserId, err +} + +func RemoveTeamMember(teamId pgtype.UUID, userId pgtype.UUID) (DBTeamMember, error) { + deletedMember, err := GetRow[DBTeamMember](` + DELETE FROM team_members + WHERE team_id = $1 AND user_id = $2 + RETURNING team_id, user_id, team_role, created_on AS user_created_on`, + teamId, userId) + if err != nil { + logger.Error("Failed to remove team member: %v", err) + return DBTeamMember{}, err + } + return deletedMember, nil +} + +func GetMembersByTeamId(teamId pgtype.UUID) (*[]DBTeamMemberInfo, error) { + // In Go, you never return slice-data. + // Having * in sig means I'm returning the slice-header, which means I need & in my return + // Not having * means I'm returning a small copy of the slice-header, no need for & in my return + members, err := GetRows[DBTeamMemberInfo]( + // select all the info of a user (a user row) and their tm.role () + `SELECT u.*, tm.team_role + FROM team_members tm + INNER JOIN users u on (u.id = tm.user_id) + WHERE tm.team_id = $1`, + teamId) + return &members, err +} diff --git a/backend/app/database/users.go b/backend/app/database/users.go index 9f280a2..e2a2101 100644 --- a/backend/app/database/users.go +++ b/backend/app/database/users.go @@ -1,40 +1,118 @@ package database import ( + "fmt" + "github.com/emicklei/pgtalk/convert" "github.com/jackc/pgx/v5/pgtype" ) type DBUser struct { - Id pgtype.UUID `db:"id" ` - ServiceName string `db:"service_name"` - ServiceUserId string `db:"service_user_id" json:"-"` - DisplayName string `db:"display_name"` - CreatedOn pgtype.Timestamp `db:"created_on" json:"-"` + Id pgtype.UUID `db:"id"` + ServiceName string `db:"service_name"` + ServiceUserId string `db:"service_user_id"` + ServiceUserName string `db:"service_user_name"` + Role string `db:"role"` + DisplayName string `db:"display_name"` + AvatarId *string `db:"avatar_id"` + AccountStatus string `db:"account_status"` + LockDisplayName bool `db:"lock_display_name"` + CreatedOn pgtype.Timestamp `db:"created_on" json:"-"` } -func CreateUser(serviceName string, serviceUserId string, serviceDisplayName string) DBUser { +// Roles +const ( + Admin = "ADMIN" +) + +func CreateUser(serviceName string, serviceUserId string, serviceDisplayName string, avatarId string) DBUser { user, err := GetRow[DBUser]( - `INSERT INTO users (service_name, service_user_id, display_name) - VALUES ($1, $2, $3) - ON CONFLICT (service_name, service_user_id) - DO UPDATE - SET display_name = $3 - RETURNING *`, - serviceName, serviceUserId, serviceDisplayName) + `INSERT INTO users (service_name, service_user_id, service_user_name, display_name, avatar_id) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (service_name, service_user_id) + DO UPDATE + SET service_user_name = $3, avatar_id = $5 + RETURNING service_name, service_user_id, display_name, avatar_id`, + serviceName, serviceUserId, serviceDisplayName, avatarId) if err != nil { + fmt.Println(serviceName, serviceUserId, serviceDisplayName, avatarId) logger.Error("error getting user: %v", err) } return user } -func GetUser(userId pgtype.UUID) DBUser { +func GetUser(userId pgtype.UUID) (DBUser, error) { user, err := GetRow[DBUser]( `SELECT * - FROM users - WHERE id = $1`, + FROM users + WHERE id = $1`, userId) if err != nil { - logger.Error("error getting user: %v", err) + logger.Error("error getting user: id: %s, error: %v", convert.UUIDToString(userId), err) } - return user + return user, err +} + +func UpdateUser(user DBUser) (DBUser, error) { + user, err := GetRow[DBUser]( + `UPDATE users + SET display_name = $2 + WHERE id = $1 + RETURNING *`, + user.Id, user.DisplayName) + if err != nil { + logger.Error("error updating user: %v", err) + } + return user, err +} + +func GetAllUsers() ([]DBUser, error) { + users, err := GetRows[DBUser]( + `SELECT * + FROM users + ORDER BY ( + CASE -- put empty roles after actual roles + WHEN role is not null and length(role) > 0 then role + ELSE 'zzz' + END + ), service_user_name`) + return users, err +} + +func SetAccountStatus(userId pgtype.UUID, status string) (DBUser, error) { + user, err := GetRow[DBUser]( + `UPDATE users + SET account_status = $2 + WHERE id= $1 + RETURNING *`, + userId, status) + if err != nil { + logger.Error("SetAccountStatus Error: %v", err) + } + return user, err +} + +func SetDisplayName(userId pgtype.UUID, displayName string) (DBUser, error) { + user, err := GetRow[DBUser]( + `UPDATE users + SET display_name = $2 + WHERE id = $1 + RETURNING *`, + userId, displayName) + if err != nil { + logger.Error("SetDisplayName Error: %v", err) + } + return user, err +} + +func SetDisplayNameLock(userId pgtype.UUID, locked bool) (DBUser, error) { + user, err := GetRow[DBUser]( + `UPDATE users + SET lock_display_name = $2 + WHERE id = $1 + RETURNING *`, + userId, locked) + if err != nil { + logger.Error("SetDisplayNameLock Error: %v", err) + } + return user, err } diff --git a/backend/app/go.mod b/backend/app/go.mod index f12678e..513aa57 100644 --- a/backend/app/go.mod +++ b/backend/app/go.mod @@ -23,12 +23,16 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.19.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/golang-migrate/migrate/v4 v4.17.1 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/gomodule/redigo v2.0.0+incompatible // indirect github.com/google/uuid v1.5.0 // indirect github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.2.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect @@ -39,9 +43,11 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mrz1836/go-sanitize v1.3.2 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/atomic v1.7.0 // indirect golang.org/x/arch v0.7.0 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/net v0.22.0 // indirect diff --git a/backend/app/go.sum b/backend/app/go.sum index ebd9d76..93d68c5 100644 --- a/backend/app/go.sum +++ b/backend/app/go.sum @@ -35,10 +35,14 @@ github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= +github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s= +github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -58,6 +62,13 @@ github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pw github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= +github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= @@ -68,6 +79,8 @@ github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jteeuwen/go-bindata v3.0.7+incompatible h1:91Uy4d9SYVr1kyTJ15wJsog+esAZZl7JmEfTkwmhJts= +github.com/jteeuwen/go-bindata v3.0.7+incompatible/go.mod h1:JVvhzYOiGBnFSYRyV00iY8q7/0PThjIYav1p9h5dmKs= github.com/jwalton/go-supportscolor v1.2.0 h1:g6Ha4u7Vm3LIsQ5wmeBpS4gazu0UP1DRDE8y6bre4H8= github.com/jwalton/go-supportscolor v1.2.0/go.mod h1:hFVUAZV2cWg+WFFC4v8pT2X/S2qUUBYMioBD9AINXGs= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -87,6 +100,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mrz1836/go-sanitize v1.3.2 h1:sGhusPxP4L+7NAUVAUl/WrrBazNSNECsvYh3yumuJXQ= +github.com/mrz1836/go-sanitize v1.3.2/go.mod h1:wvRS2ALFDxOCK3ORQPwKUxl7HTIBUV8S3U34Hwn96r4= github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -109,6 +124,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= diff --git a/backend/app/integrations/generic.go b/backend/app/integrations/generic.go index 642872d..a651a15 100644 --- a/backend/app/integrations/generic.go +++ b/backend/app/integrations/generic.go @@ -4,6 +4,7 @@ import ( "codejam.io/integrations/discord" "codejam.io/integrations/github" "codejam.io/logging" + "encoding/json" "strings" ) @@ -12,8 +13,8 @@ var logger = logging.NewLogger(logging.Options{Name: "Integrations", Level: logg type IntegrationUser struct { IntegrationName string UserId string - DisplayName string - AvatarUrl string + ServiceUserName string + AvatarId string } func getGitHubUser(accessToken string) *IntegrationUser { @@ -23,21 +24,29 @@ func getGitHubUser(accessToken string) *IntegrationUser { } else { return &IntegrationUser{ IntegrationName: "github", - UserId: user["id"].(string), + UserId: string(user["id"].(json.Number)), } } } func getDiscordUser(accessToken string) *IntegrationUser { user := discord.GetUser(accessToken) + avatar, ok := user["avatar"].(string) + if !ok { + avatar = "" // Or set a default avatar URL if preferred + } if user == nil { logger.Error("User not found for token: %s", accessToken) return nil } else { + if user["avatar"] != nil { + avatar = user["avatar"].(string) + } return &IntegrationUser{ IntegrationName: "discord", UserId: user["id"].(string), - DisplayName: user["global_name"].(string), + ServiceUserName: user["global_name"].(string), + AvatarId: avatar, } } } @@ -51,5 +60,4 @@ func GetUser(integrationName string, accessToken string) *IntegrationUser { default: return nil } - } diff --git a/backend/app/main.go b/backend/app/main.go index cc287a7..5dc2db1 100644 --- a/backend/app/main.go +++ b/backend/app/main.go @@ -1,14 +1,20 @@ package main import ( + "codejam.io/config" "codejam.io/database" + //"codejam.io/logging" + "flag" + "codejam.io/server" "github.com/gin-gonic/gin" ) func main() { + debugArg := flag.Bool("debug", false, "Enable debug mode.") + flag.Parse() // TODO setup logger // Disables the debug logging... Comment this out to enable debug logging for GIN @@ -26,9 +32,11 @@ func main() { config.LoadFromFile("config.toml") database.Initialize(config.Database) + database.RunMigrations() server := server.Server{ Config: *config, + Debug: *debugArg, } server.StartServer() } diff --git a/backend/app/server/admin_common_api.go b/backend/app/server/admin_common_api.go new file mode 100644 index 0000000..6bfb436 --- /dev/null +++ b/backend/app/server/admin_common_api.go @@ -0,0 +1,88 @@ +package server + +import ( + "codejam.io/database" + "github.com/emicklei/pgtalk/convert" + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "net/http" +) + +// VerifyAdminAccess checks if the user has admin access by retrieving the session user account +// and verifying if their role is 'ADMIN'. Appropriate HTTP responses will be set automatically. +// Returns true if the user has admin access, false otherwise. +func (server *Server) VerifyAdminAccess(ctx *gin.Context) bool { + session := sessions.Default(ctx) + userId := session.Get("userId") + if userId == nil { + ctx.Status(http.StatusUnauthorized) + return false + } + + // get the session user account so we can verify they're an admin - TODO, pull from cache + currentUser, err := database.GetUser(convert.StringToUUID(userId.(string))) + if err != nil { + logger.Error("AdminAccessVerified: GetUser for session user error: %v", err) + ctx.Status(http.StatusInternalServerError) + return false + } + + if currentUser.Role != database.Admin { + logger.Error("AdminAccessVerified: unauthorized user: %v", userId) + ctx.Status(http.StatusForbidden) + return false + } + + return true +} + +// UserIsAdmin checks if the user with the given userId has admin access by retrieving the user from the database +// and verifying if their role is 'ADMIN'. Returns true if the user has admin access, false otherwise. +func (server *Server) UserIsAdmin(userId string) (bool, error) { + user, err := database.GetUser(convert.StringToUUID(userId)) + if err != nil { + return false, err + } + + if user.Role == database.Admin { + return true, nil + } + + return false, nil +} + +// VerifyUserNotAdmin checks if the user identified by the given userId is not an admin. This is +// for scenarios when admin accounts should not allow operations to take place on them, such as +// moderation actions. +// Appropriate HTTP responses are set automatically. +// Returns true if the user is not an admin, false otherwise. +func (server *Server) VerifyUserNotAdmin(ctx *gin.Context, userId string) bool { + // Admin accounts cannot be locked, even by other admins + isAdmin, err := server.UserIsAdmin(userId) + if err != nil { + logger.Error("VerifyUserNotAdmin: UserIsAdmin error: %v", err) + ctx.Status(http.StatusInternalServerError) + return false + } + + if isAdmin { + logger.Error("VerifyUserNotAdmin: UserIsAdmin TRUE") + ctx.Status(http.StatusForbidden) + return false + } + + return true +} + +// DeserializeRequest is a helper function to handle deserialization and automatically handling errors. +// Appropriate HTTP responses are set automatically +// Returns true if the request deserialized successfully, false otherwise +func (server *Server) DeserializeRequest(ctx *gin.Context, request any) bool { + err := ctx.ShouldBindJSON(&request) + if err != nil { + logger.Error("%T ShouldBindJSON error: %v", request, err) + ctx.Status(http.StatusBadRequest) + return false + } + return true +} diff --git a/backend/app/server/admin_user_api.go b/backend/app/server/admin_user_api.go new file mode 100644 index 0000000..9496d0e --- /dev/null +++ b/backend/app/server/admin_user_api.go @@ -0,0 +1,122 @@ +package server + +import ( + "codejam.io/database" + "github.com/emicklei/pgtalk/convert" + "github.com/gin-gonic/gin" + "net/http" +) + +func (server *Server) GetAllUsers(ctx *gin.Context) { + if server.VerifyAdminAccess(ctx) { + users, err := database.GetAllUsers() + if err != nil { + logger.Error("Error getting all users: %v", err) + ctx.Status(http.StatusInternalServerError) + return + } + ctx.JSON(http.StatusOK, users) + } +} + +type PutAccountStatusRequest struct { + AccountStatus string +} + +func (server *Server) PutAccountStatus(ctx *gin.Context) { + userIdParam := ctx.Param("id") + var request PutAccountStatusRequest + if server.VerifyAdminAccess(ctx) && + server.VerifyUserNotAdmin(ctx, userIdParam) && + server.DeserializeRequest(ctx, &request) { + user, err := database.SetAccountStatus(convert.StringToUUID(userIdParam), request.AccountStatus) + if err != nil { + logger.Error("PutAccountStatus: SetAccountStatus error: %v", err) + ctx.Status(http.StatusInternalServerError) + return + } + ctx.JSON(http.StatusOK, user) + } +} + +type PutDisplayNameRequest struct { + DisplayName string +} + +func (server *Server) PutDisplayName(ctx *gin.Context) { + userIdParam := ctx.Param("id") + var request PutDisplayNameRequest + if server.VerifyAdminAccess(ctx) && + server.VerifyUserNotAdmin(ctx, userIdParam) && + server.DeserializeRequest(ctx, &request) { + user, err := database.SetDisplayName(convert.StringToUUID(userIdParam), request.DisplayName) + if err != nil { + logger.Error("SetDisplayName error: %v", err) + ctx.Status(http.StatusInternalServerError) + return + } + ctx.JSON(http.StatusOK, user) + } +} + +type PutDisplayNameLockRequest struct { + Lock bool +} + +func (server *Server) PutDisplayNameLock(ctx *gin.Context) { + userIdParam := ctx.Param("id") + var request PutDisplayNameLockRequest + if server.VerifyAdminAccess(ctx) && + server.VerifyUserNotAdmin(ctx, userIdParam) && + server.DeserializeRequest(ctx, &request) { + user, err := database.SetDisplayNameLock(convert.StringToUUID(userIdParam), request.Lock) + if err != nil { + logger.Error("SetDisplayNameLock error: %v", err) + ctx.Status(http.StatusInternalServerError) + return + } + ctx.JSON(http.StatusOK, user) + } +} + +func (server *Server) PutBan(ctx *gin.Context) { + userIdParam := ctx.Param("id") + if server.VerifyAdminAccess(ctx) && + server.VerifyUserNotAdmin(ctx, userIdParam) { + user, err := database.SetAccountStatus(convert.StringToUUID(userIdParam), "BANNED") + if err != nil { + logger.Error("PutBan: SetAccountStatus error: %v", err) + ctx.Status(http.StatusInternalServerError) + return + } + ctx.JSON(http.StatusOK, user) + } +} + +func (server *Server) PutUnban(ctx *gin.Context) { + userIdParam := ctx.Param("id") + if server.VerifyAdminAccess(ctx) && + server.VerifyUserNotAdmin(ctx, userIdParam) { + user, err := database.SetAccountStatus(convert.StringToUUID(userIdParam), "ACTIVE") + if err != nil { + logger.Error("PutUnban: SetAccountStatus error: %v", err) + ctx.Status(http.StatusInternalServerError) + return + } + ctx.JSON(http.StatusOK, user) + } +} + +func (server *Server) SetupAdminUserRoutes() { + logger.Info("Setting up Admin User routes...") + + group := server.Gin.Group("/admin/user") + { + group.GET("/all", server.GetAllUsers) + group.PUT("/:id/display_name/", server.PutDisplayName) + group.PUT("/:id/display_name_lock/", server.PutDisplayNameLock) + group.PUT("/:id/ban/", server.PutBan) + group.PUT("/:id/unban/", server.PutUnban) + } + +} diff --git a/backend/app/server/event.go b/backend/app/server/event.go index 05347bd..b001bee 100644 --- a/backend/app/server/event.go +++ b/backend/app/server/event.go @@ -2,12 +2,36 @@ package server import ( "codejam.io/database" + "codejam.io/server/models" + "errors" "github.com/emicklei/pgtalk/convert" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v5" + "github.com/mrz1836/go-sanitize" "net/http" + "strings" ) +type CodeJamEvent struct { + database.DBEvent + AllowSignups bool +} + +func sanitizeEvent(event *database.DBEvent) { + event.Title = sanitize.Scripts(event.Title) + event.Description = sanitize.Scripts(event.Description) + event.Timeline = sanitize.Scripts(event.Timeline) + event.Rules = sanitize.Scripts(event.Rules) +} + +func validateEvent(event database.DBEvent, response *models.FormResponse) { + // Title is required + if strings.Trim(event.Title, " ") == "" { + response.AddError("Title", "required") + } +} + func (server *Server) GetAllEvents(ctx *gin.Context) { events, err := database.GetEvents() if err == nil { @@ -28,7 +52,28 @@ func (server *Server) GetEvent(ctx *gin.Context) { } } -func (server *Server) CreateEvent(ctx *gin.Context) { +func (server *Server) GetActiveEvent(ctx *gin.Context) { + event, err := database.GetActiveEvent() + + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + // NoRows means no active event exist (or it's still set to PLANNING) + ctx.Status(http.StatusNoContent) + } else { + logger.Error("GetActiveEvent error: %v", err) + ctx.Status(http.StatusInternalServerError) + } + return + } + + var codeJamEvent = CodeJamEvent{ + DBEvent: event, + AllowSignups: server.signupsAllowed(convert.UUIDToString(event.Id)), + } + ctx.JSON(http.StatusOK, codeJamEvent) +} + +func (server *Server) PostEvent(ctx *gin.Context) { session := sessions.Default(ctx) userId := session.Get("userId") if userId != nil { @@ -44,35 +89,78 @@ func (server *Server) CreateEvent(ctx *gin.Context) { } } -func (server *Server) UpdateEvent(ctx *gin.Context) { +func (server *Server) PutEvent(ctx *gin.Context) { session := sessions.Default(ctx) userId := session.Get("userId") if userId != nil { var event database.DBEvent + + // taking submitted data and putting it into an object err := ctx.ShouldBindJSON(&event) if err != nil { logger.Error("UpdateEvent Request ShouldBindJSON error: %v", err) ctx.Status(http.StatusBadRequest) return } + + // Check Authorization + user, err := database.GetUser(convert.StringToUUID(userId.(string))) + if err != nil { + ctx.Status(http.StatusInternalServerError) + return + } + + if user.Role != database.Admin { + logger.Error("PutEvent unauthorized user: %v", userId) + ctx.Status(http.StatusUnauthorized) + return + } + + response := models.NewFormResponse() + + sanitizeEvent(&event) + validateEvent(event, &response) + + // Perform validation + if len(response.Errors) > 0 { + ctx.JSON(http.StatusBadRequest, response) + return + } + + // Update the event in the DB event, err = database.UpdateEvent(event) if err != nil { logger.Error("Error calling database.UpdateEvent: %v", err) ctx.Status(http.StatusInternalServerError) } else { - ctx.JSON(http.StatusOK, event) + logger.Info("User %v updated Event %v", userId, event.Id) + response.Data = event + ctx.JSON(http.StatusOK, response) } } else { + logger.Error("PutEvent Unauthorized: no session") ctx.Status(http.StatusUnauthorized) } } +func (server *Server) GetStatuses(ctx *gin.Context) { + statuses, err := database.GetStatuses() + if err != nil { + logger.Error("Error in GetStatuses: %v", err) + ctx.Status(http.StatusInternalServerError) + } else { + ctx.JSON(http.StatusOK, statuses) + } +} + func (server *Server) SetupEventRoutes() { group := server.Gin.Group("/event") { group.GET("/", server.GetAllEvents) + group.GET("/active", server.GetActiveEvent) group.GET("/:id", server.GetEvent) - group.PUT("/:id", server.UpdateEvent) - group.POST("/", server.CreateEvent) + group.PUT("/:id", server.PutEvent) + group.POST("/", server.PostEvent) + group.GET("/statuses", server.GetStatuses) } } diff --git a/backend/app/server/models/form_response.go b/backend/app/server/models/form_response.go new file mode 100644 index 0000000..83d6fc4 --- /dev/null +++ b/backend/app/server/models/form_response.go @@ -0,0 +1,28 @@ +package models + +type FormError struct { + Field string + Error string +} + +type FormResponse struct { + Errors []FormError + Data any +} + +func NewFormResponse() FormResponse { + var errors []FormError + response := FormResponse{ + Errors: errors, + Data: nil, + } + return response +} + +func (fr *FormResponse) AddError(field string, message string) { + err := FormError{ + Field: field, + Error: message, + } + fr.Errors = append(fr.Errors, err) +} diff --git a/backend/app/server/oauth.go b/backend/app/server/oauth.go index ef88ba7..00e85f2 100644 --- a/backend/app/server/oauth.go +++ b/backend/app/server/oauth.go @@ -1,6 +1,11 @@ package server import ( + "fmt" + "net/http" + "os" + "strings" + "codejam.io/database" "codejam.io/integrations" "github.com/emicklei/pgtalk/convert" @@ -8,13 +13,20 @@ import ( "github.com/gin-gonic/gin" "golang.org/x/oauth2" githubOAuth "golang.org/x/oauth2/github" - "net/http" - "os" - "strings" ) +type StateData struct { + Token string + Redirect string +} + // SetupOAuth initializes the OAuth provider specified in the application config. func (server *Server) SetupOAuth() { + if server.Debug { + logger.Warn("Debug mode is set. No OAuth Providers are set!") + return + } + var endpoint oauth2.Endpoint switch strings.ToLower(server.Config.OAuth.Provider) { @@ -40,13 +52,92 @@ func (server *Server) SetupOAuth() { } func (server *Server) GetOAuthRedirect(ctx *gin.Context) { - url := server.OAuth.AuthCodeURL(ctx.Request.Header.Get("Referer")) + session := sessions.Default(ctx) + token, err := GenerateToken(16) + + if err != nil { + ctx.String(500, "Internal Server Error") + } + + redirect := ctx.Query("redirect") + if redirect == "" { + redirect = "/" + } + + if !strings.HasPrefix(redirect, "/") { + redirect = "/" + } else if strings.HasPrefix(redirect, "/oauth") { + redirect = "/" + } else { + redirect = fmt.Sprintf("/#%s", redirect) + } + + state := StateData{Token: token, Redirect: redirect} + session.Set("state", state) + err = session.Save() + + if err != nil { + logger.Error("Error saving session: %v", err) + } + + if server.Debug { + ctx.Redirect(http.StatusFound, "/oauth/debug-login") + return + } + + url := server.OAuth.AuthCodeURL(token) ctx.Redirect(http.StatusFound, url) } +func (server *Server) GetDebugSession(ctx *gin.Context) { + redir := ctx.Query("state") + + dbUser := database.CreateUser("discord", "0", "DebugCow", "") + session := sessions.Default(ctx) + session.Set("userId", convert.UUIDToString(dbUser.Id)) + session.Set("displayName", dbUser.DisplayName) + + err := session.Save() + + if err != nil { + logger.Error("Error saving debug session: %v", err) + } + + ctx.Redirect(http.StatusFound, redir) +} + func (server *Server) GetOAuthCallback(ctx *gin.Context) { authCode := ctx.Query("code") - redir := ctx.Query("state") + stateCode := ctx.Query("state") + fmt.Println(stateCode) + + if len(stateCode) == 0 { + ctx.String(400, "Bad Request: Missing State Value.") + return + } + + session := sessions.Default(ctx) + stateI := session.Get("state") + + session.Clear() + session.Save() + + var stateData *StateData + // give the stateI a defined type/struct of stateData instead of interface{}/any: + stateData, ok := stateI.(*StateData) + if !ok { + ctx.String(400, "Bad Request: Invalid State Data.") + return + } + + if stateData.Token != stateCode { + logger.Error("Invalid state token provided.") + ctx.String(400, "Bad Request: Invalid State Code Provided.") + return + } + + redir := stateData.Redirect + token, err := server.OAuth.Exchange(oauth2.NoContext, authCode) if err != nil { // todo - can any of these be handled? @@ -57,8 +148,7 @@ func (server *Server) GetOAuthCallback(ctx *gin.Context) { integrationName := strings.ToLower(server.Config.OAuth.Provider) providerUser := integrations.GetUser(integrationName, token.AccessToken) if providerUser != nil { - dbUser := database.CreateUser(integrationName, providerUser.UserId, providerUser.DisplayName) - session := sessions.Default(ctx) + dbUser := database.CreateUser(integrationName, providerUser.UserId, providerUser.ServiceUserName, providerUser.AvatarId) session.Set("userId", convert.UUIDToString(dbUser.Id)) session.Set("displayName", dbUser.DisplayName) err = session.Save() @@ -80,5 +170,6 @@ func (server *Server) SetupOAuthRoutes() { { group.GET("/redirect", server.GetOAuthRedirect) group.GET("/callback", server.GetOAuthCallback) + group.GET("/debug-login", server.GetDebugSession) } } diff --git a/backend/app/server/server.go b/backend/app/server/server.go index 4b1e1fa..1536db1 100644 --- a/backend/app/server/server.go +++ b/backend/app/server/server.go @@ -3,6 +3,7 @@ package server import ( "codejam.io/config" "codejam.io/logging" + "encoding/gob" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/redis" "github.com/gin-gonic/gin" @@ -18,6 +19,7 @@ type Server struct { Config config.Config OAuth *oauth2.Config Gin *gin.Engine + Debug bool } func (server *Server) SetupSessionStore() { @@ -46,6 +48,8 @@ func (server *Server) SetupSessionStore() { func (server *Server) StartServer() { logger.Info("Starting server...") + gob.Register(&StateData{}) + server.Gin = gin.Default() server.SetupSessionStore() @@ -55,6 +59,9 @@ func (server *Server) StartServer() { server.SetupOAuthRoutes() server.SetupUserRoutes() server.SetupEventRoutes() + server.SetupTeamRoutes() + server.SetupAdminUserRoutes() + server.SetupStaticRoutes() // Start the server... diff --git a/backend/app/server/static.go b/backend/app/server/static.go index 197cfa3..9b4a6ff 100644 --- a/backend/app/server/static.go +++ b/backend/app/server/static.go @@ -2,7 +2,6 @@ package server import ( "embed" - _ "embed" "github.com/gin-gonic/gin" "mime" "net/http" diff --git a/backend/app/server/teams.go b/backend/app/server/teams.go new file mode 100644 index 0000000..df44c66 --- /dev/null +++ b/backend/app/server/teams.go @@ -0,0 +1,409 @@ +package server + +import ( + "fmt" + "net/http" + + "codejam.io/database" + "github.com/emicklei/pgtalk/convert" + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v5/pgtype" + + "crypto/md5" + "crypto/rand" + "encoding/hex" + "math" + "math/big" +) + +type CreateTeamRequest struct { + // sent from UI, to be processed in the server into a DBTeam structure + // referenced in server/teams.go/CreateTeam line 53 + EventId string + Name string + Visibility string + Availability string + Description string + Technologies string + Timezone string +} + +type GetTeamResponse struct { + Team *database.DBTeam + Event *database.DBEvent + TeamMembers *[]database.DBTeamMemberInfo // array(slice) of a struct +} + +type InvitePayload struct { + TeamId string `json:"teamId"` + InviteCode string `json:"inviteCode"` +} + +type JoinPayload struct { + TeamId string `json:"teamId"` +} + +type MemberPayload struct { + TeamId string `json:"teamId"` + MemberId string `json:"memberId"` +} + +func MD5HashCode(teamName string) (string, error) { + randNum, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + return "Md5 hash error", err + } + hash := md5.Sum([]byte(teamName + "." + randNum.String())) + return hex.EncodeToString(hash[:7]), nil +} + +func (server *Server) signupsAllowed(eventId string) bool { + statusCode, err := database.GetEventStatusCode(convert.StringToUUID(eventId)) + if err != nil { + logger.Error("Error in GetEventStatusCode: %v+", err) + return false + } + + if statusCode == "SIGNUP" || statusCode == "STARTED" { + return true + } else { + return false + } +} + +// for path teams/browse +func (server *Server) GetAllTeams(ctx *gin.Context) { + // what if there's no session => no user id + teams, err := database.GetTeams() + if err == nil { + ctx.JSON(http.StatusOK, teams) + } else { + fmt.Println("ERROR: ", err) + ctx.Status(http.StatusInternalServerError) + return + } +} + +func (server *Server) GetUserTeams(ctx *gin.Context) { + session := sessions.Default(ctx) + userId := session.Get("userId") + + if userId == nil { + ctx.Status(http.StatusNotFound) + return + } + strUserId := userId.(string) + + teams, err := database.GetUserTeams(convert.StringToUUID(strUserId)) + if err != nil { + ctx.Status(http.StatusInternalServerError) + return + } + + ctx.JSON(http.StatusOK, teams) +} + +// purpose is to construct the DBTeamMemberInfo +func (server *Server) GetTeamInfo(id pgtype.UUID) (*GetTeamResponse, error) { + + var teamResponse GetTeamResponse + var team database.DBTeam + var event database.DBEvent + var teamMembers *[]database.DBTeamMemberInfo //user info based on teamId + // fmt.Println("========id: ", id) prints: {[204 69 126 62 33 10 77 93 131 216 8 153 66 109 252 147] true} + team, err := database.GetTeam(id) + if err != nil { + logger.Error("failed to get team: %v", err) + return nil, err + } + + event, err = database.GetEvent(team.EventId) + if err != nil { + logger.Error("failed to get event: %v", err) + return nil, err + } + + teamMembers, err = database.GetMembersByTeamId(team.Id) + if err != nil { + logger.Error("failed to get event: %v", err) + return nil, err + } + + // attach all 3 structures to GetTeamResponse --> nested structs turn into nested JSON (with ctx.JSON) + teamResponse.Team = &team + teamResponse.Event = &event + teamResponse.TeamMembers = teamMembers + + return &teamResponse, nil +} + +func (server *Server) sendTeamInfo(ctx *gin.Context) { + id := convert.StringToUUID(ctx.Param("id")) + + teamResponse, err := server.GetTeamInfo(id) + + if err != nil { + ctx.Status(http.StatusBadRequest) + } + fmt.Printf("%+v\n", teamResponse.TeamMembers) + ctx.JSON(http.StatusOK, teamResponse) +} + +func (server *Server) GetTeamInfoByInviteCode(ctx *gin.Context) { + inviteCode := ctx.Param("invitecode") + + var teamResponse GetTeamResponse + var team database.DBTeam + var event database.DBEvent + var teamMembers *[]database.DBTeamMemberInfo //user info based on teamId + + team, err := database.GetTeamByInvite(inviteCode) + if err != nil { + logger.Error("failed to get team: %v", err) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get team: %v", err)}) + return + } else { + fmt.Println("TEST if GetTeamByInvite was SUCCESS: ", err, " + ", team) + } + + event, err = database.GetEvent(team.EventId) + if err != nil { + logger.Error("failed to get event: %v", err) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get event: %v", err)}) + return + } + + teamMembers, err = database.GetMembersByTeamId(team.Id) + if err != nil { + logger.Error("failed to get event: %v", err) + ctx.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get members: %v", err)}) + return + } + + // attach all 3 structures to GetTeamResponse --> nested structs turn into nested JSON (with ctx.JSON) + teamResponse.Team = &team + teamResponse.Event = &event + teamResponse.TeamMembers = teamMembers + + fmt.Println(teamResponse) + ctx.JSON(http.StatusOK, teamResponse) +} + +func (server *Server) CreateTeam(ctx *gin.Context) { + // ctx of *gin.Context has HTTP request info. + // Step 4: Post Team Data API (TWO PARTS 1) create team 2) add team members) + session := sessions.Default(ctx) + userId := session.Get("userId") + if userId == nil { + ctx.Status(http.StatusUnauthorized) + return + } + strUserId := userId.(string) + + var team database.DBTeam + var teamReq CreateTeamRequest + + err := ctx.ShouldBindJSON(&teamReq) + if err != nil { + logger.Error("CreateTeam Request ShouldBindJSON error: %v", err) + ctx.Status(http.StatusBadRequest) + return + } + + if !server.signupsAllowed(teamReq.EventId) { + ctx.Status(http.StatusForbidden) + return + } + + // CONVERT teamReq to team + team.EventId = convert.StringToUUID(teamReq.EventId) + team.Name = teamReq.Name + team.Availability = teamReq.Availability + team.Description = teamReq.Description + team.Visibility = teamReq.Visibility + team.Technologies = teamReq.Technologies + team.Timezone = teamReq.Timezone + + md5code, err := MD5HashCode(team.Name) + if err != nil { + logger.Error("Error - Md5HashCode failed", err) + ctx.Status(http.StatusInternalServerError) + return + } + team.InviteCode = md5code + + fmt.Printf("%+v", team) + + // INSERTS TEAM into DB + // PART 1/2 DONE + teamUUID, err := database.CreateTeam(team) + if err != nil { + logger.Error("Error trying to CreateTeam(team)") + ctx.Status(http.StatusBadRequest) + return + } + fmt.Println(convert.StringToUUID(strUserId), teamUUID) + + // PART 2/2 DONE + // construct TeamMember + _, err = database.AddTeamMember(convert.StringToUUID(strUserId), teamUUID, "owner") + + if err == nil { + ctx.JSON(http.StatusCreated, map[string]pgtype.UUID{ + "id": teamUUID, + }) + } else { + fmt.Println(err) + logger.Error("AddTeamMember error: %v for user %s", err, strUserId) + ctx.Status(http.StatusInternalServerError) + return + } +} + +func (server *Server) UpdateTeam(ctx *gin.Context) { + session := sessions.Default(ctx) + userId := session.Get("userId") + + if userId != nil { + var team database.DBTeam + err := ctx.ShouldBindJSON(&team) // "message incoming data to this struct" + + if err != nil { + logger.Error("UpdateEvent Request ShouldBindJSON error: %v", err) + ctx.Status(http.StatusBadRequest) + return + } + + updatedTeam, err := database.UpdateTeam(team) + if err != nil { + logger.Error("Error calling database.UpdateTeam: %v", err) + ctx.Status(http.StatusInternalServerError) + } else { + ctx.JSON(http.StatusOK, updatedTeam) + } + } else { + ctx.Status(http.StatusUnauthorized) + } +} + +func (server *Server) RemoveTeamMember(ctx *gin.Context) { + session := sessions.Default(ctx) + userId := session.Get("userId") + if userId == nil { + ctx.Status(http.StatusUnauthorized) + return + } + + var MemberPayload MemberPayload + + if err := ctx.ShouldBindJSON(&MemberPayload); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + uuidTeamId := convert.StringToUUID(MemberPayload.TeamId) + uuidMemberId := convert.StringToUUID(MemberPayload.MemberId) + + removedMember, err := database.RemoveTeamMember(uuidTeamId, uuidMemberId) + if err != nil { + fmt.Println("Error removing team member: ", err) + } + + strTeamId := convert.UUIDToString(removedMember.TeamId) + + ctx.JSON(http.StatusOK, strTeamId) +} + +func (server *Server) MemberJoin(ctx *gin.Context) { + session := sessions.Default(ctx) + userId := session.Get("userId") + + if userId == nil { + ctx.Status(http.StatusUnauthorized) + return + } + + strUserId := userId.(string) + uuidUserId := convert.StringToUUID(strUserId) + + var teamId JoinPayload + + if err := ctx.ShouldBindJSON(&teamId); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // teamId prints: {cc457e3e-210a-4d5d-83d8-0899426dfc93} + uuidTeamId := convert.StringToUUID(teamId.TeamId) + teamInfo, err := server.GetTeamInfo(uuidTeamId) + + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // verifies team is public + var isPublic string = teamInfo.Team.Visibility + if isPublic == "private" { + ctx.Status(http.StatusForbidden) + return + } + _, err = database.AddTeamMember(uuidUserId, uuidTeamId, "member") + if err != nil { + ctx.JSON(http.StatusConflict, err) + return + } + strTeamId := convert.UUIDToString(teamInfo.Team.Id) + ctx.JSON(http.StatusOK, strTeamId) +} + +func (server *Server) MemberInvite(ctx *gin.Context) { + session := sessions.Default(ctx) + userId := session.Get("userId") + + var payload InvitePayload + // bind the message context to the structure, and do an error check + if err := ctx.ShouldBindJSON(&payload); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + inviteCode := payload.InviteCode + teamId := payload.TeamId + uuidTeamId := convert.StringToUUID(teamId) + strUserId := userId.(string) + uuidUserId := convert.StringToUUID(strUserId) + + // I want to check if the inviteCode matches the teams invite code + dbInviteCode, err := database.GetTeamInviteCode(uuidTeamId) + if err != nil { + ctx.JSON(http.StatusBadRequest, inviteCode) + } + fmt.Println("server dbInviteCode: ", dbInviteCode) + if inviteCode != dbInviteCode.InviteCode { + fmt.Println("invalid request") + } else { + _, err = database.AddTeamMember(uuidUserId, uuidTeamId, "member") + if err != nil { + ctx.JSON(http.StatusConflict, err) + return + } + } +} + +func (server *Server) SetupTeamRoutes() { + group := server.Gin.Group("/team") + { + group.POST("/", server.CreateTeam) + group.POST("/join", server.MemberJoin) + group.POST("/:invitecode", server.MemberInvite) + + group.GET("/:id", server.sendTeamInfo) + group.GET("/invite/:invitecode", server.GetTeamInfoByInviteCode) + + group.PUT("/edit/:teamid", server.UpdateTeam) // for admin to remove people + group.DELETE("/:teamid/member/:memberid", server.RemoveTeamMember) + } + server.Gin.GET("/teams", server.GetUserTeams) // I think this works rofl + server.Gin.GET("/teams/browse", server.GetAllTeams) +} diff --git a/backend/app/server/user.go b/backend/app/server/user.go deleted file mode 100644 index dc634f7..0000000 --- a/backend/app/server/user.go +++ /dev/null @@ -1,45 +0,0 @@ -package server - -import ( - "codejam.io/database" - "github.com/emicklei/pgtalk/convert" - "github.com/gin-contrib/sessions" - "github.com/gin-gonic/gin" - "net/http" -) - -func (server *Server) GetUser(ctx *gin.Context) { - session := sessions.Default(ctx) - userId := session.Get("userId") - if userId != nil { - dbUser := database.GetUser(convert.StringToUUID(userId.(string))) - ctx.JSON(http.StatusOK, dbUser) - return - } - ctx.Status(http.StatusUnauthorized) -} - -// Logout is a GET route for logging out a user. -// This involved clearing the session cookie, clearing/deleting the entry from the session store -func (server *Server) Logout(ctx *gin.Context) { - session := sessions.Default(ctx) - session.Clear() - err := session.Save() - if err != nil { - logger.Error("Logout: error saving session: %v", err) - } - - // Have to manually do this, not sure why the session middleware doesn't handle this... - ctx.SetCookie(SessionCookieName, "", -1, "/", "", false, false) - ctx.Redirect(http.StatusFound, ctx.Request.Header.Get("Referer")) -} - -func (server *Server) SetupUserRoutes() { - logger.Info("Setting up User routes...") - - group := server.Gin.Group("/user") - { - group.GET("/", server.GetUser) - group.GET("/logout", server.Logout) - } -} diff --git a/backend/app/server/users_api.go b/backend/app/server/users_api.go new file mode 100644 index 0000000..54d6fe4 --- /dev/null +++ b/backend/app/server/users_api.go @@ -0,0 +1,117 @@ +package server + +import ( + "codejam.io/database" + "codejam.io/server/models" + "github.com/emicklei/pgtalk/convert" + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "net/http" + "strings" +) + +func validateDisplayName(displayName string, response *models.FormResponse) { + if displayName == "" { + response.AddError("DisplayName", "required") + } +} + +func (server *Server) GetUser(ctx *gin.Context) { + session := sessions.Default(ctx) + userId := session.Get("userId") + if userId != nil { + dbUser, err := database.GetUser(convert.StringToUUID(userId.(string))) + if err != nil { + logger.Error("GetUser: %v, %v", userId, err) + ctx.Status(http.StatusInternalServerError) + return + } + ctx.JSON(http.StatusOK, dbUser) + return + } + ctx.Status(http.StatusUnauthorized) +} + +type PutProfileRequest struct { + DisplayName string +} + +func (server *Server) PutProfile(ctx *gin.Context) { + session := sessions.Default(ctx) + userId := session.Get("userId") + if userId != nil { + user, err := database.GetUser(convert.StringToUUID(userId.(string))) + if err != nil { + logger.Error("PutProfile: GetUser error: %v", err) + ctx.Status(http.StatusInternalServerError) + return + } + + // admin display name lock prevents a user from changing it + if user.LockDisplayName { + logger.Info("DisplayName Locked") + ctx.Status(http.StatusForbidden) + return + } + + var request PutProfileRequest + err = ctx.ShouldBindJSON(&request) + if err != nil { + ctx.Status(http.StatusBadRequest) + return + } + + response := models.NewFormResponse() + request.DisplayName = strings.Trim(request.DisplayName, " ") + + // Perform validation + validateDisplayName(request.DisplayName, &response) + if len(response.Errors) > 0 { + logger.Error("Validation Error: %v+", user) + ctx.JSON(http.StatusBadRequest, response) + return + } + + user.DisplayName = request.DisplayName + _, err = database.UpdateUser(user) + if err != nil { + logger.Error("Error calling database.UpdateEvent: %v", err) + ctx.Status(http.StatusInternalServerError) + return + } else { + response.Data = user + ctx.JSON(http.StatusOK, response) + } + + } else { + logger.Error("PutUser Unauthorized: no session") + ctx.Status(http.StatusUnauthorized) + } +} + +// Logout is a GET route for logging out a user. +// This involved clearing the session cookie, clearing/deleting the entry from the session store +func (server *Server) Logout(ctx *gin.Context) { + session := sessions.Default(ctx) + session.Clear() + err := session.Save() + if err != nil { + logger.Error("Logout: error saving session: %v", err) + } + + // Have to manually do this, not sure why the session middleware doesn't handle this... + ctx.SetCookie(SessionCookieName, "", -1, "/", "", false, false) + ctx.Redirect(http.StatusFound, ctx.Request.Header.Get("Referer")) +} + +func (server *Server) SetupUserRoutes() { + logger.Info("Setting up User routes...") + + group := server.Gin.Group("/user") + { + group.GET("/", server.GetUser) + group.PUT("/profile", server.PutProfile) + group.GET("/logout", server.Logout) + } + +} diff --git a/backend/app/server/utils.go b/backend/app/server/utils.go new file mode 100644 index 0000000..147ed60 --- /dev/null +++ b/backend/app/server/utils.go @@ -0,0 +1,17 @@ +package server + +import ( + "crypto/rand" + "encoding/base64" +) + +func GenerateToken(length uint8) (string, error) { + token := make([]byte, length) + + _, err := rand.Read(token) + if err != nil { + return "", err + } + + return base64.URLEncoding.EncodeToString(token), nil +} diff --git a/backend/database/schema.sql b/backend/database/schema.sql index 6f84fec..cdfdba9 100644 --- a/backend/database/schema.sql +++ b/backend/database/schema.sql @@ -15,6 +15,7 @@ CREATE TABLE IF NOT EXISTS events ( status_id integer, title TEXT NOT NULL, description TEXT NOT NULL, + timeline TEXT NOT NULL, rules TEXT NOT NULL, organizer_user_id UUID NOT NULL, max_teams integer default -1, diff --git a/build.sh b/build.sh index 2808635..df0980d 100755 --- a/build.sh +++ b/build.sh @@ -1,9 +1,41 @@ #!/bin/bash +# set -x # enable debug output +set -e # exit on error +trap ErrorHandler ERR + +function ErrorHandler { + echo -e "\e[91mError Occurred\e[0m" + exit 1 +} + +echo "Building Frontend" cd html || return npm install npm run build mkdir -p ../backend/app/server/static_files +rm -rf ../backend/app/server/static_files/* cp -r dist/* ../backend/app/server/static_files/ +cp -r static/ ../backend/app/server/static_files/ + +echo "Building Backend" + +cd ../backend/app || return +go build + +if [ -e codejam.io ]; then + if [ -e /opt/codejam/staging/codejam.io ]; then + rm /opt/codejam/staging/codejam.io + else + echo "WARNING: Existing deployment not found" + fi + + cp codejam.io /opt/codejam/staging/codejam.io +else + echo -e "\e[92mBackend Build Failed: missing binary file found\e[0m" + exit 1 +fi + +echo -e "\e[32mSuccess\e[0m" diff --git a/docker-compose.override.yaml b/docker-compose.override.yaml new file mode 100644 index 0000000..2caf1df --- /dev/null +++ b/docker-compose.override.yaml @@ -0,0 +1,10 @@ +version: '3.9' + +services: + database: + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: codejam + volumes: + - './docker/postgres/:/var/lib/postgresql/data' diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..ba40536 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,19 @@ +version: '3.9' + +services: + server: + image: 'timeenjoyed/codejam:latest' + container_name: codejam_backend + build: . + volumes: + - ./backend/app/config.toml:/app/config.toml + ports: + - '8080:8080' + + database: + image: 'library/postgres:16.3-alpine3.20' + container_name: codejam_database + + redis: + image: 'library/redis:7.0.15-alpine3.20' + container_name: codejam_redis diff --git a/html/index.html b/html/index.html index 0745c37..3157f02 100644 --- a/html/index.html +++ b/html/index.html @@ -3,6 +3,13 @@ + + + + + + + CodeJam diff --git a/html/package-lock.json b/html/package-lock.json index c132a36..f857f36 100644 --- a/html/package-lock.json +++ b/html/package-lock.json @@ -11,6 +11,7 @@ "@fortawesome/free-brands-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/svelte-fontawesome": "^0.2.2", + "svelte-sonner": "^0.3.24", "svelte-spa-router": "^4.0.1" }, "devDependencies": { @@ -18,7 +19,7 @@ "@tsconfig/svelte": "^5.0.2", "autoprefixer": "^10.4.16", "flowbite": "^2.3.0", - "flowbite-svelte": "^0.45.0", + "flowbite-svelte": "^0.46.5", "postcss": "^8.4.32", "postcss-load-config": "^5.0.2", "svelte": "^4.2.12", @@ -54,9 +55,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", - "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", + "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -434,18 +435,18 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", - "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", + "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", "dev": true, "dependencies": { - "@floating-ui/utils": "^0.2.1" + "@floating-ui/utils": "^0.2.0" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", - "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", + "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", "dev": true, "dependencies": { "@floating-ui/core": "^1.0.0", @@ -453,9 +454,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", - "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", + "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==", "dev": true }, "node_modules/@fortawesome/fontawesome-common-types": { @@ -629,9 +630,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.2.tgz", - "integrity": "sha512-3XFIDKWMFZrMnao1mJhnOT1h2g0169Os848NhhmGweEcfJ4rCi+3yMCOLG4zA61rbJdkcrM/DjVZm9Hg5p5w7g==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", + "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", "cpu": [ "arm" ], @@ -642,9 +643,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.2.tgz", - "integrity": "sha512-GdxxXbAuM7Y/YQM9/TwwP+L0omeE/lJAR1J+olu36c3LqqZEBdsIWeQ91KBe6nxwOnb06Xh7JS2U5ooWU5/LgQ==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", + "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", "cpu": [ "arm64" ], @@ -655,9 +656,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.2.tgz", - "integrity": "sha512-mCMlpzlBgOTdaFs83I4XRr8wNPveJiJX1RLfv4hggyIVhfB5mJfN4P8Z6yKh+oE4Luz+qq1P3kVdWrCKcMYrrA==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", + "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", "cpu": [ "arm64" ], @@ -668,9 +669,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.2.tgz", - "integrity": "sha512-yUoEvnH0FBef/NbB1u6d3HNGyruAKnN74LrPAfDQL3O32e3k3OSfLrPgSJmgb3PJrBZWfPyt6m4ZhAFa2nZp2A==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", + "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", "cpu": [ "x64" ], @@ -681,9 +682,22 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.2.tgz", - "integrity": "sha512-GYbLs5ErswU/Xs7aGXqzc3RrdEjKdmoCrgzhJWyFL0r5fL3qd1NPcDKDowDnmcoSiGJeU68/Vy+OMUluRxPiLQ==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", + "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", + "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", "cpu": [ "arm" ], @@ -694,9 +708,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.2.tgz", - "integrity": "sha512-L1+D8/wqGnKQIlh4Zre9i4R4b4noxzH5DDciyahX4oOz62CphY7WDWqJoQ66zNR4oScLNOqQJfNSIAe/6TPUmQ==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", + "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", "cpu": [ "arm64" ], @@ -707,9 +721,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.2.tgz", - "integrity": "sha512-tK5eoKFkXdz6vjfkSTCupUzCo40xueTOiOO6PeEIadlNBkadH1wNOH8ILCPIl8by/Gmb5AGAeQOFeLev7iZDOA==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", + "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", "cpu": [ "arm64" ], @@ -720,11 +734,11 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.13.2.tgz", - "integrity": "sha512-zvXvAUGGEYi6tYhcDmb9wlOckVbuD+7z3mzInCSTACJ4DQrdSLPNUeDIcAQW39M3q6PDquqLWu7pnO39uSMRzQ==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", + "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", "cpu": [ - "ppc64le" + "ppc64" ], "dev": true, "optional": true, @@ -733,9 +747,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.2.tgz", - "integrity": "sha512-C3GSKvMtdudHCN5HdmAMSRYR2kkhgdOfye4w0xzyii7lebVr4riCgmM6lRiSCnJn2w1Xz7ZZzHKuLrjx5620kw==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", + "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", "cpu": [ "riscv64" ], @@ -746,9 +760,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.13.2.tgz", - "integrity": "sha512-l4U0KDFwzD36j7HdfJ5/TveEQ1fUTjFFQP5qIt9gBqBgu1G8/kCaq5Ok05kd5TG9F8Lltf3MoYsUMw3rNlJ0Yg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", + "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", "cpu": [ "s390x" ], @@ -759,9 +773,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.2.tgz", - "integrity": "sha512-xXMLUAMzrtsvh3cZ448vbXqlUa7ZL8z0MwHp63K2IIID2+DeP5iWIT6g1SN7hg1VxPzqx0xZdiDM9l4n9LRU1A==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", + "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", "cpu": [ "x64" ], @@ -772,9 +786,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.2.tgz", - "integrity": "sha512-M/JYAWickafUijWPai4ehrjzVPKRCyDb1SLuO+ZyPfoXgeCEAlgPkNXewFZx0zcnoIe3ay4UjXIMdXQXOZXWqA==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", + "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", "cpu": [ "x64" ], @@ -785,9 +799,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.2.tgz", - "integrity": "sha512-2YWwoVg9KRkIKaXSh0mz3NmfurpmYoBBTAXA9qt7VXk0Xy12PoOP40EFuau+ajgALbbhi4uTj3tSG3tVseCjuA==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", + "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", "cpu": [ "arm64" ], @@ -798,9 +812,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.2.tgz", - "integrity": "sha512-2FSsE9aQ6OWD20E498NYKEQLneShWes0NGMPQwxWOdws35qQXH+FplabOSP5zEe1pVjurSDOGEVCE2agFwSEsw==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", + "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", "cpu": [ "ia32" ], @@ -811,9 +825,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.2.tgz", - "integrity": "sha512-7h7J2nokcdPePdKykd8wtc8QqqkqxIrUz7MHj6aNr8waBRU//NLDVnNjQnqQO6fqtjrtCdftpbTuOKAyrAQETQ==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", + "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", "cpu": [ "x64" ], @@ -824,17 +838,17 @@ ] }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.0.2.tgz", - "integrity": "sha512-MpmF/cju2HqUls50WyTHQBZUV3ovV/Uk8k66AN2gwHogNAG8wnW8xtZDhzNBsFJJuvmq1qnzA5kE7YfMJNFv2Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.0.tgz", + "integrity": "sha512-sY6ncCvg+O3njnzbZexcVtUqOBE3iYmQPJ9y+yXSkOwG576QI/xJrBnQSRXFLGwJNBa0T78JEKg5cIR0WOAuUw==", "dev": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^2.0.0", "debug": "^4.3.4", "deepmerge": "^4.3.1", "kleur": "^4.1.5", - "magic-string": "^0.30.5", - "svelte-hmr": "^0.15.3", + "magic-string": "^0.30.9", + "svelte-hmr": "^0.16.0", "vitefu": "^0.2.5" }, "engines": { @@ -846,9 +860,9 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.0.0.tgz", - "integrity": "sha512-gjr9ZFg1BSlIpfZ4PRewigrvYmHWbDrq2uvvPB1AmTWKuM+dI1JXQSUu2pIrYLb/QncyiIGkFDFKTwJ0XqQZZg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz", + "integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==", "dev": true, "dependencies": { "debug": "^4.3.4" @@ -940,9 +954,9 @@ } }, "node_modules/apexcharts": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.48.0.tgz", - "integrity": "sha512-Lhpj1Ij6lKlrUke8gf+P+SE6uGUn+Pe1TnCJ+zqrY0YMvbqM3LMb1lY+eybbTczUyk0RmMZomlTa2NgX2EUs4Q==", + "version": "3.49.1", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.49.1.tgz", + "integrity": "sha512-MqGtlq/KQuO8j0BBsUJYlRG8VBctKwYdwuBtajHgHTmSgUU3Oai+8oYN/rKCXwXzrUlYA+GiMgotAIbXY2BCGw==", "dev": true, "dependencies": { "@yr/monotone-cubic-spline": "^1.0.3", @@ -1032,12 +1046,13 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { @@ -1112,9 +1127,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001610", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001610.tgz", - "integrity": "sha512-QFutAY4NgaelojVMjY63o6XlZyORPaLfyMnsl3HgnWdJUcX6K0oaJymHjH8PT5Gk7sTm8rvC/c5COUQKXqmOMA==", + "version": "1.0.30001618", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001618.tgz", + "integrity": "sha512-p407+D1tIkDvsEAPS22lJxLQQaG8OTBEqo0KhzfABGk0TU4juBNDSfH0hyAp/HRyx+M8L17z/ltyhxh27FTfQg==", "dev": true, "funding": [ { @@ -1155,18 +1170,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/code-red": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", @@ -1312,9 +1315,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.739", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.739.tgz", - "integrity": "sha512-koRkawXOuN9w/ymhTNxGfB8ta4MRKVW0nzifU17G1UwTWlBg0vv7xnz4nxDnRFSBe9nXMGRgICcAzqXc0PmLeA==", + "version": "1.4.771", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.771.tgz", + "integrity": "sha512-b/CmBh1c5SXZy5oFu4a0o+2TdM0AYStiwoQebEoImGAINstCsS/s/MOvPKMoxu1nA2BJtEOJI1nC/VoVRzdXWA==", "dev": true }, "node_modules/emoji-regex": { @@ -1400,18 +1403,6 @@ "node": ">=8.6.0" } }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -1444,22 +1435,22 @@ } }, "node_modules/flowbite-svelte": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/flowbite-svelte/-/flowbite-svelte-0.45.0.tgz", - "integrity": "sha512-laYG6CXvQ7huzrqI24jf8mYwBEw3kGesVCpS9WgIg6jUgBPzaYba/R4FPPdKei3LQBRj8V/pzzUERzUPizm7Aw==", + "version": "0.46.5", + "resolved": "https://registry.npmjs.org/flowbite-svelte/-/flowbite-svelte-0.46.5.tgz", + "integrity": "sha512-YSykTuR4AtD9RpACnrg9fac/fDtK+VB3oNggsM2MOmO9d2qr+bYyb7xdoLeSHepcVI330rEusEBZzTvUnwHfEQ==", "dev": true, "dependencies": { - "@floating-ui/dom": "^1.6.3", - "apexcharts": "^3.48.0", + "@floating-ui/dom": "^1.6.5", + "apexcharts": "^3.49.1", "flowbite": "^2.3.0", - "tailwind-merge": "^2.2.2" + "tailwind-merge": "^2.3.0" }, "engines": { - "node": ">=20.0.0", + "node": ">=18.0.0", "pnpm": ">=8.0.0" }, "peerDependencies": { - "svelte": "^4.0.0" + "svelte": "^3.55.1 || ^4.0.0" } }, "node_modules/foreground-child": { @@ -1541,37 +1532,15 @@ } }, "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "is-glob": "^4.0.1" }, "engines": { - "node": "*" + "node": ">= 6" } }, "node_modules/graceful-fs": { @@ -1761,23 +1730,20 @@ "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" }, "node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", "dev": true, "engines": { "node": "14 || >=16.14" } }, "node_modules/magic-string": { - "version": "0.30.8", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", - "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" } }, "node_modules/mdn-data": { @@ -1826,18 +1792,15 @@ } }, "node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" } }, "node_modules/minimist": { @@ -1850,9 +1813,9 @@ } }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz", + "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==", "dev": true, "engines": { "node": ">=16 || 14 >=14.17" @@ -2002,16 +1965,16 @@ "dev": true }, "node_modules/path-scurry": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", - "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2028,9 +1991,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "dev": true }, "node_modules/picomatch": { @@ -2128,9 +2091,9 @@ } }, "node_modules/postcss-load-config": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-5.0.3.tgz", - "integrity": "sha512-90pBBI5apUVruIEdCxZic93Wm+i9fTrp7TXbgdUCH+/L+2WnfpITSpq5dFU/IPvbv7aNiMlQISpUkAm3fEcvgQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-5.1.0.tgz", + "integrity": "sha512-G5AJ+IX0aD0dygOE0yFZQ/huFFMSNneyfp0e3/bT05a8OfPC5FUoZRPfGijUdGOJNMewJiwzcHJXFafFzeKFVA==", "dev": true, "funding": [ { @@ -2143,15 +2106,16 @@ } ], "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" + "lilconfig": "^3.1.1", + "yaml": "^2.4.2" }, "engines": { "node": ">= 18" }, "peerDependencies": { "jiti": ">=1.21.0", - "postcss": ">=8.0.9" + "postcss": ">=8.0.9", + "tsx": "^4.8.1" }, "peerDependenciesMeta": { "jiti": { @@ -2159,6 +2123,9 @@ }, "postcss": { "optional": true + }, + "tsx": { + "optional": true } } }, @@ -2291,10 +2258,22 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/rollup": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.2.tgz", - "integrity": "sha512-MIlLgsdMprDBXC+4hsPgzWUasLO9CE4zOkj/u6j+Z6j5A4zRY+CtiXAdJyPtgCsc42g658Aeh1DlrdVEJhsL2g==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", + "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -2307,21 +2286,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.13.2", - "@rollup/rollup-android-arm64": "4.13.2", - "@rollup/rollup-darwin-arm64": "4.13.2", - "@rollup/rollup-darwin-x64": "4.13.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.13.2", - "@rollup/rollup-linux-arm64-gnu": "4.13.2", - "@rollup/rollup-linux-arm64-musl": "4.13.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.13.2", - "@rollup/rollup-linux-riscv64-gnu": "4.13.2", - "@rollup/rollup-linux-s390x-gnu": "4.13.2", - "@rollup/rollup-linux-x64-gnu": "4.13.2", - "@rollup/rollup-linux-x64-musl": "4.13.2", - "@rollup/rollup-win32-arm64-msvc": "4.13.2", - "@rollup/rollup-win32-ia32-msvc": "4.13.2", - "@rollup/rollup-win32-x64-msvc": "4.13.2", + "@rollup/rollup-android-arm-eabi": "4.17.2", + "@rollup/rollup-android-arm64": "4.17.2", + "@rollup/rollup-darwin-arm64": "4.17.2", + "@rollup/rollup-darwin-x64": "4.17.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", + "@rollup/rollup-linux-arm-musleabihf": "4.17.2", + "@rollup/rollup-linux-arm64-gnu": "4.17.2", + "@rollup/rollup-linux-arm64-musl": "4.17.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", + "@rollup/rollup-linux-riscv64-gnu": "4.17.2", + "@rollup/rollup-linux-s390x-gnu": "4.17.2", + "@rollup/rollup-linux-x64-gnu": "4.17.2", + "@rollup/rollup-linux-x64-musl": "4.17.2", + "@rollup/rollup-win32-arm64-msvc": "4.17.2", + "@rollup/rollup-win32-ia32-msvc": "4.17.2", + "@rollup/rollup-win32-x64-msvc": "4.17.2", "fsevents": "~2.3.2" } }, @@ -2372,18 +2352,6 @@ "rimraf": "^2.5.2" } }, - "node_modules/sander/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2570,21 +2538,45 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/sucrase/node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "version": "10.3.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", + "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.6", "minimatch": "^9.0.1", "minipass": "^7.0.4", - "path-scurry": "^1.10.2" + "path-scurry": "^1.11.0" }, "bin": { "glob": "dist/esm/bin.mjs" }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, "engines": { "node": ">=16 || 14 >=14.17" }, @@ -2605,9 +2597,9 @@ } }, "node_modules/svelte": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.12.tgz", - "integrity": "sha512-d8+wsh5TfPwqVzbm4/HCXC783/KPHV60NvwitJnyTA5lWn1elhXMNWhXGCJ7PwPa8qFUnyJNIyuIRt2mT0WMug==", + "version": "4.2.17", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.17.tgz", + "integrity": "sha512-N7m1YnoXtRf5wya5Gyx3TWuTddI4nAyayyIWFojiWV5IayDYNV5i2mRp/7qNGol4DtxEYxljmrbgp1HM6hUbmQ==", "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", @@ -2629,9 +2621,9 @@ } }, "node_modules/svelte-check": { - "version": "3.6.8", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.8.tgz", - "integrity": "sha512-rhXU7YCDtL+lq2gCqfJDXKTxJfSsCgcd08d7VWBFxTw6IWIbMWSaASbAOD3N0VV9TYSSLUqEBiratLd8WxAJJA==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.7.1.tgz", + "integrity": "sha512-U4uJoLCzmz2o2U33c7mPDJNhRYX/DNFV11XTUDlFxaKLsO7P+40gvJHMPpoRfa24jqZfST4/G9fGNcUGMO8NAQ==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", @@ -2651,9 +2643,9 @@ } }, "node_modules/svelte-hmr": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", - "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz", + "integrity": "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==", "dev": true, "engines": { "node": "^12.20 || ^14.13.1 || >= 16" @@ -2663,9 +2655,9 @@ } }, "node_modules/svelte-preprocess": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz", - "integrity": "sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz", + "integrity": "sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -2676,8 +2668,7 @@ "strip-indent": "^3.0.0" }, "engines": { - "node": ">= 16.0.0", - "pnpm": "^8.0.0" + "node": ">= 16.0.0" }, "peerDependencies": { "@babel/core": "^7.10.2", @@ -2725,6 +2716,14 @@ } } }, + "node_modules/svelte-sonner": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.24.tgz", + "integrity": "sha512-txuL0JBUs0v6qGrr0PGCsbXmKHuthdrAkfISYi8umuveF7+gINb6EXl6VmKY9aHhyxCqvVgqd6yophQNrnor4w==", + "peerDependencies": { + "svelte": ">=3 <5" + } + }, "node_modules/svelte-spa-router": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-4.0.1.tgz", @@ -2828,12 +2827,12 @@ } }, "node_modules/tailwind-merge": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.2.tgz", - "integrity": "sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.3.0.tgz", + "integrity": "sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==", "dev": true, "dependencies": { - "@babel/runtime": "^7.24.0" + "@babel/runtime": "^7.24.1" }, "funding": { "type": "github", @@ -2877,6 +2876,18 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/tailwindcss/node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -2979,9 +2990,9 @@ "dev": true }, "node_modules/typescript": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", - "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -2992,9 +3003,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", "dev": true, "funding": [ { @@ -3011,8 +3022,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -3028,13 +3039,13 @@ "dev": true }, "node_modules/vite": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz", - "integrity": "sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==", + "version": "5.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", + "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", "dev": true, "dependencies": { "esbuild": "^0.20.1", - "postcss": "^8.4.36", + "postcss": "^8.4.38", "rollup": "^4.13.0" }, "bin": { @@ -3209,9 +3220,9 @@ "dev": true }, "node_modules/yaml": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", - "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", + "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", "dev": true, "bin": { "yaml": "bin.mjs" diff --git a/html/package.json b/html/package.json index b2da96c..d4bf808 100644 --- a/html/package.json +++ b/html/package.json @@ -16,7 +16,7 @@ "@tsconfig/svelte": "^5.0.2", "autoprefixer": "^10.4.16", "flowbite": "^2.3.0", - "flowbite-svelte": "^0.45.0", + "flowbite-svelte": "^0.46.5", "postcss": "^8.4.32", "postcss-load-config": "^5.0.2", "svelte": "^4.2.12", @@ -30,6 +30,7 @@ "@fortawesome/free-brands-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/svelte-fontawesome": "^0.2.2", + "svelte-sonner": "^0.3.24", "svelte-spa-router": "^4.0.1" } } diff --git a/html/src/App.svelte b/html/src/App.svelte index 24e809e..88cef65 100644 --- a/html/src/App.svelte +++ b/html/src/App.svelte @@ -1,8 +1,10 @@
- + +
\ No newline at end of file diff --git a/html/src/app.pcss b/html/src/app.pcss index c41f784..65c368a 100644 --- a/html/src/app.pcss +++ b/html/src/app.pcss @@ -29,25 +29,34 @@ a:hover { body { margin: 0; display: flex; - place-items: center; min-width: 320px; min-height: 100vh; } +h1, h2, h3, h4, h5 { + font-weight: bold; +} + h1 { font-size: 3.2em; line-height: 1.1; } +h2 { + font-size: 2.0em; +} + +h3 { + font-size: 1.5em; +} + .card { padding: 2em; } #app { - max-width: 1280px; + width: 100%; margin: 0 auto; - padding: 2rem; - text-align: center; } button { diff --git a/html/src/lib/components/Banner.svelte b/html/src/lib/components/Banner.svelte new file mode 100644 index 0000000..baf7251 --- /dev/null +++ b/html/src/lib/components/Banner.svelte @@ -0,0 +1,56 @@ + + + + + + \ No newline at end of file diff --git a/html/src/lib/components/BtnCreateTeam.svelte b/html/src/lib/components/BtnCreateTeam.svelte new file mode 100644 index 0000000..b1a3443 --- /dev/null +++ b/html/src/lib/components/BtnCreateTeam.svelte @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/html/src/lib/components/Card.svelte b/html/src/lib/components/Card.svelte new file mode 100644 index 0000000..a85ec6e --- /dev/null +++ b/html/src/lib/components/Card.svelte @@ -0,0 +1,14 @@ + + +
+ this is a card +
\ No newline at end of file diff --git a/html/src/lib/components/CreateTeamForm.svelte b/html/src/lib/components/CreateTeamForm.svelte new file mode 100644 index 0000000..e5d4e8a --- /dev/null +++ b/html/src/lib/components/CreateTeamForm.svelte @@ -0,0 +1,130 @@ + + + + + {#if formData !== null} +
+
+ + + + +
+ Public Team + (If you want your team to be searchable.) +
+
+ Private Team + (Your team will be invite only) +
+ + + + + + + + + + + + + + +
+
+ {/if} + + diff --git a/html/src/lib/pages/ProfilePage.svelte b/html/src/lib/pages/ProfilePage.svelte new file mode 100644 index 0000000..02a66f9 --- /dev/null +++ b/html/src/lib/pages/ProfilePage.svelte @@ -0,0 +1,77 @@ + + + + + +

Edit Profile

+ {#await $activeUserStore} + {:then activeUser} + {#if formData !== null} + {#if activeUser !== null} +
+ + {#if activeUser.user?.LockDisplayName} +
+ Display Name + Your Display Name has been locked by an admin and may not be changed +
+
{formData.DisplayName}
+ {:else} + + + + {/if} + +
+ + + {/if} + {/if} + {/await} +
+ + +
diff --git a/html/src/lib/pages/TeamEdit.svelte b/html/src/lib/pages/TeamEdit.svelte new file mode 100644 index 0000000..c8f9e93 --- /dev/null +++ b/html/src/lib/pages/TeamEdit.svelte @@ -0,0 +1,292 @@ + + + + + Home + Team Options + Edit Team + + +
+ {#if loading} +
Loading...
+ {:else if error} +
{error}
+ {:else if formData !== null} +
+
+ + + +
+ Public Team + (If you want your team to be searchable.) +
+
+ Private Team + (Your team will be invite only) +
+ + + + + + + + + + + + +
+

Team Members

+ + + Avatar + Username + Role + + Delete + + + + {#each teamTeamMembers as Member} + + + + + {Member.DisplayName} + {Member.TeamRole} + + {#if Member.TeamRole !== 'owner'} + + {/if} + + + {/each} + +
+ {:else} + + {/if} +
+
diff --git a/html/src/lib/pages/TeamOptions.svelte b/html/src/lib/pages/TeamOptions.svelte new file mode 100644 index 0000000..8830d9b --- /dev/null +++ b/html/src/lib/pages/TeamOptions.svelte @@ -0,0 +1,42 @@ + + + + + Home + Team Options + + +

Team Options

+
+ + +

Find a Team to Join

+

Browse other teams looking for members like you!

+

+
+ +

Start a Team

+

+ By creating a team, you're in control of the team page. Decide whether anyone can join or + just people with the invite code. +

+

+
+ + +

Be a Solo Participant

+

For those who want to work alone. You can change your mind later.

+

+
+
+
diff --git a/html/src/lib/pages/TeamsBrowse.svelte b/html/src/lib/pages/TeamsBrowse.svelte new file mode 100644 index 0000000..85d7030 --- /dev/null +++ b/html/src/lib/pages/TeamsBrowse.svelte @@ -0,0 +1,192 @@ + + + + +

Browse All Teams

+ + {#if loading} +
Loading...
+ {:else if error} +
{error}
+ {:else} + {#each publicTeams as Team} + +
+

Team {Team.Name}

+
+ + Owner: {getTeamOwner(Team.TeamMembers)} + + + Members: +
+ {#each Team.TeamMembers as Member} + + {/each} +
+
+ + + Visibility: {Team.Visibility} + + + Technologies: {Team.Technologies} + + + Availability: {Team.Availability} + + + Description: {Team.Description} + + + + {#if !$loggedInStore} + + +

+ Login with Discord to join a team! +

+
+ {:else if $loggedInStore} + {#if !isUserInTeam(Team.TeamMembers)} + + {:else if isUserInTeam(Team.TeamMembers)} + + {/if} + {/if} +
+ {:else} +
+ There's no existing teams. Be the first to create one! +
+ {/each} + {/if} +
+
diff --git a/html/src/lib/pages/TeamsCreate.svelte b/html/src/lib/pages/TeamsCreate.svelte new file mode 100644 index 0000000..9db4a9a --- /dev/null +++ b/html/src/lib/pages/TeamsCreate.svelte @@ -0,0 +1,30 @@ + + + + + Home + Team Options + Create Team + + +

Create your team!

+
+
+ +
+ + + +
+ \ No newline at end of file diff --git a/html/src/lib/pages/UserTeams.svelte b/html/src/lib/pages/UserTeams.svelte new file mode 100644 index 0000000..567bdf3 --- /dev/null +++ b/html/src/lib/pages/UserTeams.svelte @@ -0,0 +1,142 @@ + + + + + Home + My Teams + + + +

Teams You Own

+ + {#if loading} +
Loading...
+ {:else if error} +
{error}
+ {:else if userTeams === null} +
You currently don't have any teams!
+ {:else if userTeams.length === 0} +
+ Looks like you don't have any teams. Go to browse teams to + join one or create your own team! +
+ {:else} + {#each userTeams as userTeam} + + {#if getTeamOwner(userTeam.TeamMembers) == $userStore?.DisplayName} +
+

{userTeam.Name}

+ Edit your team +
+ {:else} +
+

Team {userTeam.Name}

+
+ {/if} + + Owner: {getTeamOwner(userTeam.TeamMembers)} + + + Members: +
+ {#each userTeam.TeamMembers as Member} + + {/each} +
+
+ + + Visibility: {userTeam.Visibility} + + + Technologies: {userTeam.Technologies} + + + Availability: {userTeam.Availability} + + + Description: {userTeam.Description} + +
+ {/each} + {/if} +
+
diff --git a/html/src/lib/pages/admin/EventEdit.svelte b/html/src/lib/pages/admin/EventEdit.svelte new file mode 100644 index 0000000..16d3455 --- /dev/null +++ b/html/src/lib/pages/admin/EventEdit.svelte @@ -0,0 +1,140 @@ + + + + + Home + Manage Events + Edit Event + + + + +

Edit Event

+ {#if formData !== null} +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + {:else} + + {/if} +
+
diff --git a/html/src/lib/pages/admin/EventList.svelte b/html/src/lib/pages/admin/EventList.svelte new file mode 100644 index 0000000..7b8a540 --- /dev/null +++ b/html/src/lib/pages/admin/EventList.svelte @@ -0,0 +1,71 @@ + + + + + +

Events

+
+
+ + {#each events as event} + +
+
+ {event.Title} + {getEventStatus(event.StatusId)} +
+ +
+
+ {/each} + +
+ +
\ No newline at end of file diff --git a/html/src/lib/pages/admin/UserList.svelte b/html/src/lib/pages/admin/UserList.svelte new file mode 100644 index 0000000..fad0426 --- /dev/null +++ b/html/src/lib/pages/admin/UserList.svelte @@ -0,0 +1,208 @@ + + + + + {modalUser?.DisplayName} + + +
+
Discord Username
+
{modalUser.ServiceUserName}
+ +
Display Name
+
+ {modalUser.DisplayName} + {#if modalUser.Role !== "ADMIN"} + {#if modalUser.DisplayName !== modalUser.ServiceUserName} + + Reset Display Name to Discord Username + {/if} + {#if modalUser.LockDisplayName === true} + + User NOT allowed to modify Display Name. + {:else} + + User is allowed to modify Display Name. + {/if} + {/if} +
+ +
Role
+
{modalUser.Role}
+ +
Status
+
{modalUser.AccountStatus}
+
+ + + {#if modalUser.Role !== "ADMIN"} + {#if modalUser.AccountStatus === "ACTIVE" } + + {:else} + + {/if} + {/if} +
+ +
+
+
+ + + +

Users

+
+
+ + + + {displayedUsers.length} Users + + + + Display Name + Discord Name + Discord ID + Role + Status + + + {#each displayedUsers as user} + openUserModal(user)}> + + + {user.DisplayName} + {#if user.LockDisplayName === true} + + {/if} + + {user.ServiceUserName} + {user.ServiceUserId} + {user.Role} + {user.AccountStatus} + + {/each} + + +
+ +
\ No newline at end of file diff --git a/html/src/lib/services/adminServices.ts b/html/src/lib/services/adminServices.ts new file mode 100644 index 0000000..a12036f --- /dev/null +++ b/html/src/lib/services/adminServices.ts @@ -0,0 +1,71 @@ +import {baseApiUrl} from "./services"; +import type {User} from "../models/user"; + + +export async function getAllUsers() { + return await fetch(baseApiUrl + "/admin/user/all"); +} + + +interface PutAccountStatusRequest { + AccountStatus: string, +} + +export async function putAccountStatus(userId: string, status: string) : Promise { + const requestInit : RequestInit = { + method: 'PUT', + body: JSON.stringify( + { + AccountStatus: status + } + ) + } + const response: Response = await fetch(`${baseApiUrl}/admin/user/${userId}/account_status/`, requestInit); + return await response.json() as User; +} + +export async function BanUser(userId: string) :Promise { + const response : Response = await fetch(`${baseApiUrl}/admin/user/${userId}/ban`, { method: 'PUT' }); + return await response.json() as User; +} + +export async function UnbanUser(userId: string) : Promise { + const response : Response = await fetch(`${baseApiUrl}/admin/user/${userId}/unban`, { method: 'PUT' }); + return await response.json() as User; +} + + +interface PutDisplayNameLockRequest { + Lock: boolean, +} + +export async function updateDisplayNameLock(userId: string, lock: boolean) { + const requestInit : RequestInit = { + method: 'PUT', + body: JSON.stringify( + { + Lock: lock + } + ) + } + const response: Response = await fetch(`${baseApiUrl}/admin/user/${userId}/display_name_lock`, requestInit); + return await response.json() as User; +} + + +interface PutDisplayNameRequest { + DisplayName: string, +} + +export async function UpdateDisplayName(userId: string, displayName: string) { + const requestInit : RequestInit = { + method: 'PUT', + body: JSON.stringify( + { + DisplayName: displayName + } + ) + } + const response : Response = await fetch(`${baseApiUrl}/admin/user/${userId}/display_name`, requestInit); + return await response.json() as User; +} \ No newline at end of file diff --git a/html/src/lib/services/services.ts b/html/src/lib/services/services.ts index 245d81d..2ac331a 100644 --- a/html/src/lib/services/services.ts +++ b/html/src/lib/services/services.ts @@ -1,8 +1,15 @@ -import {readable} from "svelte/store"; -import {userStore} from "../stores/stores"; +import { + activeEventStore, + eventStatusStore, + userStore, + activeUserStore +} from "../stores/stores"; +import CodeJamEvent from "../models/event"; +import CodeJamTeam from "../models/team"; +import type { ActiveUser, User } from "../models/user"; // This shouldn't ever need to be set since dev and prod environments will just use relative endpoints -export let baseApiUrl : string = ""; +export let baseApiUrl: string = ""; const originalFetch = window.fetch; @@ -21,14 +28,16 @@ window.fetch = (...args) => { } export async function getUser() { - return fetch(baseApiUrl + "/user/", {method: 'GET', credentials: 'include'}) + return fetch(baseApiUrl + "/user/", { method: 'GET', credentials: 'include' }) .then((response) => { if (response.status === 401) { userStore.set(null); + activeUserStore.set({ user: null, loggedIn: false }); } else { response.json() .then((data) => { userStore.set(data); + activeUserStore.set({ user: data as User, loggedIn: true }) }) .catch((err) => { console.error("error deserializing user", err); @@ -37,15 +46,189 @@ export async function getUser() { }); } +interface PutProfileRequest { + DisplayName : string, +} +export async function putProfile(displayName: string): Promise { + const requestInit : RequestInit = { + method: 'PUT', + body: JSON.stringify( + { + DisplayName: displayName + } + ) + } + return await fetch(`${baseApiUrl}/user/profile/`, requestInit); +} + export async function logout() { return fetch(baseApiUrl + "/user/logout") - .then((response) => { + .then(() => { userStore.set(null); + activeUserStore.set({ user: null, loggedIn: false }); }) .catch((err) => { console.error("Logout error", err); }); } +export async function getActiveEvent() { + return fetch(baseApiUrl + "/event/active") + .then((response) => { + if (response.status === 401) { + userStore.set(null); + } else if (response.status === 204) { + activeEventStore.set(null); + } else { + response.json() + .then((data) => { + activeEventStore.set(data as CodeJamEvent); + }) + .catch((err) => { + console.error("error deserializing event", response, err); + }); + } + }); +} + +export async function getEvents() { + return fetch(baseApiUrl + "/event/"); +} + +export async function getEvent(id: string) { + return fetch(baseApiUrl + "/event/" + id); +} + +export async function putEvent(event: CodeJamEvent) { + return await fetch(baseApiUrl + "/event/" + event.Id, + { + method: "PUT", + body: JSON.stringify(event) + }); +} + +export async function getEventStatuses() { + return fetch(baseApiUrl + "/event/statuses") + .then((response) => { + response.json() + .then((data) => { + eventStatusStore.set(data) + }); + }) +} + +export async function postTeam(team: CodeJamTeam) { + // Step 2: Post Team Data API (accepts formData(CodeJamTeam) as argument) + return await fetch(baseApiUrl + "/team/" + team.Id, + // formData turned into JSON and sent via POST, retreived at CreateTeam(ctx *gin.Context) + { + method: "POST", + body: JSON.stringify(team) + }); +} + +export async function putTeam(team: CodeJamTeam) { + console.log("baseApiUrl:", baseApiUrl); + console.log("team.Id before URL construction:", team.Id); + + if (team.Id == undefined) { + console.log("team.Id is undefined:", team.Id); + } else { + console.log("team.Id is:", team.Id); + } + let teamid: string = team.Id + // Construct the URL using template literals and encodeURIComponent for safety + const url = `${baseApiUrl}/team/edit/${teamid}`; + console.log("Constructed PUT request URL:", url); + + const response = await fetch(url, { + method: "PUT", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(team) + }); + return response +} + +export async function getTeams() { + return await fetch(baseApiUrl + "/teams/browse") +} + + +export async function getUserTeams() { + return fetch(baseApiUrl + "/teams"); +} + +export async function getTeamById(id: string) { + return fetch(baseApiUrl + "/team/" + id); +} + +export async function getTeamByInvite(inviteCode: string) { + // stepp 3 pt 1: + return fetch(baseApiUrl + "/team/invite/" + inviteCode); +} + + +// any one who has a link joinTeam() works on, can join the team. +// make sure invite_code matches + +export async function joinTeam(teamId: string, inviteCode: string) { + // making a post to team_members + return await fetch(baseApiUrl + "/team/" + inviteCode, + { + method: "POST", + body: JSON.stringify({ teamId, inviteCode }) + } + ) +} + + +// write a joinPublicTeam function that accepts team Id. +// server: check if team is public. +export async function joinPublicTeam(teamId: string) { + const response = await fetch(baseApiUrl + "/team/join", + { + method: "POST", + body: JSON.stringify({ teamId }) + } + ) + return response.json() +} + +export async function removeMemberFromTeam(teamId: string, memberId: string) { + // DELETE request to remove a member from a specific team + + const response = await fetch(baseApiUrl + `/team/${teamId}/member/${memberId}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ teamId, memberId }) + }); + + // Check if response status is OK (successful deletion) + if (response.ok) { + // Log the full response for debugging purposes + console.log("Full response:", response); + + // Check if the response has JSON content + const contentType = response.headers.get("content-type"); + if (contentType && contentType.includes("application/json")) { + const jsonResponse = await response.json(); + console.log("JSON Response:", jsonResponse); // Log the JSON response + return jsonResponse; + } + + console.log("Response has no JSON content, returning empty object."); + return {}; // Return an empty object if no JSON response is provided + } else { + throw new Error(`Failed to delete member: ${response.status} ${response.statusText}`); + } +} + // Always call at startup to get the initial states -getUser(); \ No newline at end of file +async function initialLoad() { + await Promise.all([getUser(), getActiveEvent(), getEventStatuses()]); +} +initialLoad(); \ No newline at end of file diff --git a/html/src/lib/stores/stores.ts b/html/src/lib/stores/stores.ts index 23279bd..e13b65c 100644 --- a/html/src/lib/stores/stores.ts +++ b/html/src/lib/stores/stores.ts @@ -1,5 +1,28 @@ import {writable, derived} from 'svelte/store'; +import CodeJamEvent from "../models/event"; +import {type ActiveUser, type User} from "../models/user"; -export const userStore = writable(null); -export const loggedInStore = derived(userStore, (userData) => userData != null); \ No newline at end of file +export const activeContent = writable(''); + +/** + * Deprecated - use activeUserStore.user instead + */ + +export const userStore = writable(null); + +/** + * Deprecated - use activeUserStore.loggedIn instead + */ + +export const loggedInStore = derived(userStore, (userData) => userData != null); + +/** + * activeUserStore will allow components to use a single object / store to handle loggedIn logic as well as User + * data handling. This solves a problem with being able to correctly initialize component states based on logged-in and + * User values without causing flickering due to DOM updates. + */ +export const activeUserStore = writable(null); + +export const activeEventStore = writable(null); +export const eventStatusStore = writable(null); diff --git a/html/src/main.ts b/html/src/main.ts index a7eb4fb..8ddc1e3 100644 --- a/html/src/main.ts +++ b/html/src/main.ts @@ -3,7 +3,7 @@ import App from './App.svelte' import "./lib/services/services" const app = new App({ - target: document.getElementById('app')!, + target: document.getElementById('app')!, }) export default app diff --git a/html/src/routes.ts b/html/src/routes.ts index e3e175a..5090528 100644 --- a/html/src/routes.ts +++ b/html/src/routes.ts @@ -1,5 +1,28 @@ import HomePage from "./lib/pages/HomePage.svelte"; +import EventEdit from "./lib/pages/admin/EventEdit.svelte"; +import EventList from "./lib/pages/admin/EventList.svelte"; +import TeamOptions from "./lib/pages/TeamOptions.svelte"; +import TeamsBrowse from "./lib/pages/TeamsBrowse.svelte"; +import TeamsCreate from "./lib/pages/TeamsCreate.svelte"; +import MyTeam from "./lib/pages/MyTeam.svelte"; +import Invite from "./lib/pages/Invite.svelte"; +import UserTeams from "./lib/pages/UserTeams.svelte"; +import ProfilePage from "./lib/pages/ProfilePage.svelte"; +import UserList from "./lib/pages/admin/UserList.svelte"; +import TeamEdit from "./lib/pages/TeamEdit.svelte"; export default { '/': HomePage, + '/home': HomePage, + '/team': TeamOptions, + '/team/:id': MyTeam, // link to one of your teams (sharable) We get an id here in this route... + '/team/invite/:invitecode': Invite, // sharable + '/teams': UserTeams, // displays all the user's teams (private) + '/teams/browse': TeamsBrowse, + '/teams/create': TeamsCreate, + '/profile': ProfilePage, + '/team/edit/:id': TeamEdit, + '/admin/events': EventList, + '/admin/event/:id': EventEdit, + '/admin/users': UserList, } \ No newline at end of file diff --git a/html/src/styles/styles.css b/html/src/styles/styles.css new file mode 100644 index 0000000..0d7d1c5 --- /dev/null +++ b/html/src/styles/styles.css @@ -0,0 +1,156 @@ +/* +1. Use a more-intuitive box-sizing model. +*/ +*, *::before, *::after { +box-sizing: border-box; +scroll-behavior: smooth; +} +/* +2. Remove default margin +*/ +* { +margin: 0; +} +/* +Typographic tweaks! +3. Add accessible line-height +4. Improve text rendering +*/ +body { +line-height: 1.5; +-webkit-font-smoothing: antialiased; +} +/* +5. Improve media defaults +*/ +img, picture, video, canvas, svg { +display: block; +max-width: 100%; +} +/* +6. Remove built-in form typography styles +*/ +input, button, textarea, select { +font: inherit; +} +/* +7. Avoid text overflows +*/ +p, h1, h2, h3, h4, h5, h6 { +overflow-wrap: break-word; +} +/* +8. Create a root stacking context +*/ +#root, #__next { +isolation: isolate; +} +* { + outline: 0px solid red; +} +.fira-code-nav { + font-family: "Fira Code", monospace; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; +} +:root { + --primary-color: white; + --background-color: #3a4080; + --background-gradient: pink; + --card-color: #ece8d8cd; + --card-font-color: #2a2e5d; + --gradient-background: linear-gradient(90deg, rgb(203, 164, 122) 0%, rgb(170, 121, 154) 100%); +} +body { + color: white; + font-size: 1rem; + font-family: "Fira Code", monospace; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + color: var(--primary-color); + background: var(--gradient-background); + /* background: rgb(190,138,110); + background: linear-gradient(90deg, rgb(203, 164, 122) 0%, rgb(170, 121, 154) 100%); */ +} +a { + /*color: white;*/ + text-decoration: None; + +} +a:hover { + color: black; +} +.flex { + display: flex; +} +.row { + flex-direction: row; +} +.col { + flex-direction: column; +} +.grow-1 { + flex-grow: 1; +} +.sp-around { + justify-content: space-around; +} +.sp-between { + align-content: space-between; +} +.card { + border-radius: 15px; + background-color: var(--card-color); + margin-bottom: 2rem; +} +.card { + padding: 3rem 5rem; + color: black; + border-radius: 50px; + margin-left: 3rem; + margin-right: 3rem; + font-size: 1rem; + text-align: left; + border-radius: 50px; + background: #e0e0e0; + box-shadow: -5px 5px 20 #7d7d7d, + 5px -5px 20 #ffffff; +} +.banner { + position: fixed; + z-index: 1; + display: flex; + flex-direction: row; + gap: 1em; + padding-left: 1em; + width: calc(100% - 6em); + height: 3.5em; +} +.banner-top { + font-size: 2em; + font-family: 'Liu Jian Mao Cao', cursive; + text-align: center; +} +.banner-bottom { + margin-top: -2rem; + font-size: 3em; + text-wrap: none; + text-align: center; +} +.banner-bottom { + text-shadow: + 0 0 0.05em pink, /* Horizontal offset, vertical offset, blur radius, color */ + 0.05em 0 0.05em pink, + -0.05em 0 0.05em pink, + 0 0.05em 0.05em rgb(161, 107, 222), + 0 -0.05em 0.05em pink, + 0.05em 0.05em 0.05em rgb(55, 149, 193), + -0.05em -0.05em 0.05em pink, + 0.05em -0.05em 0.05em pink, + -0.05em 0.05em 0.05em pink; +} +.center { + justify-content: center; +} diff --git a/html/static/pinktoasttransparent.png b/html/static/pinktoasttransparent.png new file mode 100644 index 0000000..8bae00a Binary files /dev/null and b/html/static/pinktoasttransparent.png differ diff --git a/html/static/pinktoasttransparent.webp b/html/static/pinktoasttransparent.webp new file mode 100644 index 0000000..224fe87 Binary files /dev/null and b/html/static/pinktoasttransparent.webp differ diff --git a/html/static/toast1.png b/html/static/toast1.png new file mode 100644 index 0000000..dae199a Binary files /dev/null and b/html/static/toast1.png differ diff --git a/html/tailwind.config.cjs b/html/tailwind.config.cjs index 3b4e2a3..385f31e 100644 --- a/html/tailwind.config.cjs +++ b/html/tailwind.config.cjs @@ -10,17 +10,18 @@ const config = { colors: { // flowbite-svelte primary: { - 50: '#FFF5F2', - 100: '#FFF1EE', - 200: '#FFE4DE', - 300: '#FFD5CC', - 400: '#FFBCAD', - 500: '#FE795D', - 600: '#EF562F', - 700: '#EB4F27', - 800: '#CC4522', - 900: '#A5371B' - } + "50":"#f0f9ff", + "100":"#e0f2fe", + "200":"#bae6fd", + "300":"#7dd3fc", + "400":"#38bdf8", + "500":"#0ea5e9", + "600":"#0284c7", + "700":"#0369a1", + "800":"#075985", + "900":"#0c4a6e" + } + } } } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a4d9579 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,250 @@ +{ + "name": "Codejam2024", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "svelte-french-toast": "^1.2.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "peer": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "peer": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "peer": true + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/code-red": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", + "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1", + "acorn": "^8.10.0", + "estree-walker": "^3.0.3", + "periscopic": "^3.1.0" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "peer": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "peer": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "peer": true + }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "peer": true + }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svelte": { + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", + "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/estree": "^1.0.1", + "acorn": "^8.9.0", + "aria-query": "^5.3.0", + "axobject-query": "^4.0.0", + "code-red": "^1.0.3", + "css-tree": "^2.3.1", + "estree-walker": "^3.0.3", + "is-reference": "^3.0.1", + "locate-character": "^3.0.0", + "magic-string": "^0.30.4", + "periscopic": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/svelte-french-toast": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/svelte-french-toast/-/svelte-french-toast-1.2.0.tgz", + "integrity": "sha512-5PW+6RFX3xQPbR44CngYAP1Sd9oCq9P2FOox4FZffzJuZI2mHOB7q5gJBVnOiLF5y3moVGZ7u2bYt7+yPAgcEQ==", + "dependencies": { + "svelte-writable-derived": "^3.1.0" + }, + "peerDependencies": { + "svelte": "^3.57.0 || ^4.0.0" + } + }, + "node_modules/svelte-writable-derived": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.1.tgz", + "integrity": "sha512-w4LR6/bYZEuCs7SGr+M54oipk/UQKtiMadyOhW0PTwAtJ/Ai12QS77sLngEcfBx2q4H8ZBQucc9ktSA5sUGZWw==", + "funding": { + "url": "https://ko-fi.com/pixievoltno1" + }, + "peerDependencies": { + "svelte": "^3.2.1 || ^4.0.0-next.1 || ^5.0.0-next.94" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6b62c9f --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "svelte-french-toast": "^1.2.0" + } +}