update
@ -0,0 +1,16 @@
|
|||||||
|
FROM golang:1.18-alpine AS build_base
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apk add git
|
||||||
|
RUN apk add build-base
|
||||||
|
|
||||||
|
COPY go.mod .
|
||||||
|
COPY go.sum .
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=1 GOOS=linux go build -o gw ./cmd/gw
|
||||||
|
|
||||||
|
FROM alpine:3.9
|
||||||
|
COPY --from=build_base /app/gw /app/gw
|
||||||
|
|
||||||
|
CMD ["/app/gw"]
|
@ -0,0 +1,17 @@
|
|||||||
|
FROM golang:1.18-alpine AS build_base
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apk add git
|
||||||
|
RUN apk add build-base
|
||||||
|
|
||||||
|
COPY go.mod .
|
||||||
|
COPY go.sum .
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=1 GOOS=linux go build -o srv ./cmd/srv
|
||||||
|
|
||||||
|
FROM alpine:3.9
|
||||||
|
COPY --from=build_base /app/srv /app/srv
|
||||||
|
COPY client.secret.json /app
|
||||||
|
|
||||||
|
CMD ["/app/srv", "-secret", "/app/client.secret.json"]
|
@ -0,0 +1,13 @@
|
|||||||
|
FROM node:17-alpine
|
||||||
|
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
|
COPY . /cq
|
||||||
|
|
||||||
|
WORKDIR /cq
|
||||||
|
|
||||||
|
RUN pnpm install
|
||||||
|
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
CMD ["sh", "-c", "node build"]
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,67 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CooldownEntry struct {
|
||||||
|
UserID string
|
||||||
|
QuestionID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cooldowns struct {
|
||||||
|
m map[CooldownEntry]time.Time
|
||||||
|
mu *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCooldowns() Cooldowns {
|
||||||
|
c := make(map[CooldownEntry]time.Time)
|
||||||
|
mu := &sync.Mutex{}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
mu.Lock()
|
||||||
|
now := time.Now()
|
||||||
|
for k, v := range c {
|
||||||
|
if now.After(v) {
|
||||||
|
delete(c, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
time.Sleep(time.Minute * 5)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return Cooldowns{c, mu}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Cooldowns) NewAttempt(userID string, questionID string, attempts int) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
var timeout time.Duration
|
||||||
|
switch attempts {
|
||||||
|
case 0:
|
||||||
|
timeout = time.Minute
|
||||||
|
case 1:
|
||||||
|
timeout = time.Minute * 3 / 2 // 90 seconds
|
||||||
|
case 2:
|
||||||
|
timeout = time.Minute * 3
|
||||||
|
default:
|
||||||
|
timeout = time.Minute * 5
|
||||||
|
}
|
||||||
|
|
||||||
|
c.m[CooldownEntry{userID, questionID}] = time.Now().Add(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Cooldowns) Check(userID string, questionID string) time.Duration {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
expires, ok := c.m[CooldownEntry{userID, questionID}]
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if !ok || now.After(expires) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return expires.Sub(now)
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// Code generated by xo. DO NOT EDIT.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LeaderboardEntry represents a row from 'leaderboard_entry'.
|
||||||
|
type LeaderboardEntry struct {
|
||||||
|
Name string `json:"name"` // name
|
||||||
|
Points int `json:"points"` // points
|
||||||
|
LastValid Time `json:"last_valid"` // last_valid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leaderboard runs a custom query, returning results as LeaderboardEntry.
|
||||||
|
func Leaderboard(ctx context.Context, db DB) ([]*LeaderboardEntry, error) {
|
||||||
|
// query
|
||||||
|
const sqlstr = `SELECT u.name, ` +
|
||||||
|
`COALESCE(SUM(qa.points_awarded), 0) AS points, ` +
|
||||||
|
`COALESCE(MAX(qa.submitted_at), u.created_at) as last_valid_submission ` +
|
||||||
|
`FROM users u ` +
|
||||||
|
`LEFT JOIN question_attempt qa ON u.id = qa.user_id AND qa.correct ` +
|
||||||
|
`GROUP BY u.id ` +
|
||||||
|
`ORDER BY COALESCE(SUM(qa.points_awarded), 0) DESC, ` +
|
||||||
|
`MAX(qa.submitted_at) ASC`
|
||||||
|
// run
|
||||||
|
logf(sqlstr)
|
||||||
|
rows, err := db.QueryContext(ctx, sqlstr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, logerror(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
// load results
|
||||||
|
var res []*LeaderboardEntry
|
||||||
|
for rows.Next() {
|
||||||
|
var le LeaderboardEntry
|
||||||
|
// scan
|
||||||
|
if err := rows.Scan(&le.Name, &le.Points, &le.LastValid); err != nil {
|
||||||
|
return nil, logerror(err)
|
||||||
|
}
|
||||||
|
res = append(res, &le)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, logerror(err)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// Code generated by xo. DO NOT EDIT.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Points represents a row from 'points'.
|
||||||
|
type Points struct {
|
||||||
|
Points int `json:"points"` // points
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserPoints runs a custom query, returning results as Points.
|
||||||
|
func UserPoints(ctx context.Context, db DB, user_id string) (*Points, error) {
|
||||||
|
// query
|
||||||
|
const sqlstr = `SELECT COALESCE(SUM(points_awarded), 0) FROM question_attempt ` +
|
||||||
|
`WHERE user_id=$1`
|
||||||
|
// run
|
||||||
|
logf(sqlstr, user_id)
|
||||||
|
var p Points
|
||||||
|
if err := db.QueryRowContext(ctx, sqlstr, user_id).Scan(&p.Points); err != nil {
|
||||||
|
return nil, logerror(err)
|
||||||
|
}
|
||||||
|
return &p, nil
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package q03
|
||||||
|
|
||||||
|
func solveP1(nums [][]int) (sum int) {
|
||||||
|
for _, row := range nums {
|
||||||
|
for _, n := range row {
|
||||||
|
sum += n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package q03
|
||||||
|
|
||||||
|
func solveP2(nums [][]int) int {
|
||||||
|
var maxR, maxC int
|
||||||
|
for _, ns := range nums {
|
||||||
|
var sum int
|
||||||
|
for _, n := range ns {
|
||||||
|
sum += n
|
||||||
|
}
|
||||||
|
if sum > maxR {
|
||||||
|
maxR = sum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range nums[0] {
|
||||||
|
var sum int
|
||||||
|
for j := range nums {
|
||||||
|
sum += nums[j][i]
|
||||||
|
}
|
||||||
|
if sum > maxC {
|
||||||
|
maxC = sum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxR * maxC
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
package q03
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/hhhapz/codequest/models"
|
||||||
|
"github.com/hhhapz/codequest/question"
|
||||||
|
)
|
||||||
|
|
||||||
|
type weightedChoice struct {
|
||||||
|
weight int
|
||||||
|
value int
|
||||||
|
}
|
||||||
|
|
||||||
|
func weightedRandom(choices []weightedChoice, r *rand.Rand) int {
|
||||||
|
var total int
|
||||||
|
for _, c := range choices {
|
||||||
|
total += c.weight
|
||||||
|
}
|
||||||
|
|
||||||
|
rnd := r.Intn(total)
|
||||||
|
for _, c := range choices {
|
||||||
|
if rnd < c.weight {
|
||||||
|
return c.value
|
||||||
|
}
|
||||||
|
rnd -= c.weight
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
t := template.New("dragon")
|
||||||
|
var err error
|
||||||
|
t, err = t.Parse(q03Text)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
question.Register(
|
||||||
|
&question.Question{
|
||||||
|
ID: "dragon",
|
||||||
|
Name: "The Dragon Festival",
|
||||||
|
Text: t,
|
||||||
|
Level: question.Level1,
|
||||||
|
Generate: func(u *models.User) string {
|
||||||
|
inp := generate(u)
|
||||||
|
res := make([]string, rows)
|
||||||
|
|
||||||
|
for i, row := range inp {
|
||||||
|
for _, n := range row {
|
||||||
|
res[i] += strconv.Itoa(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(res, "\n")
|
||||||
|
},
|
||||||
|
Validate: func(u *models.User, part question.Part, solution string) bool {
|
||||||
|
return Validate(u, part, solution)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
rows = 120
|
||||||
|
cols = 80
|
||||||
|
)
|
||||||
|
|
||||||
|
var numWeights = []weightedChoice{
|
||||||
|
{weight: 1, value: 0},
|
||||||
|
{weight: 4, value: 1},
|
||||||
|
{weight: 4, value: 2},
|
||||||
|
{weight: 4, value: 3},
|
||||||
|
{weight: 4, value: 4},
|
||||||
|
{weight: 4, value: 5},
|
||||||
|
{weight: 4, value: 6},
|
||||||
|
{weight: 2, value: 7},
|
||||||
|
{weight: 2, value: 8},
|
||||||
|
{weight: 1, value: 9},
|
||||||
|
}
|
||||||
|
|
||||||
|
func generate(u *models.User) [][]int {
|
||||||
|
res := make([][]int, rows)
|
||||||
|
|
||||||
|
r := question.UserRandom(u)
|
||||||
|
|
||||||
|
for i := 0; i < rows; i++ {
|
||||||
|
res[i] = make([]int, cols)
|
||||||
|
for j := 0; j < cols; j++ {
|
||||||
|
res[i][j] = weightedRandom(numWeights, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func Validate(u *models.User, p question.Part, sol string) bool {
|
||||||
|
inp := generate(u)
|
||||||
|
|
||||||
|
var n int
|
||||||
|
switch p {
|
||||||
|
case question.Part1:
|
||||||
|
n = solveP1(inp)
|
||||||
|
case question.Part2:
|
||||||
|
n = solveP2(inp)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("submitted", sol)
|
||||||
|
fmt.Println("actual", n)
|
||||||
|
return strconv.Itoa(n) == sol
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed q03.md
|
||||||
|
var q03Text string
|
@ -0,0 +1,89 @@
|
|||||||
|
As June approaches and summer sports rise in popularity, people are becoming more festive, and want
|
||||||
|
to celebrate.
|
||||||
|
|
||||||
|
You are participating the start of the Dragon Boat Festival, where you will be competing to collect
|
||||||
|
as many coins as possible within 3 hours. As part of the competition, you have been provided the
|
||||||
|
number of coins in each section of the lake.
|
||||||
|
|
||||||
|
As part of your team strategy, you want to first calculate the total number of coins that can be
|
||||||
|
collected.
|
||||||
|
|
||||||
|
Your map is sectioned into a grid, where each digit represents the number of coins in that area.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
- Given the following input
|
||||||
|
|
||||||
|
```
|
||||||
|
46327
|
||||||
|
85390
|
||||||
|
29543
|
||||||
|
```
|
||||||
|
|
||||||
|
Shows that there are a total of `5 * 3 = 15` sections within the lake.
|
||||||
|
Adding up all of the individual digits, we see that the total number of coins within this lake
|
||||||
|
is `70`.
|
||||||
|
|
||||||
|
**What is the total number of coins within the lake?**
|
||||||
|
|
||||||
|
{{ if .Part1.Completed -}}
|
||||||
|
|
||||||
|
**Congratulations! You got Part 1 correct. Your answer was `{{ .Part1.Solution }}`.**
|
||||||
|
|
||||||
|
## Part 2
|
||||||
|
|
||||||
|
You recently learned there there is going to be some serious competition within the Dragon Boat
|
||||||
|
Festival. Nevertheless, you are still determined to win. You have developed a new strategy in order
|
||||||
|
to collect coins as efficiently as possible.
|
||||||
|
|
||||||
|
As part of this secret strategy, which produces the dragon number, you will need to calculate two
|
||||||
|
values:
|
||||||
|
|
||||||
|
1. The maximum sum of coins in any given column
|
||||||
|
|
||||||
|
2. The maximum sum of coins in any given row
|
||||||
|
|
||||||
|
Your final answer is the product of these two sums.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
- Given the following input
|
||||||
|
|
||||||
|
```
|
||||||
|
46327
|
||||||
|
85390
|
||||||
|
29543
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, there are 3 rows, and 5 columns.
|
||||||
|
|
||||||
|
1. Sum of row 1: `4+6+3+2+7 = 22`
|
||||||
|
2. Sum of row 2: `8+5+3+9+0 = 25`
|
||||||
|
3. Sum of row 2: `2+9+5+4+3 = 23`
|
||||||
|
|
||||||
|
**The maximum sum from rows 1-3 is 25**
|
||||||
|
|
||||||
|
1. Sum of column 1: `4+8+2 = 14`
|
||||||
|
2. Sum of column 2: `6+5+9 = 20`
|
||||||
|
3. Sum of column 3: `3+3+5 = 11`
|
||||||
|
4. Sum of column 4: `2+9+4 = 15`
|
||||||
|
5. Sum of column 5: `7+0+3 = 10`
|
||||||
|
|
||||||
|
**The maximum sum from columns 1-5 is 20**
|
||||||
|
|
||||||
|
Using this information, the maximums of the rows and columns is `25` ad `20`.
|
||||||
|
|
||||||
|
The answer, and the dragon number, is `25*20 = 500`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**What is the dragon number for your lake?**
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
{{ if .Part2.Completed -}}
|
||||||
|
|
||||||
|
**Congratulations! You have completed both parts! The answer was `{{ .Part2.Solution }}`.**
|
||||||
|
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
@ -0,0 +1,50 @@
|
|||||||
|
package q03
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hhhapz/codequest/models"
|
||||||
|
"github.com/hhhapz/codequest/question"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQ03(t *testing.T) {
|
||||||
|
u := &models.User{
|
||||||
|
ID: "123",
|
||||||
|
}
|
||||||
|
|
||||||
|
q := question.QuestionByID("forestry")
|
||||||
|
|
||||||
|
raw := q.Generate(u)
|
||||||
|
|
||||||
|
t.Logf("INPUT:\n\n%s\n\n", raw)
|
||||||
|
|
||||||
|
input := make([][]int, 0, rows)
|
||||||
|
|
||||||
|
for _, row := range strings.Split(raw, "\n") {
|
||||||
|
s := make([]int, 0, cols)
|
||||||
|
for _, num := range strings.Split(row, "") {
|
||||||
|
n, _ := strconv.Atoi(num)
|
||||||
|
s = append(s, n)
|
||||||
|
}
|
||||||
|
input = append(input, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := solveP1(input)
|
||||||
|
t.Logf("part 1 result: %d", res)
|
||||||
|
if !q.Validate(u, question.Part1, strconv.Itoa(res)) {
|
||||||
|
t.Errorf("Expected question 1 part 1(%v) to be correct!", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
res = solveP2(input)
|
||||||
|
if !q.Validate(u, question.Part2, strconv.Itoa(res)) {
|
||||||
|
t.Errorf("Expected question 2 part 2(%v) to be correct!", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
if q.Validate(u, question.Part1, "") {
|
||||||
|
t.Errorf("Expected bad input to be invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Input:\n%v", raw)
|
||||||
|
}
|
After Width: | Height: | Size: 8.5 KiB |
After Width: | Height: | Size: 8.5 KiB |
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,19 @@
|
|||||||
|
<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="647.63626" height="632.17383"
|
||||||
|
viewBox="0 0 647.63626 632.17383" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<path d="M687.3279,276.08691H512.81813a15.01828,15.01828,0,0,0-15,15v387.85l-2,.61005-42.81006,13.11a8.00676,8.00676,0,0,1-9.98974-5.31L315.678,271.39691a8.00313,8.00313,0,0,1,5.31006-9.99l65.97022-20.2,191.25-58.54,65.96972-20.2a7.98927,7.98927,0,0,1,9.99024,5.3l32.5498,106.32Z"
|
||||||
|
transform="translate(-276.18187 -133.91309)" fill="#BDC7F1"/>
|
||||||
|
<path d="M725.408,274.08691l-39.23-128.14a16.99368,16.99368,0,0,0-21.23-11.28l-92.75,28.39L380.95827,221.60693l-92.75,28.4a17.0152,17.0152,0,0,0-11.28028,21.23l134.08008,437.93a17.02661,17.02661,0,0,0,16.26026,12.03,16.78926,16.78926,0,0,0,4.96972-.75l63.58008-19.46,2-.62v-2.09l-2,.61-64.16992,19.65a15.01489,15.01489,0,0,1-18.73-9.95l-134.06983-437.94a14.97935,14.97935,0,0,1,9.94971-18.73l92.75-28.4,191.24024-58.54,92.75-28.4a15.15551,15.15551,0,0,1,4.40966-.66,15.01461,15.01461,0,0,1,14.32032,10.61l39.0498,127.56.62012,2h2.08008Z"
|
||||||
|
transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/>
|
||||||
|
<path d="M398.86279,261.73389a9.0157,9.0157,0,0,1-8.61133-6.3667l-12.88037-42.07178a8.99884,8.99884,0,0,1,5.9712-11.24023l175.939-53.86377a9.00867,9.00867,0,0,1,11.24072,5.9707l12.88037,42.07227a9.01029,9.01029,0,0,1-5.9707,11.24072L401.49219,261.33887A8.976,8.976,0,0,1,398.86279,261.73389Z"
|
||||||
|
transform="translate(-276.18187 -133.91309)" fill="#6c63ff"/>
|
||||||
|
<circle cx="190.15351" cy="24.95465" r="20" fill="#6c63ff"/>
|
||||||
|
<circle cx="190.15351" cy="24.95465" r="12.66462" fill="#fff"/>
|
||||||
|
<path d="M878.81836,716.08691h-338a8.50981,8.50981,0,0,1-8.5-8.5v-405a8.50951,8.50951,0,0,1,8.5-8.5h338a8.50982,8.50982,0,0,1,8.5,8.5v405A8.51013,8.51013,0,0,1,878.81836,716.08691Z"
|
||||||
|
transform="translate(-276.18187 -133.91309)" fill="#BDC7F1"/>
|
||||||
|
<path d="M723.31813,274.08691h-210.5a17.02411,17.02411,0,0,0-17,17v407.8l2-.61v-407.19a15.01828,15.01828,0,0,1,15-15H723.93825Zm183.5,0h-394a17.02411,17.02411,0,0,0-17,17v458a17.0241,17.0241,0,0,0,17,17h394a17.0241,17.0241,0,0,0,17-17v-458A17.02411,17.02411,0,0,0,906.81813,274.08691Zm15,475a15.01828,15.01828,0,0,1-15,15h-394a15.01828,15.01828,0,0,1-15-15v-458a15.01828,15.01828,0,0,1,15-15h394a15.01828,15.01828,0,0,1,15,15Z"
|
||||||
|
transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/>
|
||||||
|
<path d="M801.81836,318.08691h-184a9.01015,9.01015,0,0,1-9-9v-44a9.01016,9.01016,0,0,1,9-9h184a9.01016,9.01016,0,0,1,9,9v44A9.01015,9.01015,0,0,1,801.81836,318.08691Z"
|
||||||
|
transform="translate(-276.18187 -133.91309)" fill="#6c63ff"/>
|
||||||
|
<circle cx="433.63626" cy="105.17383" r="20" fill="#6c63ff"/>
|
||||||
|
<circle cx="433.63626" cy="105.17383" r="12.18187" fill="#fff"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,7 @@
|
|||||||
|
//go:build tools
|
||||||
|
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/gunk/opt"
|
||||||
|
)
|
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"0 debug pnpm:scope": {
|
||||||
|
"selected": 1
|
||||||
|
},
|
||||||
|
"1 error pnpm": {
|
||||||
|
"errno": 1,
|
||||||
|
"code": "ELIFECYCLE",
|
||||||
|
"pkgid": "cq-ui@0.0.1",
|
||||||
|
"stage": "lint",
|
||||||
|
"script": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
||||||
|
"pkgname": "cq-ui",
|
||||||
|
"err": {
|
||||||
|
"name": "pnpm",
|
||||||
|
"message": "cq-ui@0.0.1 lint: `prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .`\nExit status 1",
|
||||||
|
"code": "ELIFECYCLE",
|
||||||
|
"stack": "pnpm: cq-ui@0.0.1 lint: `prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .`\nExit status 1\n at EventEmitter.<anonymous> (/usr/lib/node_modules/pnpm/dist/pnpm.cjs:105830:20)\n at EventEmitter.emit (node:events:527:28)\n at ChildProcess.<anonymous> (/usr/lib/node_modules/pnpm/dist/pnpm.cjs:92391:18)\n at ChildProcess.emit (node:events:527:28)\n at maybeClose (node:internal/child_process:1090:16)\n at Process.ChildProcess._handle.onexit (node:internal/child_process:302:5)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,10 @@
|
|||||||
/* Write your global styles here, in PostCSS syntax */
|
@import 'tailwindcss/base';
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
@import 'tailwindcss/components';
|
||||||
@tailwind utilities;
|
@import './components.css';
|
||||||
|
|
||||||
|
@import 'tailwindcss/utilities';
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
a:not(.btn) {
|
||||||
|
@apply text-indigo-600 hover:text-indigo-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
@apply block bg-indigo-600 hover:bg-indigo-700 text-white text-center font-bold transition duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.inverse {
|
||||||
|
@apply bg-indigo-100 hover:bg-indigo-200 text-indigo-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.cancel {
|
||||||
|
@apply bg-transparent hover:bg-black hover:text-white text-black border border-black font-normal transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.ghost {
|
||||||
|
@apply bg-gray-50 hover:bg-gray-300 border border-neutral-500 text-gray-600;
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
|
||||||
|
export let end: string;
|
||||||
|
$: endTime = new Date(end);
|
||||||
|
|
||||||
|
let h = 0;
|
||||||
|
let m = 0;
|
||||||
|
let s = 0;
|
||||||
|
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
let i;
|
||||||
|
|
||||||
|
const calc = () => {
|
||||||
|
let now = new Date();
|
||||||
|
let secs = Math.ceil((endTime.getTime() - now.getTime()) / 1000);
|
||||||
|
h = Math.floor(secs / 3600);
|
||||||
|
m = Math.floor((secs % 3600) / 60);
|
||||||
|
s = Math.floor((secs % 3600) % 60);
|
||||||
|
|
||||||
|
if (secs < 0) {
|
||||||
|
active = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onMount(() => {
|
||||||
|
calc();
|
||||||
|
i = setInterval(calc, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
clearInterval(i);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if active}
|
||||||
|
Active - {h}:{('0' + m).slice(-2)}:{('0' + s).slice(-2)}
|
||||||
|
{:else}
|
||||||
|
Contest Ended
|
||||||
|
{/if}
|
@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
const { close } = getContext('simple-modal');
|
||||||
|
export let title: string = 'Error';
|
||||||
|
export let reason: string;
|
||||||
|
export let btn: { title: string; do: any } = null;
|
||||||
|
|
||||||
|
const perform = () => {
|
||||||
|
close();
|
||||||
|
btn.do();
|
||||||
|
};
|
||||||
|
|
||||||
|
let cleaned = '';
|
||||||
|
$: {
|
||||||
|
let first = reason[0] || '';
|
||||||
|
let rest = reason.slice(1) || '';
|
||||||
|
|
||||||
|
if (!['.', '!'].includes(rest.slice(-1))) {
|
||||||
|
rest += '.';
|
||||||
|
}
|
||||||
|
cleaned = first.toUpperCase() + rest;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="sm:flex sm:items-start pl-3 pt-2">
|
||||||
|
<div
|
||||||
|
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"
|
||||||
|
>
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
|
<h3 class="text-xl leading-6 font-medium text-gray-900" id="modal-title">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2">
|
||||||
|
<p class="text-lg text-gray-500">{cleaned}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse gap-4">
|
||||||
|
{#if btn}
|
||||||
|
<button type="button" class="btn px-2 text-lg md:py-1 rounded" on:click={perform}>
|
||||||
|
{btn.title}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<button type="button" class="btn cancel px-2 text-lg md:py-1 rounded" on:click={close}>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,27 @@
|
|||||||
|
<div class="bg-indigo-200 shadow-inner">
|
||||||
|
<div class="max-w-2xl mx-auto py-10">
|
||||||
|
<div
|
||||||
|
class="relative flex flex-col md:flex-row md:justify-between items-center text-sm text-gray-800"
|
||||||
|
>
|
||||||
|
<p class="order-2 md:order-1 mt-8 md:mt-0">© Hamza Ali, 2022.</p>
|
||||||
|
<p class="order-1 md:order-2 mt-8 md:mt-0 flex gap-1">
|
||||||
|
Made with
|
||||||
|
<span class="text-red-500">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-5 w-5"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
using Go, gRPC, and Svelte Kit.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,53 @@
|
|||||||
|
<script>
|
||||||
|
import { AuthService } from '../pb/all.pb';
|
||||||
|
import { noToken } from '../pb/pbutil';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { auth } from '$lib/stores';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
let loggedIn = false;
|
||||||
|
|
||||||
|
const login = async () => {
|
||||||
|
if (loggedIn) {
|
||||||
|
goto('/dashboard');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = await AuthService.OAuthCode({}, noToken());
|
||||||
|
console.log(url);
|
||||||
|
window.location.href = url.redirectURI;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
auth.subscribe((auth) => {
|
||||||
|
loggedIn = auth.loggedIn();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
on:click={login}
|
||||||
|
class="inline-flex justify-center items-center gap-2
|
||||||
|
border border-black p-2 text-sm shadow-sm rounded cursor-pointer hover:bg-black hover:text-white transition-all duration-300"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" width="20" height="20" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="matrix(1, 0, 0, 1, 27.009001, -39.238998)">
|
||||||
|
<path
|
||||||
|
fill="#4285F4"
|
||||||
|
d="M -3.264 51.509 C -3.264 50.719 -3.334 49.969 -3.454 49.239 L -14.754 49.239 L -14.754 53.749 L -8.284 53.749 C -8.574 55.229 -9.424 56.479 -10.684 57.329 L -10.684 60.329 L -6.824 60.329 C -4.564 58.239 -3.264 55.159 -3.264 51.509 Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#34A853"
|
||||||
|
d="M -14.754 63.239 C -11.514 63.239 -8.804 62.159 -6.824 60.329 L -10.684 57.329 C -11.764 58.049 -13.134 58.489 -14.754 58.489 C -17.884 58.489 -20.534 56.379 -21.484 53.529 L -25.464 53.529 L -25.464 56.619 C -23.494 60.539 -19.444 63.239 -14.754 63.239 Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#FBBC05"
|
||||||
|
d="M -21.484 53.529 C -21.734 52.809 -21.864 52.039 -21.864 51.239 C -21.864 50.439 -21.724 49.669 -21.484 48.949 L -21.484 45.859 L -25.464 45.859 C -26.284 47.479 -26.754 49.299 -26.754 51.239 C -26.754 53.179 -26.284 54.999 -25.464 56.619 L -21.484 53.529 Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#EA4335"
|
||||||
|
d="M -14.754 43.989 C -12.984 43.989 -11.404 44.599 -10.154 45.789 L -6.734 42.369 C -8.804 40.429 -11.514 39.239 -14.754 39.239 C -19.444 39.239 -23.494 41.939 -25.464 45.859 L -21.484 48.949 C -20.534 46.099 -17.884 43.989 -14.754 43.989 Z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Sign in with Google
|
||||||
|
</div>
|
@ -0,0 +1,39 @@
|
|||||||
|
<script>
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import GoogleButton from './GoogleButton.svelte';
|
||||||
|
const { close } = getContext('simple-modal');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="sm:flex sm:items-start pl-3 pt-2">
|
||||||
|
<div
|
||||||
|
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-6 w-6 text-red-600"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">Login Required</h3>
|
||||||
|
<div class="mt-2">
|
||||||
|
<p class="text-sm text-gray-500">To access this page, you must be logged in.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse gap-4">
|
||||||
|
<GoogleButton />
|
||||||
|
<button type="button" class="btn cancel px-2 text-sm md:py-1 rounded" on:click={close}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,124 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import GoogleButton from './GoogleButton.svelte';
|
||||||
|
import { auth } from '$lib/stores';
|
||||||
|
|
||||||
|
let pages = [{ id: 'about', name: 'About', loc: '/about' }];
|
||||||
|
|
||||||
|
let hidden = true;
|
||||||
|
let loggedIn = false;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if ($auth.loggedIn()) {
|
||||||
|
pages = [
|
||||||
|
{ id: 'home', name: 'Home', loc: '/' },
|
||||||
|
{ id: 'dashboard', name: 'Dashboard', loc: '/dashboard' },
|
||||||
|
{ id: 'questions', name: 'Puzzles', loc: '/dashboard/questions' },
|
||||||
|
{ id: 'leaderboard', name: 'Leaderboard', loc: '/dashboard/leaderboard' },
|
||||||
|
{ id: 'logout', name: 'Log Out', loc: '/logout' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
hidden = !hidden;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
auth.subscribe((auth) => {
|
||||||
|
loggedIn = auth.loggedIn();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav class="relative bg-white shadow-xl">
|
||||||
|
<div class="h-16 flex justify-between items-center container mx-auto">
|
||||||
|
{#if loggedIn}
|
||||||
|
<a class="text-3xl font-bold leading-none ml-4 lg:ml-0" href="/dashboard" sveltekit:prefetch>
|
||||||
|
<h2 class="text-lg">
|
||||||
|
CodeQuest
|
||||||
|
<sub class="text-xs font-medium tracking-tight">Alpha</sub>
|
||||||
|
</h2>
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<a class="text-3xl font-bold leading-none ml-4 lg:ml-0" href="/" sveltekit:prefetch>
|
||||||
|
<h2 class="text-lg">
|
||||||
|
CodeQuest
|
||||||
|
<sub class="text-xs font-medium tracking-tight">Alpha</sub>
|
||||||
|
</h2>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
<div class="lg:hidden">
|
||||||
|
<button on:click={toggle} class="flex items-center text-blue-600 p-3">
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg class="block h-4 w-4 fill-current" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" > <path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z" />
|
||||||
|
</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<ul class="hidden lg:flex lg:flex lg:items-center gap-1.5">
|
||||||
|
{#each pages as p, i}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="text-sm text-gray-600 hover:text-gray-800 hover:border-b hover:border-blue-500"
|
||||||
|
href={p.loc}
|
||||||
|
>
|
||||||
|
{p.name}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{#if i + 1 < pages.length || !loggedIn}
|
||||||
|
<li class="text-gray-500">
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" class="w-4 h-4 current-fill" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v0m0 7v0m0 7v0m0-13a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" /> </svg>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<li>
|
||||||
|
{#if !loggedIn}
|
||||||
|
<GoogleButton />
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{#if !hidden}
|
||||||
|
<div class="relative z-50 -left-100" transition:fade={{ duration: 100 }}>
|
||||||
|
<nav
|
||||||
|
class="fixed top-0 left-0 bottom-0 flex flex-col w-5/6 max-w-[250px] py-6 px-6 bg-white border-r overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div class="flex items-center mb-8">
|
||||||
|
<a class="mr-auto text-3xl font-bold leading-none" href="/">
|
||||||
|
<h2 class="text-lg">CodeQuest ALPHA</h2>
|
||||||
|
</a>
|
||||||
|
<button on:click={toggle}>
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg class="h-6 w-6 text-gray-400 cursor-pointer hover:text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> </svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
{#each pages as p}
|
||||||
|
<li class="mb-1">
|
||||||
|
<a
|
||||||
|
class="block p-4 text-sm font-semibold text-gray-400 hover:bg-blue-50 hover:text-blue-600 rounded"
|
||||||
|
href={p.loc}
|
||||||
|
>{p.name}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="mt-auto">
|
||||||
|
<div class="pt-6">
|
||||||
|
{#if !loggedIn}
|
||||||
|
<GoogleButton />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<p class="my-4 text-xs text-center text-gray-400">
|
||||||
|
<span>Copyright © 2021-2022</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
{/if}
|
@ -0,0 +1,61 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Difficulty, type Question } from '../pb/all.pb';
|
||||||
|
export let q: Question;
|
||||||
|
|
||||||
|
type Status = 'available' | 'partial' | 'full';
|
||||||
|
|
||||||
|
let status: Status = 'available';
|
||||||
|
let solved = 0;
|
||||||
|
$: {
|
||||||
|
if (q.part1.completed && q.part2.completed) {
|
||||||
|
status = 'full';
|
||||||
|
solved = 2;
|
||||||
|
} else if (q.part1.completed || q.part2.completed) {
|
||||||
|
status = 'partial';
|
||||||
|
solved = 1;
|
||||||
|
} else {
|
||||||
|
status = 'available';
|
||||||
|
solved = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a class={`question h-24 border-2 ${status}`} href={`/dashboard/question/${q.id}`}>
|
||||||
|
<span class="title">{q.title} - {solved}/2</span>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="points">Points: {q.part1.pointsWorth + q.part2.pointsWorth}</span>
|
||||||
|
<span class="difficulty">
|
||||||
|
{#if q.difficulty == Difficulty.Level1}
|
||||||
|
Difficulty: 1
|
||||||
|
{:else if q.difficulty == Difficulty.Level2}
|
||||||
|
Difficulty: 2
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.question {
|
||||||
|
@apply flex justify-between flex-col p-4 w-full rounded-md shadow-md border-2;
|
||||||
|
@apply cursor-pointer transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available {
|
||||||
|
@apply border-gray-400 hover:bg-gray-100;
|
||||||
|
@apply text-gray-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partial {
|
||||||
|
@apply border-yellow-400 bg-yellow-50 hover:bg-yellow-100;
|
||||||
|
@apply text-yellow-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full {
|
||||||
|
@apply border-green-400 bg-green-50 hover:bg-green-100;
|
||||||
|
@apply text-green-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@apply font-bold uppercase;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,61 @@
|
|||||||
|
<script>
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
const { close } = getContext('simple-modal');
|
||||||
|
|
||||||
|
export let correct = false;
|
||||||
|
export let points = 0;
|
||||||
|
export let part = 1;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="sm:flex sm:items-start pl-3 pt-2">
|
||||||
|
{#if correct}
|
||||||
|
<div
|
||||||
|
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-green-200 sm:mx-0 sm:h-10 sm:w-10"
|
||||||
|
>
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-green-700" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"
|
||||||
|
>
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
|
<h3 class="text-xl leading-6 font-medium text-gray-900" id="modal-title">
|
||||||
|
{#if correct}
|
||||||
|
Correct!
|
||||||
|
{:else}
|
||||||
|
Incorrect
|
||||||
|
{/if}
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2">
|
||||||
|
<p class="text-lg text-gray-500">
|
||||||
|
{#if correct}
|
||||||
|
{#if part == 1}
|
||||||
|
You have completed the first part of the question.
|
||||||
|
<br />
|
||||||
|
You earned <strong>{points} points</strong> for part 1.
|
||||||
|
{:else}
|
||||||
|
You have completed both parts of the question.
|
||||||
|
<br />
|
||||||
|
You earned <strong>{points} points</strong> for part 2.
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
The answer you have provided was not correct. Try again!
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse gap-4">
|
||||||
|
<button type="button" class="btn cancel px-2 text-lg md:py-1 rounded" on:click={close}>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"0 debug pnpm:scope": {
|
||||||
|
"selected": 1
|
||||||
|
},
|
||||||
|
"1 error pnpm": {
|
||||||
|
"errno": 1,
|
||||||
|
"code": "ELIFECYCLE",
|
||||||
|
"pkgid": "cq-ui@0.0.1",
|
||||||
|
"stage": "lint",
|
||||||
|
"script": "prettier --ignore-path .gitignore --check --plugin-search-dir=. web_src && eslint --ignore-path .gitignore web_src",
|
||||||
|
"pkgname": "cq-ui",
|
||||||
|
"err": {
|
||||||
|
"name": "pnpm",
|
||||||
|
"message": "cq-ui@0.0.1 lint: `prettier --ignore-path .gitignore --check --plugin-search-dir=. web_src && eslint --ignore-path .gitignore web_src`\nExit status 1",
|
||||||
|
"code": "ELIFECYCLE",
|
||||||
|
"stack": "pnpm: cq-ui@0.0.1 lint: `prettier --ignore-path .gitignore --check --plugin-search-dir=. web_src && eslint --ignore-path .gitignore web_src`\nExit status 1\n at EventEmitter.<anonymous> (/usr/pnpm-global/5/node_modules/.pnpm/pnpm@6.32.8/node_modules/pnpm/dist/pnpm.cjs:105794:20)\n at EventEmitter.emit (node:events:527:28)\n at ChildProcess.<anonymous> (/usr/pnpm-global/5/node_modules/.pnpm/pnpm@6.32.8/node_modules/pnpm/dist/pnpm.cjs:92355:18)\n at ChildProcess.emit (node:events:527:28)\n at maybeClose (node:internal/child_process:1090:16)\n at Process.ChildProcess._handle.onexit (node:internal/child_process:302:5)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,204 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fm from "./fetch.pb"
|
||||||
|
|
||||||
|
export enum Difficulty {
|
||||||
|
Level1 = "Level1",
|
||||||
|
Level2 = "Level2",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Token = {
|
||||||
|
token?: string
|
||||||
|
expires?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OAuthCodeRequest = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OAuthCodeResponse = {
|
||||||
|
redirectURI?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TokenRequest = {
|
||||||
|
code?: string
|
||||||
|
state?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DeleteTokenRequest = {
|
||||||
|
all?: boolean
|
||||||
|
token?: Token
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DeleteTokenResponse = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Info = {
|
||||||
|
currentUser?: User
|
||||||
|
active?: boolean
|
||||||
|
points?: number
|
||||||
|
startTime?: string
|
||||||
|
endTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
id?: string
|
||||||
|
name?: string
|
||||||
|
email?: string
|
||||||
|
picture?: string
|
||||||
|
admin?: boolean
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QuestionsRequest = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PartData = {
|
||||||
|
completed?: boolean
|
||||||
|
pointsWorth?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Question = {
|
||||||
|
id?: string
|
||||||
|
title?: string
|
||||||
|
text?: string
|
||||||
|
difficulty?: Difficulty
|
||||||
|
part1?: PartData
|
||||||
|
part2?: PartData
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QuestionsResponse = {
|
||||||
|
questions?: Question[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QuestionByIDRequest = {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QuestionInputRequest = {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QuestionInput = {
|
||||||
|
id?: string
|
||||||
|
input?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubmitRequestData = {
|
||||||
|
answer?: string
|
||||||
|
code?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubmitRequest = {
|
||||||
|
id?: string
|
||||||
|
part?: number
|
||||||
|
body?: SubmitRequestData
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubmitResponse = {
|
||||||
|
correct?: boolean
|
||||||
|
points?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LeaderboardRequest = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LeaderboardResponse = {
|
||||||
|
leaderboard?: LeaderboardEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LeaderboardEntry = {
|
||||||
|
username?: string
|
||||||
|
points?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InfoRequest = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateFields = {
|
||||||
|
name?: string
|
||||||
|
gradeLevel?: number
|
||||||
|
admin?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdminUpdateFields = {
|
||||||
|
email?: string
|
||||||
|
name?: string
|
||||||
|
gradeLevel?: number
|
||||||
|
admin?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateUserRequest = {
|
||||||
|
body?: UpdateFields
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdminUpdateUserRequest = {
|
||||||
|
body?: AdminUpdateFields
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserByEmailRequest = {
|
||||||
|
email?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DeleteUserRequest = {
|
||||||
|
email?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AllUsersRequest = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AllUsersResponse = {
|
||||||
|
users?: User[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthService {
|
||||||
|
static OAuthCode(req: OAuthCodeRequest, initReq?: fm.InitReq): Promise<OAuthCodeResponse> {
|
||||||
|
return fm.fetchReq<OAuthCodeRequest, OAuthCodeResponse>(`/v1/auth/code?${fm.renderURLSearchParams(req, [])}`, {...initReq, method: "GET"})
|
||||||
|
}
|
||||||
|
static Token(req: TokenRequest, initReq?: fm.InitReq): Promise<Token> {
|
||||||
|
return fm.fetchReq<TokenRequest, Token>(`/v1/auth/token?${fm.renderURLSearchParams(req, [])}`, {...initReq, method: "GET"})
|
||||||
|
}
|
||||||
|
static DeleteToken(req: DeleteTokenRequest, initReq?: fm.InitReq): Promise<DeleteTokenResponse> {
|
||||||
|
return fm.fetchReq<DeleteTokenRequest, DeleteTokenResponse>(`/v1/auth/token`, {...initReq, method: "DELETE"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class QuestService {
|
||||||
|
static Questions(req: QuestionsRequest, initReq?: fm.InitReq): Promise<QuestionsResponse> {
|
||||||
|
return fm.fetchReq<QuestionsRequest, QuestionsResponse>(`/v1/questions?${fm.renderURLSearchParams(req, [])}`, {...initReq, method: "GET"})
|
||||||
|
}
|
||||||
|
static QuestionByID(req: QuestionByIDRequest, initReq?: fm.InitReq): Promise<Question> {
|
||||||
|
return fm.fetchReq<QuestionByIDRequest, Question>(`/v1/questions/${req["id"]}?${fm.renderURLSearchParams(req, ["id"])}`, {...initReq, method: "GET"})
|
||||||
|
}
|
||||||
|
static QuestionInput(req: QuestionInputRequest, initReq?: fm.InitReq): Promise<QuestionInput> {
|
||||||
|
return fm.fetchReq<QuestionInputRequest, QuestionInput>(`/v1/questions/${req["id"]}/input?${fm.renderURLSearchParams(req, ["id"])}`, {...initReq, method: "GET"})
|
||||||
|
}
|
||||||
|
static Submit(req: SubmitRequest, initReq?: fm.InitReq): Promise<SubmitResponse> {
|
||||||
|
return fm.fetchReq<SubmitRequest, SubmitResponse>(`/v1/questions/${req["id"]}/${req["part"]}`, {...initReq, method: "POST", body: JSON.stringify(req["Body"])})
|
||||||
|
}
|
||||||
|
static Leaderboard(req: LeaderboardRequest, initReq?: fm.InitReq): Promise<LeaderboardResponse> {
|
||||||
|
return fm.fetchReq<LeaderboardRequest, LeaderboardResponse>(`/v1/questions/leaderboard?${fm.renderURLSearchParams(req, [])}`, {...initReq, method: "GET"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class UserService {
|
||||||
|
static Info(req: InfoRequest, initReq?: fm.InitReq): Promise<Info> {
|
||||||
|
return fm.fetchReq<InfoRequest, Info>(`/v1/users/me?${fm.renderURLSearchParams(req, [])}`, {...initReq, method: "GET"})
|
||||||
|
}
|
||||||
|
static UserByEmail(req: UserByEmailRequest, initReq?: fm.InitReq): Promise<User> {
|
||||||
|
return fm.fetchReq<UserByEmailRequest, User>(`/v1/admin/users/${req["email"]}?${fm.renderURLSearchParams(req, ["email"])}`, {...initReq, method: "GET"})
|
||||||
|
}
|
||||||
|
static AllUsers(req: AllUsersRequest, initReq?: fm.InitReq): Promise<AllUsersResponse> {
|
||||||
|
return fm.fetchReq<AllUsersRequest, AllUsersResponse>(`/v1/admin/users?${fm.renderURLSearchParams(req, [])}`, {...initReq, method: "GET"})
|
||||||
|
}
|
||||||
|
static UpdateUser(req: UpdateUserRequest, initReq?: fm.InitReq): Promise<User> {
|
||||||
|
return fm.fetchReq<UpdateUserRequest, User>(`/v1/users/me`, {...initReq, method: "PATCH", body: JSON.stringify(req["Body"])})
|
||||||
|
}
|
||||||
|
static AdminUpdateUser(req: AdminUpdateUserRequest, initReq?: fm.InitReq): Promise<User> {
|
||||||
|
return fm.fetchReq<AdminUpdateUserRequest, User>(`/v1/admin/users/${req["bodyEmail"]}`, {...initReq, method: "PATCH", body: JSON.stringify(req["Body"])})
|
||||||
|
}
|
||||||
|
static DeleteUser(req: DeleteUserRequest, initReq?: fm.InitReq): Promise<User> {
|
||||||
|
return fm.fetchReq<DeleteUserRequest, User>(`/v1/admin/users/${req["email"]}`, {...initReq, method: "DELETE"})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,233 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* This file is a generated Typescript file for GRPC Gateway, DO NOT MODIFY
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface InitReq extends RequestInit {
|
||||||
|
pathPrefix?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchReq<I, O>(path: string, init?: InitReq): Promise<O> {
|
||||||
|
const { pathPrefix, ...req } = init || {};
|
||||||
|
|
||||||
|
const url = pathPrefix ? `${pathPrefix}${path}` : path;
|
||||||
|
|
||||||
|
return fetch(url, req).then((r) =>
|
||||||
|
r.json().then((body: O) => {
|
||||||
|
if (!r.ok) {
|
||||||
|
throw body;
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
})
|
||||||
|
) as Promise<O>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyStreamEntityArrival is a callback that will be called on streaming entity arrival
|
||||||
|
export type NotifyStreamEntityArrival<T> = (resp: T) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fetchStreamingRequest is able to handle grpc-gateway server side streaming call
|
||||||
|
* it takes NotifyStreamEntityArrival that lets users respond to entity arrival during the call
|
||||||
|
* all entities will be returned as an array after the call finishes.
|
||||||
|
**/
|
||||||
|
export async function fetchStreamingRequest<S, R>(
|
||||||
|
path: string,
|
||||||
|
callback?: NotifyStreamEntityArrival<R>,
|
||||||
|
init?: InitReq
|
||||||
|
) {
|
||||||
|
const { pathPrefix, ...req } = init || {};
|
||||||
|
const url = pathPrefix ? `${pathPrefix}${path}` : path;
|
||||||
|
const result = await fetch(url, req);
|
||||||
|
// needs to use the .ok to check the status of HTTP status code
|
||||||
|
// http other than 200 will not throw an error, instead the .ok will become false.
|
||||||
|
// see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#
|
||||||
|
if (!result.ok) {
|
||||||
|
const resp = await result.json();
|
||||||
|
const errMsg = resp.error && resp.error.message ? resp.error.message : '';
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.body) {
|
||||||
|
throw new Error('response doesnt have a body');
|
||||||
|
}
|
||||||
|
|
||||||
|
await result.body
|
||||||
|
.pipeThrough(new TextDecoderStream())
|
||||||
|
.pipeThrough<R>(getNewLineDelimitedJSONDecodingStream<R>())
|
||||||
|
.pipeTo(
|
||||||
|
getNotifyEntityArrivalSink((e: R) => {
|
||||||
|
if (callback) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// wait for the streaming to finish and return the success respond
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSONStringStreamController represents the transform controller that's able to transform the incoming
|
||||||
|
* new line delimited json content stream into entities and able to push the entity to the down stream
|
||||||
|
*/
|
||||||
|
interface JSONStringStreamController<T> extends TransformStreamDefaultController {
|
||||||
|
buf?: string;
|
||||||
|
pos?: number;
|
||||||
|
enqueue: (s: T) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getNewLineDelimitedJSONDecodingStream returns a TransformStream that's able to handle new line delimited json stream content into parsed entities
|
||||||
|
*/
|
||||||
|
function getNewLineDelimitedJSONDecodingStream<T>(): TransformStream<string, T> {
|
||||||
|
return new TransformStream({
|
||||||
|
start(controller: JSONStringStreamController<T>) {
|
||||||
|
controller.buf = '';
|
||||||
|
controller.pos = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
transform(chunk: string, controller: JSONStringStreamController<T>) {
|
||||||
|
if (controller.buf === undefined) {
|
||||||
|
controller.buf = '';
|
||||||
|
}
|
||||||
|
if (controller.pos === undefined) {
|
||||||
|
controller.pos = 0;
|
||||||
|
}
|
||||||
|
controller.buf += chunk;
|
||||||
|
while (controller.pos < controller.buf.length) {
|
||||||
|
if (controller.buf[controller.pos] === '\n') {
|
||||||
|
const line = controller.buf.substring(0, controller.pos);
|
||||||
|
const response = JSON.parse(line);
|
||||||
|
controller.enqueue(response.result);
|
||||||
|
controller.buf = controller.buf.substring(controller.pos + 1);
|
||||||
|
controller.pos = 0;
|
||||||
|
} else {
|
||||||
|
++controller.pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getNotifyEntityArrivalSink takes the NotifyStreamEntityArrival callback and return
|
||||||
|
* a sink that will call the callback on entity arrival
|
||||||
|
* @param notifyCallback
|
||||||
|
*/
|
||||||
|
function getNotifyEntityArrivalSink<T>(notifyCallback: NotifyStreamEntityArrival<T>) {
|
||||||
|
return new WritableStream<T>({
|
||||||
|
write(entity: T) {
|
||||||
|
notifyCallback(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type Primitive = string | boolean | number;
|
||||||
|
type RequestPayload = Record<string, unknown>;
|
||||||
|
type FlattenedRequestPayload = Record<string, Primitive | Array<Primitive>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if given value is a plain object
|
||||||
|
* Logic copied and adapted from below source:
|
||||||
|
* https://github.com/char0n/ramda-adjunct/blob/master/src/isPlainObj.js
|
||||||
|
* @param {unknown} value
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
function isPlainObject(value: unknown): boolean {
|
||||||
|
const isObject = Object.prototype.toString.call(value).slice(8, -1) === 'Object';
|
||||||
|
const isObjLike = value !== null && isObject;
|
||||||
|
|
||||||
|
if (!isObjLike || !isObject) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const proto = Object.getPrototypeOf(value);
|
||||||
|
|
||||||
|
const hasObjectConstructor =
|
||||||
|
typeof proto === 'object' && proto.constructor === Object.prototype.constructor;
|
||||||
|
|
||||||
|
return hasObjectConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if given value is of a primitive type
|
||||||
|
* @param {unknown} value
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
function isPrimitive(value: unknown): boolean {
|
||||||
|
return ['string', 'number', 'boolean'].some((t) => typeof value === t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if given primitive is zero-value
|
||||||
|
* @param {Primitive} value
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
function isZeroValuePrimitive(value: Primitive): boolean {
|
||||||
|
return value === false || value === 0 || value === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flattens a deeply nested request payload and returns an object
|
||||||
|
* with only primitive values and non-empty array of primitive values
|
||||||
|
* as per https://github.com/googleapis/googleapis/blob/master/google/api/http.proto
|
||||||
|
* @param {RequestPayload} requestPayload
|
||||||
|
* @param {String} path
|
||||||
|
* @return {FlattenedRequestPayload>}
|
||||||
|
*/
|
||||||
|
function flattenRequestPayload<T extends RequestPayload>(
|
||||||
|
requestPayload: T,
|
||||||
|
path: string = ''
|
||||||
|
): FlattenedRequestPayload {
|
||||||
|
return Object.keys(requestPayload).reduce((acc: T, key: string): T => {
|
||||||
|
const value = requestPayload[key];
|
||||||
|
const newPath = path ? [path, key].join('.') : key;
|
||||||
|
|
||||||
|
const isNonEmptyPrimitiveArray =
|
||||||
|
Array.isArray(value) && value.every((v) => isPrimitive(v)) && value.length > 0;
|
||||||
|
|
||||||
|
const isNonZeroValuePrimitive = isPrimitive(value) && !isZeroValuePrimitive(value as Primitive);
|
||||||
|
|
||||||
|
let objectToMerge = {};
|
||||||
|
|
||||||
|
if (isPlainObject(value)) {
|
||||||
|
objectToMerge = flattenRequestPayload(value as RequestPayload, newPath);
|
||||||
|
} else if (isNonZeroValuePrimitive || isNonEmptyPrimitiveArray) {
|
||||||
|
objectToMerge = { [newPath]: value };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...acc, ...objectToMerge };
|
||||||
|
}, {} as T) as FlattenedRequestPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a deeply nested request payload into a string of URL search
|
||||||
|
* parameters by first flattening the request payload and then removing keys
|
||||||
|
* which are already present in the URL path.
|
||||||
|
* @param {RequestPayload} requestPayload
|
||||||
|
* @param {string[]} urlPathParams
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export function renderURLSearchParams<T extends RequestPayload>(
|
||||||
|
requestPayload: T,
|
||||||
|
urlPathParams: string[] = []
|
||||||
|
): string {
|
||||||
|
const flattenedRequestPayload = flattenRequestPayload(requestPayload);
|
||||||
|
|
||||||
|
const urlSearchParams = Object.keys(flattenedRequestPayload).reduce(
|
||||||
|
(acc: string[][], key: string): string[][] => {
|
||||||
|
// key should not be present in the url path as a parameter
|
||||||
|
const value = flattenedRequestPayload[key];
|
||||||
|
if (urlPathParams.find((f) => f === key)) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
return Array.isArray(value)
|
||||||
|
? [...acc, ...value.map((m) => [key, m.toString()])]
|
||||||
|
: (acc = [...acc, [key, value.toString()]]);
|
||||||
|
},
|
||||||
|
[] as string[][]
|
||||||
|
);
|
||||||
|
|
||||||
|
return new URLSearchParams(urlSearchParams).toString();
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
import { browser } from '$app/env';
|
||||||
|
|
||||||
|
const protocol = browser ? window.location.protocol : 'http:';
|
||||||
|
const pathPrefix = protocol + '//api.playcode.quest';
|
||||||
|
|
||||||
|
export const withToken = (token: string): any => {
|
||||||
|
return {
|
||||||
|
pathPrefix,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const noToken = (): any => {
|
||||||
|
return {
|
||||||
|
pathPrefix
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,70 @@
|
|||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { browser } from '$app/env';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import type { Info } from '$lib/pb/all.pb';
|
||||||
|
import { UserService } from '$lib/pb/all.pb';
|
||||||
|
import { withToken } from '$lib/pb/pbutil';
|
||||||
|
|
||||||
|
const token = browser ? window.localStorage.getItem('authToken') : null;
|
||||||
|
|
||||||
|
export class Data {
|
||||||
|
readonly info?: Info;
|
||||||
|
readonly token?: string;
|
||||||
|
|
||||||
|
constructor(token?: string, info?: Info) {
|
||||||
|
this.info = info;
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
loggedIn(): boolean {
|
||||||
|
return this.token !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
login(newToken: string) {
|
||||||
|
window.localStorage.setItem('authToken', newToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
window.localStorage.removeItem('authToken');
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
UserService.Info({}, withToken(this.token))
|
||||||
|
.then((info) => {
|
||||||
|
auth.set(new Data(token, info));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.logout();
|
||||||
|
auth.set(new Data());
|
||||||
|
goto('/');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const auth = writable<Data>(new Data(token));
|
||||||
|
|
||||||
|
auth.subscribe(async (newAuth) => {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
// user is already set
|
||||||
|
if (newAuth.info || !newAuth.token) return;
|
||||||
|
|
||||||
|
if (!newAuth.token) {
|
||||||
|
newAuth.logout();
|
||||||
|
auth.set(new Data());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// token just got added
|
||||||
|
window.localStorage.setItem('authToken', newAuth.token);
|
||||||
|
|
||||||
|
UserService.Info({}, withToken(newAuth.token))
|
||||||
|
.then((info) => {
|
||||||
|
auth.set(new Data(newAuth.token, info));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
newAuth.logout();
|
||||||
|
auth.set(new Data());
|
||||||
|
goto('/');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,16 @@
|
|||||||
|
<script>
|
||||||
|
import '../app.css';
|
||||||
|
import Modal from 'svelte-simple-modal';
|
||||||
|
import Navbar from '$lib/components/Navbar.svelte';
|
||||||
|
import Footer from '$lib/components/Footer.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<div class="bg-indigo-50">
|
||||||
|
<Navbar />
|
||||||
|
<div class="mx-auto mt-6 relative min-h-[calc(100vh-164px)]">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
@ -0,0 +1,17 @@
|
|||||||
|
<script>
|
||||||
|
import '../app.css';
|
||||||
|
import Modal from 'svelte-simple-modal';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<slot />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,5 +1,14 @@
|
|||||||
<script>
|
<script>
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
|
import Modal from 'svelte-simple-modal';
|
||||||
|
import Navbar from '$lib/components/Navbar.svelte';
|
||||||
|
import Footer from '$lib/components/Footer.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<Navbar />
|
||||||
|
<div class="mx-auto mt-6 relative min-h-[80vh]">
|
||||||
<slot />
|
<slot />
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</Modal>
|
||||||
|
@ -0,0 +1,125 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SvelteMarkdown from 'svelte-markdown';
|
||||||
|
|
||||||
|
const about = `
|
||||||
|
### About
|
||||||
|
|
||||||
|
CodeQuest is a programming contest that was designed and created as a CAS
|
||||||
|
project. The project was primarily created by [Hamza
|
||||||
|
Ali](https://github.com/hhhapz), Smriti Srinivasan, Iona Campbell and Aaliyah
|
||||||
|
Aman. The development of this website and the design of the puzzles was done
|
||||||
|
by Hamza Ali.
|
||||||
|
|
||||||
|
**CodeQuest** is designed to be a programming competition that is approachable
|
||||||
|
by all programmers. The puzzles are designed to be able to be solved using
|
||||||
|
any programming language you like, and can use any method you wish. The first
|
||||||
|
two problems designed for this competition were able to be solved without any
|
||||||
|
programming at all, just using excel spreadsheets and WolframAlpha.
|
||||||
|
|
||||||
|
This contest was heavily inspired by the [Advent of
|
||||||
|
Code](https://adventofcode.com), a competition I have been participating in
|
||||||
|
yearly since 2017. I hope to be able to continue the ethos and the passion for
|
||||||
|
programming that the advent of code gave me through this competition.
|
||||||
|
|
||||||
|
If you have any questions, feel free to reach out at hello@<this website's
|
||||||
|
domain>.
|
||||||
|
|
||||||
|
### How Do I Begin?
|
||||||
|
|
||||||
|
If you are looking at the calendar, and can see that the current time is after
|
||||||
|
the 29th of April 2022, that most likely means that the official competition is
|
||||||
|
already over, and you will not be able to participate for prizes.
|
||||||
|
|
||||||
|
However, if you register an account, you should still be able to see all of the
|
||||||
|
puzzles as well as try to solve them. Each puzzle is split into 2 different
|
||||||
|
parts, the first part is generally a simpler version of the second.
|
||||||
|
|
||||||
|
Once you solve a puzzle, you simply need to submit the final answer. Your code
|
||||||
|
nor the method you took to get the answer are checked, however each user gets a
|
||||||
|
unique input and the answer they will obtain is specific to them.
|
||||||
|
|
||||||
|
### General Tips and Advice
|
||||||
|
|
||||||
|
#### I'm stuck - what do I do?
|
||||||
|
|
||||||
|
Each puzzle will come with at least one example. Does your answer for the
|
||||||
|
example input match? Make sure you fully understand what the puzzle is asking.
|
||||||
|
One common mistake is that while copying the input, some of it is left over.
|
||||||
|
Double check you have the full input.
|
||||||
|
|
||||||
|
Try building your own examples and solving them, see if your program does as
|
||||||
|
you expected. If you're still having difficulties, try solving a different
|
||||||
|
problem and circling back.
|
||||||
|
|
||||||
|
If you still have difficulties and the contest is finished, try asking a friend
|
||||||
|
for assistance, see if they can help you understand what's going on.
|
||||||
|
|
||||||
|
#### I'm programming in Java, how do I read the user input in my program?
|
||||||
|
|
||||||
|
Unlike in other programming languages, where you have strings with new lines in
|
||||||
|
them, Java makes that a little bit more difficult to do.
|
||||||
|
|
||||||
|
For example, in Python you can do:
|
||||||
|
|
||||||
|
\`\`\`py
|
||||||
|
input = """this is
|
||||||
|
a multi line
|
||||||
|
string literal"""
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
and in JavaScript, you can do:
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
const input = \`this is
|
||||||
|
a multi line
|
||||||
|
string literal\`
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
In Java you have two options:
|
||||||
|
|
||||||
|
1. Keep reading from your Scanner until you reach an empty line.
|
||||||
|
2. Read input from a file
|
||||||
|
|
||||||
|
(These solutions will work in other languages too!).
|
||||||
|
|
||||||
|
Here is an example for the first option:
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
Scanner sc = new Scanner(System.in);
|
||||||
|
|
||||||
|
/* either */
|
||||||
|
String input = "";
|
||||||
|
while(true) {
|
||||||
|
String line = sc.nextLine(); // Get the next line of user input.
|
||||||
|
if (line.equals("") { // If the line is empty, we have read everything.
|
||||||
|
break; // Jump out of the while loop.
|
||||||
|
}
|
||||||
|
input += line + "\n"; // If not, add the input line to the input string.
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
Scanner sc = new Scanner(System.in);
|
||||||
|
ArrayList<String> input = new ArrayList<>();
|
||||||
|
while(true) {
|
||||||
|
String line = sc.nextLine(); // Get the next line of user input.
|
||||||
|
if (line.equals("") { // If the line is empty, we have read everything.
|
||||||
|
break; // Jump out of the while loop.
|
||||||
|
}
|
||||||
|
input.add(line); // If not, add the line to the input array list.
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>CodeQuest</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<main class="min-h-screen">
|
||||||
|
<section class="mx-auto py-[7vw] prose prose-lg">
|
||||||
|
<SvelteMarkdown source={about} />
|
||||||
|
</section>
|
||||||
|
</main>
|
@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
export const router = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { AuthService } from '$lib/pb/all.pb';
|
||||||
|
import { noToken } from '$lib/pb/pbutil';
|
||||||
|
import { GoogleSpin } from 'svelte-loading-spinners';
|
||||||
|
import { auth } from '$lib/stores';
|
||||||
|
|
||||||
|
const perform = (code, state) => {
|
||||||
|
AuthService.Token({ state: state, code: code }, noToken())
|
||||||
|
.then((res) => {
|
||||||
|
$auth.login(res.token);
|
||||||
|
window.location.href = '/dashboard';
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
goto('/');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const query = $page.url.searchParams;
|
||||||
|
const code = query.get('code'),
|
||||||
|
state = query.get('state');
|
||||||
|
perform(code, state);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Authenticating... | CodeQuest</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute -top-10 left-0 w-full h-full flex flex-col gap-4 justify-center items-center pointer-events-none bg-transparent"
|
||||||
|
>
|
||||||
|
<GoogleSpin size="48px" duration="3s" />
|
||||||
|
<h1 class="text-2xl font-bold">Processing</h1>
|
||||||
|
</div>
|
@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
import { auth } from '$lib/stores';
|
||||||
|
import LoginModal from '$lib/components/LoginModal.svelte';
|
||||||
|
|
||||||
|
const { open } = getContext('simple-modal');
|
||||||
|
|
||||||
|
const goHome = () => {
|
||||||
|
open(LoginModal);
|
||||||
|
window.location.href = '/';
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
auth.subscribe((auth) => {
|
||||||
|
if (!auth.loggedIn()) {
|
||||||
|
goHome();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
@ -0,0 +1,109 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
export const prerender = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
import Countdown from '$lib/components/Countdown.svelte';
|
||||||
|
import { auth } from '$lib/stores';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
$auth.refresh();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head><title>Dashboard | CodeQuest</title></svelte:head>
|
||||||
|
|
||||||
|
<div class="relative flex min-h-[calc(100vh-164px)] pb-8">
|
||||||
|
<main class="container mx-auto pt-4">
|
||||||
|
<div class="p-4 text-white bg-indigo-300 rounded-md shadow-md">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<span class="text-3xl font-semibold tracking-wider uppercase">
|
||||||
|
{#if !$auth.info}
|
||||||
|
|
||||||
|
{:else if $auth.info?.active}
|
||||||
|
<Countdown end={$auth.info.endTime} />
|
||||||
|
{:else if new Date($auth.info.endTime) < new Date()}
|
||||||
|
Contest Over
|
||||||
|
{:else}
|
||||||
|
Starting Soon
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 gap-6 mt-4 md:grid-cols-2">
|
||||||
|
<div
|
||||||
|
class="flex justify-between w-full h-32 bg-white rounded-md shadow-md p-6 pb-2 border border-indigo-400"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-xs text-indigo-400 uppercase">Welcome</span>
|
||||||
|
<span class="text-xl text-indigo-500 font-bold">
|
||||||
|
{$auth.info?.currentUser.name || ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-end justify-end">
|
||||||
|
<span class="text-xs text-purple-400 uppercase">Points</span>
|
||||||
|
<span class="text-purple-700">
|
||||||
|
{$auth.info?.points || 0}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="box h-32 border-sky-600 hover:bg-sky-50" href="/dashboard/leaderboard">
|
||||||
|
<span class="txt text-sky-600">
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M22,7H16.333V4a1,1,0,0,0-1-1H8.667a1,1,0,0,0-1,1v7H2a1,1,0,0,0-1,1v8a1,1,0,0,0,1,1H22a1,1,0,0,0,1-1V8A1,1,0,0,0,22,7ZM7.667,19H3V13H7.667ZM14.333,8V19H9.667V5h4.666ZM21,19H16.333V9H21Z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
Leaderboard
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<div class="box h-32 border-green-600 hover:bg-green-50">
|
||||||
|
<span class="txt text-green-700">
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
Personal Stats
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<a class="box h-32 border-red-700 hover:bg-red-50" href="/about">
|
||||||
|
<span class="txt text-red-700">
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||||
|
</svg>
|
||||||
|
About
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<a class="grid grid-cols-1 gap-6 my-4 mt-4" href="/dashboard/questions">
|
||||||
|
<div class="box h-56 border-2 border-indigo-700 hover:bg-indigo-50">
|
||||||
|
<span class="txt txt-lg text-4xl text-indigo-700">
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg viewBox="0 0 24 24" class="h-9 w-9 mt-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g><path d="M20,8V3a1,1,0,0,0-1-1H13a1,1,0,0,0-1,1V4A2,2,0,0,1,8,4V3A1,1,0,0,0,7,2H1A1,1,0,0,0,0,3V21a1,1,0,0,0,1,1H7a1,1,0,0,0,1-1V20a2,2,0,0,1,4,0v1a1,1,0,0,0,1,1h6a1,1,0,0,0,1-1V16a4,4,0,0,0,0-8Zm0,6H19a1,1,0,0,0-1,1v5H14a4,4,0,0,0-8,0H2V4H6a4,4,0,0,0,8,0h4V9a1,1,0,0,0,1,1h1a2,2,0,0,1,0,4Z"/></g>
|
||||||
|
</svg>
|
||||||
|
Puzzles
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.box {
|
||||||
|
@apply flex items-center justify-center w-full rounded-md shadow-md border;
|
||||||
|
@apply transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt {
|
||||||
|
@apply inline-flex items-center gap-2;
|
||||||
|
@apply text-xl font-bold tracking-wider uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt-lg {
|
||||||
|
@apply text-4xl;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,129 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
export const prerender = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
import { type LeaderboardResponse, QuestService } from '$lib/pb/all.pb';
|
||||||
|
import { withToken } from '$lib/pb/pbutil';
|
||||||
|
import Countdown from '$lib/components/Countdown.svelte';
|
||||||
|
import { auth } from '$lib/stores';
|
||||||
|
import ErrorModal from '$lib/components/ErrorModal.svelte';
|
||||||
|
|
||||||
|
const { open } = getContext('simple-modal');
|
||||||
|
|
||||||
|
let ranking: LeaderboardResponse = null;
|
||||||
|
|
||||||
|
let queried = false;
|
||||||
|
onMount(() => {
|
||||||
|
auth.subscribe((data) => {
|
||||||
|
if (queried) return;
|
||||||
|
if (!data.loggedIn()) return;
|
||||||
|
queried = true;
|
||||||
|
|
||||||
|
QuestService.Leaderboard({}, withToken(data.token))
|
||||||
|
.then((lr) => {
|
||||||
|
ranking = lr;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
open(ErrorModal, {
|
||||||
|
title: 'Could not fetch',
|
||||||
|
reason: err.message || 'Something went wrong',
|
||||||
|
btn: {
|
||||||
|
title: 'Go to dashboard',
|
||||||
|
do: () => {
|
||||||
|
goto('/dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Questions | CodeQuest</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="relative flex min-h-[calc(100vh-164px)] pb-8 mx-2">
|
||||||
|
<main class="container mx-auto pt-4 flex flex-col gap-4">
|
||||||
|
<div class="grid grid-cols-1 gap-4">
|
||||||
|
<div class="box h-24 border-2 border-sky-600 hover:bg-sky-50">
|
||||||
|
<span class="font-bold text-4xl text-sky-600">Leaderboard</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-3 xl:grid-cols-5">
|
||||||
|
<a class="box h-12 hover:bg-neutral-200 border-neutral-300" href="/dashboard">
|
||||||
|
<span class="txt text-neutral-600 flex items-center gap-1">
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
|
</svg>
|
||||||
|
Dashboard
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="box h-12 border-indigo-600 col-span-1 md:col-end-4 xl:col-end-6">
|
||||||
|
<span class="txt text-indigo-600">
|
||||||
|
{#if !$auth.info}
|
||||||
|
|
||||||
|
{:else if $auth.info?.active}
|
||||||
|
<Countdown end={String($auth.info.endTime)} />
|
||||||
|
{:else if new Date(String($auth.info.endTime)) < new Date()}
|
||||||
|
Contest Over
|
||||||
|
{:else}
|
||||||
|
Starting Soon
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||||
|
{#if ranking}
|
||||||
|
{#each ranking.leaderboard as item, i}
|
||||||
|
<div class="ranking h-24 xl:col-span-2">
|
||||||
|
<div class="inline-flex items-end gap-2">
|
||||||
|
<span class="txt text-4xl">{i + 1}.</span>
|
||||||
|
<span class="txt text-2xl">{item.username}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="txt text-2xl">{item.points} points</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.box {
|
||||||
|
@apply flex items-center justify-center w-full rounded-md shadow-md border;
|
||||||
|
@apply transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking {
|
||||||
|
@apply flex items-center justify-between w-full rounded-md shadow-md border;
|
||||||
|
@apply px-8;
|
||||||
|
@apply transition;
|
||||||
|
@apply text-gray-400 border-gray-400 hover:bg-sky-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking:nth-child(1) {
|
||||||
|
@apply text-yellow-500 border-yellow-500 hover:bg-amber-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking:nth-child(2) {
|
||||||
|
@apply text-zinc-700 border-zinc-700 hover:bg-zinc-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking:nth-child(3) {
|
||||||
|
@apply text-amber-600 border-amber-600 hover:bg-amber-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt {
|
||||||
|
@apply font-bold tracking-wider uppercase;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
import { QuestService } from '$lib/pb/all.pb';
|
||||||
|
import { withToken } from '$lib/pb/pbutil';
|
||||||
|
import { auth } from '$lib/stores';
|
||||||
|
import ErrorModal from '$lib/components/ErrorModal.svelte';
|
||||||
|
|
||||||
|
type Status = 'available' | 'partial' | 'full';
|
||||||
|
|
||||||
|
const { open } = getContext('simple-modal');
|
||||||
|
|
||||||
|
let id = $page.params.id;
|
||||||
|
let input = 'Loading...';
|
||||||
|
|
||||||
|
let queried = false;
|
||||||
|
onMount(() => {
|
||||||
|
auth.subscribe((data) => {
|
||||||
|
if (queried) return;
|
||||||
|
if (!data.loggedIn()) return;
|
||||||
|
queried = true;
|
||||||
|
|
||||||
|
QuestService.QuestionInput({ id }, withToken(data.token))
|
||||||
|
.then((inp) => {
|
||||||
|
input = inp.input;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
open(ErrorModal, {
|
||||||
|
title: 'Could not load',
|
||||||
|
reason: err.message || 'Something went wrong',
|
||||||
|
btn: {
|
||||||
|
title: 'Go to questions',
|
||||||
|
do: () => {
|
||||||
|
goto('/dashboard/questions');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let value = '';
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
if (value == '') {
|
||||||
|
open(ErrorModal, { title: 'Could not Submit', reason: 'Please provide a value to submit' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<pre class="font-mono text-sm">{input}</pre>
|
@ -0,0 +1,224 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SvelteMarkdown from 'svelte-markdown';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
import { type Question, QuestService } from '$lib/pb/all.pb';
|
||||||
|
import { withToken } from '$lib/pb/pbutil';
|
||||||
|
import { auth } from '$lib/stores';
|
||||||
|
import ErrorModal from '$lib/components/ErrorModal.svelte';
|
||||||
|
import SubmitModal from '$lib/components/SubmitModal.svelte';
|
||||||
|
|
||||||
|
type Status = 'available' | 'partial' | 'full';
|
||||||
|
|
||||||
|
const { open } = getContext('simple-modal');
|
||||||
|
|
||||||
|
let id = $page.params.id;
|
||||||
|
|
||||||
|
let q: Question;
|
||||||
|
|
||||||
|
let status: Status = 'available';
|
||||||
|
let solved = 0;
|
||||||
|
$: {
|
||||||
|
if (q?.part1?.completed && q?.part2?.completed) {
|
||||||
|
status = 'full';
|
||||||
|
solved = 2;
|
||||||
|
} else if (q?.part1?.completed || q?.part2?.completed) {
|
||||||
|
status = 'partial';
|
||||||
|
solved = 1;
|
||||||
|
} else {
|
||||||
|
status = 'available';
|
||||||
|
solved = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadQuestion = () => {
|
||||||
|
QuestService.QuestionByID({ id }, withToken($auth.token))
|
||||||
|
.then((question) => {
|
||||||
|
q = question;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
open(ErrorModal, {
|
||||||
|
title: 'Could not load',
|
||||||
|
reason: err.message || 'Something went wrong',
|
||||||
|
btn: {
|
||||||
|
title: 'Go to questions',
|
||||||
|
do: () => {
|
||||||
|
goto('/dashboard/questions');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let queried = false;
|
||||||
|
onMount(() => {
|
||||||
|
auth.subscribe(() => {
|
||||||
|
if (!queried && $auth.loggedIn()) {
|
||||||
|
queried = true;
|
||||||
|
loadQuestion();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let value = '';
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
if (value == '') {
|
||||||
|
open(ErrorModal, { title: 'Could not Submit', reason: 'Please provide a value to submit' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let part = 1;
|
||||||
|
switch (solved) {
|
||||||
|
case 0:
|
||||||
|
part = 1;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
part = 2;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
QuestService.Submit({ id, Body: { answer: value, code: '' }, part }, withToken($auth.token))
|
||||||
|
.then((sr) => {
|
||||||
|
open(SubmitModal, { ...sr, part });
|
||||||
|
value = '';
|
||||||
|
if (sr.correct) {
|
||||||
|
$auth.info.points += sr.points;
|
||||||
|
}
|
||||||
|
loadQuestion();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
open(ErrorModal, {
|
||||||
|
reason: err.message || 'Something went wrong'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{q?.title || ''} | CodeQuest</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
{#if q}
|
||||||
|
<div class="relative flex min-h-[calc(100vh-164px)] pb-8 mx-2">
|
||||||
|
<main class="container mx-auto pt-4 flex flex-col gap-4">
|
||||||
|
<div class="grid grid-cols-1 gap-4">
|
||||||
|
<div class={`box h-24 ${status}`}>
|
||||||
|
<span class="font-bold text-4xl">{q.title}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-4 xl:grid-cols-5">
|
||||||
|
<a class="box h-12 hover:bg-neutral-200 border-neutral-300" href="/dashboard/questions">
|
||||||
|
<span class="txt text-neutral-600 flex items-center gap-1">
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
|
</svg>
|
||||||
|
Puzzles
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="box h-12 border-indigo-600 md:col-span-2 md:col-end-5 xl:col-end-6">
|
||||||
|
<div class="txt text-indigo-600 flex px-4 w-full items-center justify-between">
|
||||||
|
<span>
|
||||||
|
Progress: {solved}/2
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Points Earned: {q.part1.pointsWorth + q.part2.pointsWorth}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-5 gap-4">
|
||||||
|
<div
|
||||||
|
class="prose text-justify md:col-span-1 lg:col-span-3 md:justify-self-center lg:justify-self-start"
|
||||||
|
>
|
||||||
|
<SvelteMarkdown source={q.text} />
|
||||||
|
</div>
|
||||||
|
{#if solved != 2}
|
||||||
|
<div class="submit h-[600px]">
|
||||||
|
<a
|
||||||
|
class="btn ghost mb-2 py-2 rounded-md"
|
||||||
|
href={`/dashboard/question/${id}/input`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
View Puzzle Input
|
||||||
|
</a>
|
||||||
|
<form class="flex gap-4 h-12" on:submit|preventDefault={submit}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value
|
||||||
|
placeholder={`Answer Part ${solved + 1}`}
|
||||||
|
class="rounded-md shadow-md border w-3/4 font-mono"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
value="Submit"
|
||||||
|
class="btn ghost rounded-md shadow-md w-1/4 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="submit h-[600px]">
|
||||||
|
<a
|
||||||
|
class="btn ghost mb-2 py-2 rounded-md"
|
||||||
|
href={`/dashboard/question/${id}/input`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
View Puzzle Input
|
||||||
|
</a>
|
||||||
|
<form class="flex gap-4 h-12">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
disabled
|
||||||
|
value={'Puzzle Solved'}
|
||||||
|
class="rounded-md shadow-md border font-mono bg-neutral-300 w-full"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.box {
|
||||||
|
@apply flex items-center justify-center w-full rounded-md shadow-md border;
|
||||||
|
@apply transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available {
|
||||||
|
@apply border-gray-400 hover:bg-gray-100;
|
||||||
|
@apply text-gray-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partial {
|
||||||
|
@apply border-yellow-400 bg-yellow-50 hover:bg-yellow-100;
|
||||||
|
@apply text-yellow-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full {
|
||||||
|
@apply border-green-400 bg-green-50 hover:bg-green-100;
|
||||||
|
@apply text-green-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt {
|
||||||
|
@apply font-bold tracking-wider uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question {
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit {
|
||||||
|
@apply w-3/4 md:w-1/2 lg:w-3/4 sticky top-12 mt-8;
|
||||||
|
@apply md:justify-self-center xl:justify-self-end;
|
||||||
|
@apply lg:col-span-2 lg:col-end-6;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,104 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
import * as pb from '$lib/pb/all.pb';
|
||||||
|
import { withToken } from '$lib/pb/pbutil';
|
||||||
|
import Countdown from '$lib/components/Countdown.svelte';
|
||||||
|
import { auth } from '$lib/stores';
|
||||||
|
import QuestionListing from '$lib/components/QuestionListing.svelte';
|
||||||
|
import ErrorModal from '$lib/components/ErrorModal.svelte';
|
||||||
|
|
||||||
|
const { open } = getContext('simple-modal');
|
||||||
|
|
||||||
|
let questions: pb.Question[] = null;
|
||||||
|
|
||||||
|
let queried = false;
|
||||||
|
onMount(() => {
|
||||||
|
auth.subscribe((data) => {
|
||||||
|
if (queried) return;
|
||||||
|
if (!data.loggedIn()) return;
|
||||||
|
queried = true;
|
||||||
|
|
||||||
|
pb.QuestService.Questions({}, withToken(data.token))
|
||||||
|
.then((qs) => {
|
||||||
|
questions = qs.questions;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
questions = [];
|
||||||
|
open(ErrorModal, {
|
||||||
|
title: 'Could not fetch',
|
||||||
|
reason: err.message || 'Something went wrong',
|
||||||
|
btn: {
|
||||||
|
title: 'Go to dashboard',
|
||||||
|
do: () => {
|
||||||
|
goto('/dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Puzzles | CodeQuest</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="relative flex min-h-[calc(100vh-164px)] pb-8 mx-2">
|
||||||
|
<main class="container mx-auto pt-4 flex flex-col gap-4">
|
||||||
|
<div class="grid grid-cols-1 gap-4">
|
||||||
|
<div class="box h-24 border-2 border-indigo-700 hover:bg-indigo-50">
|
||||||
|
<span class="font-bold text-4xl text-indigo-700">Puzzles</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-3 xl:grid-cols-5">
|
||||||
|
<a class="box h-12 hover:bg-neutral-200 border-neutral-300" href="/dashboard">
|
||||||
|
<span class="txt text-neutral-600 flex items-center gap-1">
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
|
</svg>
|
||||||
|
Dashboard
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="box h-12 border-indigo-600 col-span-1 md:col-end-4 xl:col-end-6">
|
||||||
|
<span class="txt text-indigo-600">
|
||||||
|
{#if !$auth.info}
|
||||||
|
|
||||||
|
{:else if $auth.info?.active}
|
||||||
|
<Countdown end={String($auth.info.endTime)} />
|
||||||
|
{:else if new Date(String($auth.info.endTime)) < new Date()}
|
||||||
|
Contest Over
|
||||||
|
{:else}
|
||||||
|
Starting Soon
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||||
|
{#if $auth.info?.active}
|
||||||
|
{#each questions || [] as question}
|
||||||
|
<QuestionListing q={question} />
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<dev class="box h-36 hover:bg-red-50 border-red-300 xl:col-span-2">
|
||||||
|
<span class="txt text-2xl text-red-600">The Contest is not Currently Active</span>
|
||||||
|
</dev>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.box {
|
||||||
|
@apply flex items-center justify-center w-full rounded-md shadow-md border;
|
||||||
|
@apply cursor-pointer transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt {
|
||||||
|
@apply font-bold tracking-wider uppercase;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,2 +1,176 @@
|
|||||||
<h1>Welcome to SvelteKit</h1>
|
<script lang="ts">
|
||||||
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
|
import GoogleButton from '$lib/components/GoogleButton.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>CodeQuest</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="relative overflow-hidden min-h-[70vh]">
|
||||||
|
<div class="max-w-7xl mx-auto">
|
||||||
|
<div
|
||||||
|
class="relative z-10 pb-8 bg-white sm:pb-16 md:pb-20 lg:max-w-lg lg:w-full lg:pb-28 xl:max-w-xl xl:pb-32 "
|
||||||
|
>
|
||||||
|
<main
|
||||||
|
class="mt-10 mx-auto max-w-7xl px-4 sm:mt-12 sm:px-6 md:mt-16 lg:mt-20 lg:px-8 xl:mt-28"
|
||||||
|
>
|
||||||
|
<div class="sm:text-center lg:text-left">
|
||||||
|
<h1 class="text-4xl tracking-tight font-extrabold text-gray-900 sm:text-5xl">
|
||||||
|
<div class="hidden lg:block">
|
||||||
|
<span class="block">Solve Some Problems</span>
|
||||||
|
<span class="block text-indigo-600">Hone Your Skills</span>
|
||||||
|
</div>
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
class="mt-3 text-base text-gray-500 text-center lg:text-left
|
||||||
|
sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-lg lg:mx-0"
|
||||||
|
>
|
||||||
|
CodeQuest is a challenge of 10 different programming puzzles.
|
||||||
|
<br />
|
||||||
|
Designed to be open and accessible to all.
|
||||||
|
<br />
|
||||||
|
Open to <i>any</i> language and technology you like.
|
||||||
|
</p>
|
||||||
|
<div class="mt-5 sm:mt-8 sm:flex sm:justify-center lg:justify-start">
|
||||||
|
<div class="rounded-md shadow w-48 md:w-auto mx-auto md:m-0">
|
||||||
|
<a href="#details" class="btn px-8 py-3 md:px-10 md:py-4 text-lg rounded">
|
||||||
|
Learn More
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lg:absolute lg:inset-y-0 lg:right-3 lg:w-1/2">
|
||||||
|
<img
|
||||||
|
class="h-56 w-full object-fit sm:h-72 md:h-96 lg:w-full lg:h-full"
|
||||||
|
src="/undraw_code_inspection.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="slant bg-indigo-200">
|
||||||
|
<div class="wrapper">
|
||||||
|
<div
|
||||||
|
id="details"
|
||||||
|
class="container mx-auto md:min-h-[40vh] p-12 md:p-0 md:flex items-center justify-evenly flex-row-reverse gap-12 lg:gap-8"
|
||||||
|
>
|
||||||
|
<div class="mt-8 md:mt-8 w-full md:max-w-sm lg:max-w-lg">
|
||||||
|
<div class="text-center md:text-left">
|
||||||
|
<h1 class=" tracking-tight font-semibold text-gray-900 text-3xl flex items-center">
|
||||||
|
How it Works
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 mt-[5px] ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||||
|
</svg>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<p class="lg:text-lg font-medium mt-2">
|
||||||
|
CodeQuest is not like most other programming competitions. Instead, it is heavily inspired
|
||||||
|
by the <a href="https://adventofcode.com">Advent of Code </a>. Instead of submitting code
|
||||||
|
which will be tested and evaluated by CodeQuest, <i>only your final answers</i> will be
|
||||||
|
validated to an input you can inspect and verify.
|
||||||
|
<br /> <br />
|
||||||
|
This is an opportunity for you to use tools that
|
||||||
|
<strong>you enjoy</strong>, and demonstrate your skills and expertise.
|
||||||
|
</p>
|
||||||
|
<a class="flex items-center mt-2" href="/about">
|
||||||
|
Learn More
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mt-[2px]" viewBox="0 0 20 20" fill="currentColor"> <path fill-rule="evenodd" d="M10.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L12.586 11H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd" /> </svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="mt-16 md:mt-8">
|
||||||
|
<img
|
||||||
|
class="w-96 object-fit md:w-96 lg:h-80 lg:h-full lg:max-w-lg md:scale-x-[-1]"
|
||||||
|
src="/undraw_thought_process.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="bg-indigo-100 pt-[7vw]">
|
||||||
|
<div
|
||||||
|
class="container mx-auto lg:min-h-[40vh] p-12 md:p-0 lg:flex items-end justify-evenly gap-12 lg:gap-8"
|
||||||
|
>
|
||||||
|
<div class="mt-8 w-full md:max-w-lg mx-auto lg:mx-0 lg:max-w-lg">
|
||||||
|
<div class="text-center lg:text-left">
|
||||||
|
<h1
|
||||||
|
class=" tracking-tight font-semibold text-gray-900 text-3xl flex items-center flex gap-2"
|
||||||
|
>
|
||||||
|
Details
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 mt-.5 ml-1" viewBox="0 0 20 20" fill="currentColor"> <path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" /> <path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" /> </svg>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<p class="lg:text-lg font-medium mt-2">
|
||||||
|
<strong>CodeQuest</strong> will be hosted on Friday, April 29 in F-201. We highly encourage everyone
|
||||||
|
to join. The entire competition will take place over approximately 2 hours, where we will spend
|
||||||
|
10-15 minutes preparing and geting ready, before the competition of 90 minutes will start. Please
|
||||||
|
try to get here by 15:15 so we can finish by 5pm!
|
||||||
|
</p>
|
||||||
|
<a class="flex items-center mt-2" href="/#">
|
||||||
|
Calendar Invite
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mt-[2px]" viewBox="0 0 20 20" fill="currentColor"> <path fill-rule="evenodd" d="M10.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L12.586 11H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd" /> </svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 w-full md:max-w-lg mx-auto lg:mx-0 lg:max-w-lg">
|
||||||
|
<div class="text-center lg:text-left">
|
||||||
|
<h1
|
||||||
|
class="tracking-tight font-semibold text-gray-900 text-3xl flex items-center gap flex-2"
|
||||||
|
>
|
||||||
|
Contest Prizes
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 mt-.5 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /> </svg>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<p class="lg:text-lg font-medium mt-2">
|
||||||
|
While we encourage everyone to join and have fun, prizes will be awarded. We want to
|
||||||
|
recognize, promote, and encourage students who have cultivated their programming skills both
|
||||||
|
within class, and outside of school.
|
||||||
|
<br />
|
||||||
|
</p>
|
||||||
|
<div class="mt-2">
|
||||||
|
<strong>Top 3 Performance Prizes</strong>
|
||||||
|
<ol class="list-decimal font-medium ml-6">
|
||||||
|
<li>IDR 350,000</li>
|
||||||
|
<li>IDR 125,000</li>
|
||||||
|
<li>IDR 75,000</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<a class="flex items-center mt-2" href="/about">
|
||||||
|
Learn More
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mt-[2px]" viewBox="0 0 20 20" fill="currentColor" > <path fill-rule="evenodd" d="M10.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L12.586 11H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd" /> </svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-indigo-400 border-2 max-w-xs mx-auto my-12 lg:my-24" />
|
||||||
|
|
||||||
|
<div class="mt-8 md:mt-8 w-full md:max-w-sm lg:max-w-lg mx-auto">
|
||||||
|
<h1 class="text-center tracking-tight font-semibold text-gray-900 text-2xl">Join Now</h1>
|
||||||
|
<p class="mt-2 pb-8 text-center">
|
||||||
|
Log in with your JIS Email and join CodeQuest!
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<GoogleButton />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.slant {
|
||||||
|
clip-path: polygon(0 7vw, 0 100%, 100% calc(100% - 7vw), 100% 0);
|
||||||
|
margin: 0 0 -7vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slant .wrapper {
|
||||||
|
padding: 9vw 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
export const router = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
import { auth } from '$lib/stores';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
$auth.logout();
|
||||||
|
window.location.href = '/';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Logging out... | CodeQuest</title>
|
||||||
|
</svelte:head>
|