[+] Upload to server
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/svelte": "^4.1.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"sass-embedded": "^1.83.0",
|
||||
|
||||
+22
-8
@@ -2,15 +2,17 @@
|
||||
import svelteLogo from './assets/svelte.svg'
|
||||
import viteLogo from '/vite.svg'
|
||||
import Number from './lib/Number.svelte'
|
||||
import { cfg, eStates, Fmt, JsonTy, Misc, nStates, randInt, range, zero8, type Checkpoint, type i8s } from "./utils";
|
||||
import { Backend, cfg, eStates, Fmt, JsonTy, Misc, nStates, randInt, range, zero8, type Checkpoint, type i8s, type MetaCheckpoint } from "./utils";
|
||||
import Line from "./lib/Line.svelte";
|
||||
import { solve } from "./solver";
|
||||
import Upload from './lib/Upload.svelte';
|
||||
import PuzzleInfo from './lib/PuzzleInfo.svelte';
|
||||
|
||||
const params = new URLSearchParams(location.search)
|
||||
const hasTouch = Misc.hasTouch()
|
||||
|
||||
// Can pass in puzzle data from props
|
||||
interface Props { puzzleId?: string, puzzleData?: Checkpoint }
|
||||
interface Props { puzzleId?: string, puzzleData?: MetaCheckpoint }
|
||||
export let { puzzleId, puzzleData }: Props = {}
|
||||
const pid = puzzleId ?? 'slitherlink'
|
||||
|
||||
@@ -53,6 +55,7 @@
|
||||
colors = pt.colors
|
||||
updateColors()
|
||||
}
|
||||
let upload: Checkpoint | null
|
||||
|
||||
// Run something on the edges of a cell
|
||||
function updateEdges(x: number, y: number, condition: (st: number) => boolean, op: (st: number) => number) {
|
||||
@@ -198,7 +201,7 @@
|
||||
updateArea.forEach(([dx, dy, _]) => checkPos(x + dx, y + dy))
|
||||
}
|
||||
|
||||
async function editModeReduce(zeros: boolean = false, n = 20) {
|
||||
async function editModeReduce(zeros: boolean = false, n = 10) {
|
||||
[solutionHStates, solutionVStates] = [hStates.slice(), vStates.slice()]
|
||||
if (n === 0) return
|
||||
// On each iteration, randomly mask n numbers and try to solve the puzzle
|
||||
@@ -221,8 +224,9 @@
|
||||
vertStates.some((st, idx) => (st === eStates.selected) !== (solutionVStates[idx] === eStates.selected))) {
|
||||
masked.forEach(idx => nMask[idx] = 0)
|
||||
console.log('Solution does not match')
|
||||
setTimeout(() => editModeReduce(zeros, n - 1), 100)
|
||||
} else setTimeout(() => editModeReduce(zeros, n), 100)
|
||||
}
|
||||
// setTimeout(() => editModeReduce(zeros, n - 1), 100)
|
||||
// } else setTimeout(() => editModeReduce(zeros, n), 100)
|
||||
console.log(nMask)
|
||||
}
|
||||
|
||||
@@ -284,14 +288,18 @@
|
||||
<img src={svelteLogo} alt="Logo"/>
|
||||
</div>
|
||||
<div class="rules">
|
||||
Welcome to SlitherLink! 🧩 The rules are simple: Draw lines between the dots to create one big loop (no crossings, no branches). The numbers are your hints – they tell you how many lines should surround them. Left-click to draw, right-click to mark with an X. Can you crack the perfect path?
|
||||
Welcome to SlitherLink! 🧩 The rules are simple: Draw lines between the dots to create one big loop (no crossings, no branches). The numbers are your hints – they tell you how many lines should surround them. Left-click to draw, right-click to mark with an X. Can you crack the perfect path? <a href="https://www.youtube.com/watch?v=fqwE-CpeGS4&list=PLH_elo2OIwaBk44COgFhnJRt0-8xKMBQn">YouTube Tutorial</a>
|
||||
</div>
|
||||
|
||||
{#if statusMsg}
|
||||
<div class="status" class:error={!complete}>{statusMsg}</div>
|
||||
{/if}
|
||||
|
||||
{#if !editMode}<div class="timer">{Fmt.duration(elapsed)}</div>{/if}
|
||||
<div>
|
||||
{#if puzzleData && !editMode}<div class="timer-line">
|
||||
<PuzzleInfo {puzzleData}></PuzzleInfo>
|
||||
<div class="timer">{Fmt.duration(elapsed)}</div>
|
||||
</div>{/if}
|
||||
<div class="puzzle-grid" style={`height: ${rows * cfg.totalW}px; width: ${cols * cfg.totalW}px;`}
|
||||
on:click={clickDiv} on:contextmenu={clickDiv} on:keypress={console.log} role="grid" tabindex="0"
|
||||
on:mousedown={startDrag} on:mousemove={e => dragging && clickDiv(e)} on:mouseup={() => dragging = false}
|
||||
@@ -313,6 +321,7 @@
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if editMode}
|
||||
<div class="btn-div">
|
||||
@@ -338,7 +347,6 @@
|
||||
}}>Clear Numbers</button>
|
||||
|
||||
<button on:click={() => genNumbers()}>Gen Numbers</button>
|
||||
<button on:click={() => JsonTy.download(ckpt(), 'slitherlink-checkpoint.json')}>Download</button>
|
||||
{/if}
|
||||
<button on:click={() => mode = modes[(modes.indexOf(mode) + 1) % modes.length]}>Mode: {mode}</button>
|
||||
</div>
|
||||
@@ -354,6 +362,10 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="btn-div">
|
||||
<button on:click={() => JsonTy.download(ckpt(), 'slitherlink-checkpoint.json')}>Download</button>
|
||||
<button on:click={() => upload = ckpt()}>Upload</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Add Checkpoint -->
|
||||
@@ -386,4 +398,6 @@
|
||||
<button on:click={() => completedOverlay = false}>Close</button>
|
||||
</div></div>
|
||||
{/if}
|
||||
|
||||
{#if upload} <Upload data={upload} callback={() => upload = null}/> {/if}
|
||||
</main>
|
||||
|
||||
+7
-2
@@ -5,15 +5,20 @@
|
||||
// Get puzzle id from params
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const puzzleId = urlParams.get("puzzle");
|
||||
const edit = urlParams.get("edit");
|
||||
let puzzleData: Checkpoint | null = null;
|
||||
let error: string | null = null;
|
||||
|
||||
if (!puzzleId) window.location.href = "/?puzzle=meow";
|
||||
else Backend.get(puzzleId).then((data) => (puzzleData = data)).catch(e => (error = e));
|
||||
if (!edit) {
|
||||
if (!puzzleId) window.location.href = "/?puzzle=meow";
|
||||
else Backend.get(puzzleId).then((data) => (puzzleData = data)).catch(e => (error = e));
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if puzzleData !== null && puzzleId !== null}
|
||||
<App {puzzleData} {puzzleId} />
|
||||
{:else if edit}
|
||||
<App></App>
|
||||
{:else}
|
||||
<div class="overlay">
|
||||
<div>
|
||||
|
||||
+7
-31
@@ -89,8 +89,8 @@ main
|
||||
height: 1em
|
||||
|
||||
.rules
|
||||
max-width: 900px
|
||||
opacity: 0.9
|
||||
font-size: 0.9em
|
||||
max-width: 800px
|
||||
|
||||
.btn-div
|
||||
display: flex
|
||||
@@ -118,9 +118,6 @@ main
|
||||
font-family: 'Teko', sans-serif
|
||||
font-size: 2em
|
||||
color: colors.$accent
|
||||
margin-bottom: -24px
|
||||
|
||||
// Larger gap between letters
|
||||
letter-spacing: 0.3em
|
||||
|
||||
.status
|
||||
@@ -129,30 +126,9 @@ main
|
||||
&.error
|
||||
color: colors.$cross
|
||||
|
||||
.overlay
|
||||
position: fixed
|
||||
inset: 0
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.5)
|
||||
|
||||
.timer-line
|
||||
// Two items, left and right
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
z-index: 1000
|
||||
backdrop-filter: blur(5px)
|
||||
|
||||
h2, p
|
||||
user-select: none
|
||||
margin: 0
|
||||
|
||||
> div
|
||||
background-color: colors.$background
|
||||
padding: 2rem
|
||||
border-radius: 8px
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1rem
|
||||
|
||||
max-width: 400px
|
||||
justify-content: space-between
|
||||
gap: 10px
|
||||
margin-bottom: -12px
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
@use "colors"
|
||||
|
||||
$font: Quicksand, Inter, LXGW Wenkai, Microsoft YaHei, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, Avenir, Helvetica, Arial, sans-serif
|
||||
$c-main: #b3c6ff
|
||||
$c-sub: rgba(0, 0, 0, 0.77)
|
||||
$c-good: #b3ffb9
|
||||
$c-darker: #646cff
|
||||
$c-bg: #242424
|
||||
$c-error: #ff6b6b
|
||||
$c-shadow: rgba(0, 0, 0, 0.1)
|
||||
|
||||
$ov-light: rgba(white, 0.04)
|
||||
$ov-lighter: rgba(white, 0.08)
|
||||
$ov-dark: rgba(black, 0.1)
|
||||
$ov-darker: rgba(black, 0.18)
|
||||
|
||||
$nav-height: 4rem
|
||||
$w-mobile: 560px
|
||||
$w-max: 900px
|
||||
|
||||
$grad-special: linear-gradient(90deg, #ffee94, #ffb798, #ffa3e5, #ebff94)
|
||||
|
||||
$border-radius: 12px
|
||||
$gap: 20px
|
||||
|
||||
$transition: all 0.25s
|
||||
|
||||
.card
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
border-radius: $border-radius
|
||||
padding: 12px 16px
|
||||
background: $ov-light
|
||||
|
||||
blockquote
|
||||
$c1: rgba(255, 149, 149, 0.05)
|
||||
$c2: rgba(255, 152, 152, 0.12)
|
||||
background: repeating-linear-gradient(45deg, $c1, $c1 10px, $c2 10px, $c2 20px)
|
||||
padding: 10px 20px 10px 20px
|
||||
margin: 16px 0
|
||||
border-left: solid #ff7c7c 3px
|
||||
border-radius: $border-radius
|
||||
|
||||
input, textarea
|
||||
border-radius: $border-radius
|
||||
border: 1px solid transparent
|
||||
padding: 0.6em 1.2em
|
||||
font-size: 1em
|
||||
font-weight: 500
|
||||
font-family: inherit
|
||||
background-color: $ov-lighter
|
||||
transition: $transition
|
||||
box-sizing: border-box
|
||||
resize: vertical
|
||||
&:focus, &:focus-visible
|
||||
border: 1px solid $c-main
|
||||
outline: none
|
||||
&.error
|
||||
border: 1px solid $c-error
|
||||
|
||||
select
|
||||
border-radius: $border-radius
|
||||
border: 1px solid transparent
|
||||
padding: 0.6em 1.2em
|
||||
font-size: 1em
|
||||
font-weight: 500
|
||||
font-family: inherit
|
||||
background-color: $ov-lighter
|
||||
transition: $transition
|
||||
box-sizing: border-box
|
||||
|
||||
input[type="checkbox"]
|
||||
width: 1.2em
|
||||
height: 1.2em
|
||||
margin: 0
|
||||
padding: 0
|
||||
border: 1px solid $c-main
|
||||
background-color: $ov-lighter
|
||||
appearance: none
|
||||
cursor: pointer
|
||||
flex-shrink: 0
|
||||
|
||||
&:checked
|
||||
background-color: $c-main
|
||||
border-color: $c-main
|
||||
|
||||
label
|
||||
cursor: pointer
|
||||
|
||||
.overlay
|
||||
position: fixed
|
||||
inset: 0
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.5)
|
||||
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
|
||||
z-index: 1000
|
||||
backdrop-filter: blur(5px)
|
||||
|
||||
h2, p
|
||||
user-select: none
|
||||
margin: 0
|
||||
|
||||
> div
|
||||
background-color: colors.$background
|
||||
padding: 2rem
|
||||
border-radius: 8px
|
||||
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1rem
|
||||
|
||||
min-width: 300px
|
||||
max-width: 400px
|
||||
|
||||
.actions
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 16px
|
||||
|
||||
button
|
||||
width: 100%
|
||||
|
||||
.loading.overlay
|
||||
font-size: 28rem
|
||||
|
||||
:global(.icon)
|
||||
opacity: 0.5
|
||||
|
||||
> span
|
||||
position: absolute
|
||||
inset: 0
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: center
|
||||
background: transparent
|
||||
|
||||
letter-spacing: 20px
|
||||
margin-left: 20px
|
||||
|
||||
font-size: 1.5rem
|
||||
|
||||
.error
|
||||
color: $c-error
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
export let puzzleData = {}
|
||||
</script>
|
||||
|
||||
<div class="puzzle-desc">
|
||||
<div class="head">
|
||||
<span class="puzzle">{puzzleData?.name}</span>
|
||||
<span class="by">by {puzzleData?.author}</span>
|
||||
</div>
|
||||
<div class="desc">
|
||||
{puzzleData?.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="sass">
|
||||
.puzzle-desc
|
||||
text-align: left
|
||||
|
||||
.puzzle
|
||||
font-weight: bold
|
||||
.by
|
||||
font-size: 0.8em
|
||||
color: #666
|
||||
.desc
|
||||
font-size: 0.8em
|
||||
color: #eee
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { Backend, cfg, nStates, pos, sty, type Checkpoint } from "../utils";
|
||||
|
||||
interface Props {
|
||||
data?: Checkpoint;
|
||||
callback?: () => void;
|
||||
}
|
||||
export let { data, callback }: Props = {}
|
||||
|
||||
let name = ""
|
||||
let author = ""
|
||||
let description = ""
|
||||
let error = ""
|
||||
|
||||
let id = ""
|
||||
$: url = id ? `https://${window.location.host}/?puzzle=${id}` : ""
|
||||
|
||||
function upload() {
|
||||
if (!data) return
|
||||
if (!name || !author || !description) {
|
||||
error = "Please fill in all fields"
|
||||
return
|
||||
}
|
||||
Backend.post({name, author, description, ...data}).then(res => {
|
||||
id = res.id
|
||||
console.log(res);
|
||||
}).catch(e => alert(e))
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if data}
|
||||
<div class="overlay">
|
||||
<div>
|
||||
{#if !id}
|
||||
<h2>Upload Puzzle</h2>
|
||||
|
||||
{#if error}
|
||||
<p class="error">{error}</p>
|
||||
{:else}
|
||||
<p>Upload your puzzle to the server</p>
|
||||
{/if}
|
||||
|
||||
<input type="text" placeholder="Puzzle Name" bind:value={name}>
|
||||
<input type="text" placeholder="Author" bind:value={author}>
|
||||
<textarea placeholder="Description" bind:value={description}></textarea>
|
||||
<button on:click={upload}>Upload</button>
|
||||
{:else}
|
||||
<h2>Uploaded 🥳</h2>
|
||||
<span>Your puzzle has been uploaded to the server</span>
|
||||
|
||||
<p>You can share it by the following link: </p>
|
||||
<p><a href={url}>{url}</a></p>
|
||||
<p>Thank you for your creation!</p>
|
||||
<button on:click={callback}>Close</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mount } from 'svelte'
|
||||
import './app.sass'
|
||||
import './aqua.sass'
|
||||
import './puzzle.sass'
|
||||
import './fonts/caveat.sass'
|
||||
import Launcher from './Launcher.svelte'
|
||||
|
||||
+6
-1
@@ -3,6 +3,8 @@ export type i8s = Int8Array
|
||||
export interface Checkpoint { rows: number, cols: number,
|
||||
hStates: i8s, vStates: i8s, hColors: i8s, vColors: i8s, numbers: i8s, nMask: i8s, colors: string[] }
|
||||
|
||||
export interface MetaCheckpoint extends Checkpoint { name: string, author: string, description: string, date?: string }
|
||||
|
||||
export const cfg = {
|
||||
cellW: 20,
|
||||
lineW: 4,
|
||||
@@ -75,7 +77,10 @@ export const Backend = {
|
||||
if (!r.ok) throw new Error(`Failed to get puzzle: ${r.status}`)
|
||||
return r.text()
|
||||
}).then(JsonTy.parse),
|
||||
post: (data: Checkpoint) => fetch(`${cfg.backend}/`, { method: "post", body: JsonTy.stringify(data) }),
|
||||
post: (data: MetaCheckpoint) => fetch(`${cfg.backend}/`, { method: "post", body: JsonTy.stringify(data) }).then(r => {
|
||||
if (!r.ok) throw new Error(`Failed to post puzzle: ${r.status}`)
|
||||
return r.text()
|
||||
}).then(JsonTy.parse),
|
||||
}
|
||||
|
||||
export const Misc = {
|
||||
|
||||
Reference in New Issue
Block a user