Compare commits

..

No commits in common. "ef08ab7970287930d05f5c4c263eb561faa9990e" and "4e2c14275b6488bd028613af7d843ccee42652cf" have entirely different histories.

17 changed files with 356 additions and 29909 deletions

File diff suppressed because it is too large Load Diff

View File

@ -10,21 +10,20 @@ class ParticipantController extends Controller
{
public static function createFromToken(Request $request, string $id)
{
if ($request->input('password') !== env('ADMIN_PASSWORD')) {
Log::info(env('ADMIN_PASSWORD'));
if ($request->input('password') !== 'password123') {
return response()->json(['message' => 'Unauthorized: Wrong password'], 401);
}
if(!Participant::where('user_id', $id)->exists()) {
if(!Participant::where('id', $id)->exists()) {
$participant = Participant::create([
'user_id' => $id,
'token' => uniqid()
'token' => (string) Str::ulid()
]);
return $participant->token;
}
else {
$participant = Participant::where('user_id', $id)->first();
$participant = Participant::where('id', $id)->first();
return $participant->token;
}
}

View File

@ -2,18 +2,13 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Log;
class Participant extends Model
{
use HasFactory;
use SoftDeletes;
protected $fillable = [
'user_id',
@ -23,6 +18,18 @@ class Participant extends Model
'token',
];
protected $appends = ['desperate'];
public function receiver()
{
return $this->belongsTo(Participant::class, 'giving_id');
}
public function giver()
{
return $this->hasOne(Participant::class, 'giving_id', 'id');
}
public static function findByToken(string $token): ?self
{
return self::where('token', $token)->first();
@ -60,60 +67,13 @@ class Participant extends Model
->whereDoesntHave('receiver');
}
public function receiver(): BelongsTo
public function getDesperateAttribute(): bool
{
return $this->belongsTo(Participant::class, 'giving_id');
$gives = !is_null($this->giver);
$gets = !is_null($this->receiver);
return ($gives && !$gets) || (!$gives && $gets);
}
public function giver(): HasOne
{
return $this->hasOne(Participant::class, 'giving_id', 'id');
}
public function hasReceiver(): bool
{
return !is_null($this->giving_id); // The simplest and most direct check for an outgoing link
}
public function hasGiver(): bool
{
return $this->hasOne(Participant::class, 'giving_id', 'id')
->exists();
}
protected function isUnmatched(): Attribute
{
return Attribute::make(
get: fn () => !$this->hasReceiver() && !$this->hasGiver(),
);
}
protected function isDesperate(): Attribute
{
return Attribute::make(
get: fn () => $this->hasReceiver() xor $this->hasGiver(),
);
}
public function withdraw(): bool
{
if (!$this->id) {
return false;
}
$giver = Participant::where('giving_id', $this->id)->first();
if ($giver) {
$giver->giving_id = null;
$giver->save();
}
$userB_id = $this->giving_id;
if ($userB_id) {
$this->giving_id = null;
}
$this->delete();
return true;
}
}

View File

@ -14,57 +14,92 @@ class MatcherService
return;
}
// desperate denotes a user A that is supposed to give to a user B that dropped out
// whilst user A has a user C giving to them, or vice versa
$desperate = $participants->where('desperate', true)->values();
$nonDesperate = $participants->where('desperate', false)->values();
$desperate = $desperate->shuffle();
$nonDesperate = $nonDesperate->shuffle();
$assignments = [];
$newParticipantIds = $participants
->filter(fn($p) => $p->is_unmatched)
->pluck('id')
->shuffle()
->all();
/*
|--------------------------------------------------------------------------
| 1. Match DESPERATE participants
|--------------------------------------------------------------------------
| A desperate user should match:
| - First to another desperate participant
| - If none left, match to a non-desperate participant
|--------------------------------------------------------------------------
*/
$desperateGiverIds = $participants
->filter(fn($p) => $p->is_desperate && !$p->hasReceiver())
->pluck('id')
->shuffle()
->all();
foreach ($desperate as $p) {
// Exclude self from possible options
$pool = $desperate->filter(fn($d) => $d->id !== $p->id);
$desperateReceiverIds = $participants
->filter(fn($p) => $p->is_desperate && !$p->hasGiver())
->pluck('id')
->shuffle()
->all();
if ($pool->isEmpty()) {
$pool = $nonDesperate;
}
while (!empty($desperateGiverIds) && !empty($desperateReceiverIds)) {
$giverId = array_shift($desperateGiverIds);
$receiverId = array_shift($desperateReceiverIds);
if ($giverId === $receiverId) {
array_unshift($desperateReceiverIds, $receiverId);
if ($pool->isEmpty()) {
continue;
}
$assignments[$giverId] = $receiverId;
}
$newCount = count($newParticipantIds);
if ($newCount >= 2) {
for ($i = 0; $i < $newCount; $i++) {
$currentId = $newParticipantIds[$i];
$nextId = $newParticipantIds[($i + 1) % $newCount];
$assignments[$currentId] = $nextId;
$candidate = $this->pickValidTarget($p, $pool, $assignments);
if ($candidate) {
$assignments[$p->id] = $candidate->id;
}
}
/*
|--------------------------------------------------------------------------
| 2. Match NON-DESPERATE participants
|--------------------------------------------------------------------------
| A non-desperate participant always matches to the non-desperate pool.
|--------------------------------------------------------------------------
*/
foreach ($assignments as $giverId => $receiverId) {
Participant::where('id', $giverId)->update([
'giving_id' => $receiverId,
foreach ($nonDesperate as $p) {
// Exclude self
$pool = $nonDesperate->filter(fn($d) => $d->id !== $p->id);
if ($pool->isEmpty()) {
continue;
}
$candidate = $this->pickValidTarget($p, $pool, $assignments);
if ($candidate) {
$assignments[$p->id] = $candidate->id;
}
}
/*
|--------------------------------------------------------------------------
| 3. Write all results back to DB
|--------------------------------------------------------------------------
*/
foreach ($assignments as $id => $givingId) {
Participant::where('id', $id)->update([
'giving_id' => $givingId,
]);
}
}
private function pickValidTarget(Participant $giver, Collection $pool, array $assignments): ?Participant
{
$shuffled = $pool->shuffle();
// Avoid duplicates
foreach ($shuffled as $candidate) {
if (($assignments[$candidate->id] ?? null) === $giver->id) {
continue;
}
return $candidate;
}
return null;
}
}

View File

@ -1,174 +1,5 @@
import asyncio
import aiohttp
import os
import sys
import json
from typing import Optional, Dict, Any
from dotenv import load_dotenv
import urllib.parse
import re
load_dotenv()
SKETCHERS_UNITED_SESSION = os.environ.get('SKETCHERS_UNITED_SESSION')
REMEMBER_WEB = os.environ.get('REMEMBER_WEB')
TARGET_URL = 'https://sketchersunited.org/chats'
API_PASSWORD = os.environ.get('API_PASSWORD')
XSRF_TOKEN = os.environ.get('XSRF_TOKEN')
def get_session_cookie_header() -> Optional[Dict[str, str]]:
if not SKETCHERS_UNITED_SESSION or not REMEMBER_WEB:
print("Missing required session or remember_web environment variables.", file=sys.stderr)
return None
cookies_to_send = []
cookies_to_send.append(f"sketchers_united_session={SKETCHERS_UNITED_SESSION}")
cookies_to_send.append(f"remember_web_59ba36addc2b2f9401580f014c7f58ea4e30989d={REMEMBER_WEB}")
if XSRF_TOKEN:
cookies_to_send.append(f"XSRF-TOKEN={XSRF_TOKEN}")
cookie_header = "; ".join(cookies_to_send)
return {
'Cookie': cookie_header,
'Accept': 'application/json',
'User-Agent': 'usernames chat checking bot',
'Content-Type': 'application/json',
}
def get_post_headers(xsrf_token: str) -> Dict[str, str]:
return {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.5',
'X-XSRF-TOKEN': xsrf_token,
'Sec-GPC': '1',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'Priority': 'u=0',
}
async def make_new_json_request(user_id: int):
url = f'http://localhost/api/token/{user_id}'
data = {
'password': API_PASSWORD
}
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
async with aiohttp.ClientSession() as session:
async with session.post(url, json=data, headers=headers) as response:
response_text = await response.text()
if(len(response_text) < 14):
return response_text
else:
return False
async def post_chat_update(session: aiohttp.ClientSession, chat_object: Dict[str, Any], xsrf_token: str):
pattern = r'/(\d+)/'
match = re.search(pattern, chat_object.get('icon_url'))
token = await make_new_json_request(match.group(1))
form_data = aiohttp.FormData()
if(token):
form_data.add_field('text', f"""You've signed up for the Secret Sketchers event, your link is http://localhost/profile/{token} - it is private and should only be used by you.
\nDon't want to be in it anymore? Go to http://localhost/withdraw/{token} - note that withdrawing is permanent and you cannot re-enter the event.""")
else:
form_data.add_field('text', f"""Something went wrong, Sorry I will fix it sooner or later... maybe""")
headers = get_post_headers(xsrf_token)
try:
cookie_headers = get_session_cookie_header()
if cookie_headers:
headers['Cookie'] = cookie_headers['Cookie']
async with session.post(
f"{TARGET_URL}/{chat_object.get('id')}/messages",
data=form_data,
headers=headers
) as response:
if response.status not in [200, 201]:
text = await response.text()
print(f" Status: {response.status}. Response: {text}", file=sys.stderr)
except aiohttp.ClientError as e:
print(f"-> POST ERROR: Client error during update for chat ID {chat_object.get('id', 'N/A')}: {e}", file=sys.stderr)
except Exception as e:
print(f"-> POST ERROR: Unexpected error during update for chat ID {chat_object.get('id', 'N/A')}: {e}", file=sys.stderr)
async def process_chats():
global XSRF_TOKEN
async with aiohttp.ClientSession() as session:
headers = get_session_cookie_header()
if not headers:
return
async with session.get(TARGET_URL, headers=headers) as response:
if response.status != 200:
print(f"failed: {response.status}", file=sys.stderr)
return
try:
chat_data_array = await response.json()
if not isinstance(chat_data_array, list):
print("not json", file=sys.stderr)
return
except aiohttp.ContentTypeError:
print("also not json", file=sys.stderr)
return
fresh_xsrf_token = None
for cookie in session.cookie_jar:
if cookie.key == "XSRF-TOKEN":
fresh_xsrf_token = urllib.parse.unquote(cookie.value)
break
XSRF_TOKEN = fresh_xsrf_token
for index, chat_object in enumerate(chat_data_array):
if(chat_object.get('type') == 'private'):
seen_at_value = chat_object.get('seen_at')
is_unread = False
try:
if isinstance(seen_at_value, dict):
is_unread = True
# print(f"Chat {index}: Status: DICT (New Chat) -> Unread: True")
else:
latest_created_at = chat_object.get('latest', {}).get('created_at', 0)
seen_at_int = int(seen_at_value) if seen_at_value is not None else 0
is_unread = seen_at_int < latest_created_at
# print(f"Chat {index}: Seen at ({seen_at_int}) < Latest message ({latest_created_at}) -> Unread: {is_unread}")
except (AttributeError, TypeError, ValueError) as e:
is_unread = False
if is_unread:
await post_chat_update(session, chat_object, fresh_xsrf_token)
async def main_loop():
while True:
try:
await process_chats()
await asyncio.sleep(5)
except asyncio.CancelledError:
print("cancelled normally (ok knowing my luck the error is gonna be this one every single time even tho it never actually is an error like this!) cute puppy: Hiiii")
break
except Exception as e:
print(f"error {e}")
await asyncio.sleep(5)
if __name__ == "__main__":
asyncio.run(main_loop())
- get chats page
- for each
- if seen_at is less than latest[created_at]
- get token url from api
- respond to that user

View File

@ -18,8 +18,6 @@
"ext-curl": "*"
},
"require-dev": {
"ext-curl": "*",
"barryvdh/laravel-ide-helper": "^3.6",
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.24",
@ -27,7 +25,8 @@
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"pestphp/pest": "^4.1",
"pestphp/pest-plugin-laravel": "^4.0"
"pestphp/pest-plugin-laravel": "^4.0",
"ext-curl": "*"
},
"autoload": {
"psr-4": {

298
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "10384b5e2e1414abcd436d205f0114a6",
"content-hash": "c78bcab45d4b86c31ecc79b40f1a12ad",
"packages": [
{
"name": "bacon/bacon-qr-code",
@ -6496,152 +6496,6 @@
}
],
"packages-dev": [
{
"name": "barryvdh/laravel-ide-helper",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-ide-helper.git",
"reference": "8d00250cba25728373e92c1d8dcebcbf64623d29"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/8d00250cba25728373e92c1d8dcebcbf64623d29",
"reference": "8d00250cba25728373e92c1d8dcebcbf64623d29",
"shasum": ""
},
"require": {
"barryvdh/reflection-docblock": "^2.4",
"composer/class-map-generator": "^1.0",
"ext-json": "*",
"illuminate/console": "^11.15 || ^12",
"illuminate/database": "^11.15 || ^12",
"illuminate/filesystem": "^11.15 || ^12",
"illuminate/support": "^11.15 || ^12",
"php": "^8.2"
},
"require-dev": {
"ext-pdo_sqlite": "*",
"friendsofphp/php-cs-fixer": "^3",
"illuminate/config": "^11.15 || ^12",
"illuminate/view": "^11.15 || ^12",
"mockery/mockery": "^1.4",
"orchestra/testbench": "^9.2 || ^10",
"phpunit/phpunit": "^10.5 || ^11.5.3",
"spatie/phpunit-snapshot-assertions": "^4 || ^5",
"vimeo/psalm": "^5.4",
"vlucas/phpdotenv": "^5"
},
"suggest": {
"illuminate/events": "Required for automatic helper generation (^6|^7|^8|^9|^10|^11)."
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider"
]
},
"branch-alias": {
"dev-master": "3.5-dev"
}
},
"autoload": {
"psr-4": {
"Barryvdh\\LaravelIdeHelper\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.",
"keywords": [
"autocomplete",
"codeintel",
"dev",
"helper",
"ide",
"laravel",
"netbeans",
"phpdoc",
"phpstorm",
"sublime"
],
"support": {
"issues": "https://github.com/barryvdh/laravel-ide-helper/issues",
"source": "https://github.com/barryvdh/laravel-ide-helper/tree/v3.6.0"
},
"funding": [
{
"url": "https://fruitcake.nl",
"type": "custom"
},
{
"url": "https://github.com/barryvdh",
"type": "github"
}
],
"time": "2025-07-17T20:11:57+00:00"
},
{
"name": "barryvdh/reflection-docblock",
"version": "v2.4.0",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/ReflectionDocBlock.git",
"reference": "d103774cbe7e94ddee7e4870f97f727b43fe7201"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/d103774cbe7e94ddee7e4870f97f727b43fe7201",
"reference": "d103774cbe7e94ddee7e4870f97f727b43fe7201",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "^8.5.14|^9"
},
"suggest": {
"dflydev/markdown": "~1.0",
"erusev/parsedown": "~1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.3.x-dev"
}
},
"autoload": {
"psr-0": {
"Barryvdh": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "mike.vanriel@naenius.com"
}
],
"support": {
"source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.4.0"
},
"time": "2025-07-17T06:07:30+00:00"
},
{
"name": "brianium/paratest",
"version": "v7.15.0",
@ -6735,154 +6589,6 @@
],
"time": "2025-11-30T08:08:11+00:00"
},
{
"name": "composer/class-map-generator",
"version": "1.7.0",
"source": {
"type": "git",
"url": "https://github.com/composer/class-map-generator.git",
"reference": "2373419b7709815ed323ebf18c3c72d03ff4a8a6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/class-map-generator/zipball/2373419b7709815ed323ebf18c3c72d03ff4a8a6",
"reference": "2373419b7709815ed323ebf18c3c72d03ff4a8a6",
"shasum": ""
},
"require": {
"composer/pcre": "^2.1 || ^3.1",
"php": "^7.2 || ^8.0",
"symfony/finder": "^4.4 || ^5.3 || ^6 || ^7 || ^8"
},
"require-dev": {
"phpstan/phpstan": "^1.12 || ^2",
"phpstan/phpstan-deprecation-rules": "^1 || ^2",
"phpstan/phpstan-phpunit": "^1 || ^2",
"phpstan/phpstan-strict-rules": "^1.1 || ^2",
"phpunit/phpunit": "^8",
"symfony/filesystem": "^5.4 || ^6 || ^7 || ^8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\ClassMapGenerator\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "https://seld.be"
}
],
"description": "Utilities to scan PHP code and generate class maps.",
"keywords": [
"classmap"
],
"support": {
"issues": "https://github.com/composer/class-map-generator/issues",
"source": "https://github.com/composer/class-map-generator/tree/1.7.0"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
}
],
"time": "2025-11-19T10:41:15+00:00"
},
{
"name": "composer/pcre",
"version": "3.3.2",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<1.11.10"
},
"require-dev": {
"phpstan/phpstan": "^1.12 || ^2",
"phpstan/phpstan-strict-rules": "^1 || ^2",
"phpunit/phpunit": "^8 || ^9"
},
"type": "library",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Pcre\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
"keywords": [
"PCRE",
"preg",
"regex",
"regular expression"
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.3.2"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-11-12T16:29:46+00:00"
},
{
"name": "doctrine/deprecations",
"version": "1.1.5",
@ -10134,5 +9840,5 @@
"platform-dev": {
"ext-curl": "*"
},
"plugin-api-version": "2.9.0"
"plugin-api-version": "2.6.0"
}

View File

@ -24,7 +24,7 @@ class ParticipantFactory extends Factory
public function definition(): array
{
return [
'token' => uniqid(),
'token' => (string) Str::ulid(),
];
}

View File

@ -24,7 +24,6 @@ return new class extends Migration
$table->string('token');
$table->timestamps();
$table->softDeletes();
});
}

3
need Normal file
View File

@ -0,0 +1,3 @@
bot gens api token
api token with link to access the page

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -1,19 +1,6 @@
@livewireStyles
<flux:main>
<!--
######
#### ### ####
## ## ## ##
## ## ## ##
## ## ### ##
## ## ## ##
## ## ## ##
#### ##########
###### ##
### ###
########
-->
{{ $slot }}
</flux:main>

View File

@ -27,8 +27,7 @@ rules([
],
]);
mount(function ($token)
{
mount(function ($token) {
$this->token = $token;
if ($this->token) {
$this->participant = Participant::findByToken($this->token);
@ -51,8 +50,7 @@ mount(function ($token)
}
});
$saveEditingPrompt = function ()
{
$saveEditingPrompt = function () {
if ($this->participant) {
$this->validate();
@ -62,14 +60,12 @@ $saveEditingPrompt = function ()
}
};
$cancelEditingPrompt = function ()
{
$cancelEditingPrompt = function () {
$this->prompt = $this->participant->prompt ?? '';
$this->isEditingPrompt = false;
};
$saveEditingSubmission = function ()
{
$saveEditingSubmission = function () {
if ($this->participant) {
$this->validate();
@ -79,8 +75,7 @@ $saveEditingSubmission = function ()
}
};
$cancelEditingSubmission = function ()
{
$cancelEditingSubmission = function () {
$this->submissionUrl = $this->participant->submission_url ?? '';
$this->isEditingSubmission = false;
};
@ -96,6 +91,7 @@ $cancelEditingSubmission = function ()
</div>
<div class="username-container">
You are... <h1 style="margin-bottom:0;padding:0"> &commat;{{ $userData['data']['username'] ?? 'Unknown' }}</h1>
<small style="color:rgba(255,255,255,0.5);">your password is {{$token}}</small>
</div>
</div>
@ -162,8 +158,11 @@ $cancelEditingSubmission = function ()
</div>
</div>
- withdraw
- bot
<div class="other-content">
<h2> Are you done? </h2>
<h2> Are you done? </h2>
<p>You will be able to link to your gift below once submissions are accepted on 22nd December.</p>
<small style="color:rgba(255,255,255,0.5)">submissions will be accepted up until 5th of january but before then is much more appreciated!</small>
@ -203,17 +202,14 @@ $cancelEditingSubmission = function ()
@elseif($participant->desperate)
<p>You're in a bind!</p>
@else
<p>You haven't been assigned anyone yet. Initial assignments will be made on 13th December.</p>
<p>You haven't been assigned anyone.</p>
@endif
</div>
<div class="other-content">
<a
href="/withdraw/{{$token}}"
>
Withdraw from Secret Sketchers...?
href="/withdraw"
>
Withdraw from Secret Sketchers...
</a>
</div>
@endif
<style>
@ -238,10 +234,12 @@ $cancelEditingSubmission = function ()
margin: 15px;
}
/* Add spacing below the main prompt text */
.prompt-content {
margin-bottom: 10px;
}
/* Reset H2 margin for better spacing */
.prompt-content h2 {
margin-top: 5px;
margin-bottom: 5px;
@ -277,15 +275,51 @@ $cancelEditingSubmission = function ()
}
body {
background-color: #021202;
background-color: #222;
margin: 10pt;
font-family: sans-serif;
background-image: url("../img/christmas-bg.png");
background-size: 140px;
color: white;
overflow: hidden;
}
#viewer {
width: 100vw;
height: 100vh;
background-color: #222;
background-image: url("image/su.png");
background-repeat: no-repeat;
background-size: 400px;
background-position: center;
image-rendering: pixelated;
}
.r6o-editor {
z-index: 3;
height: auto;
}
.r6o-widget.comment {
font-size: 1.5em;
background-color: #333
}
.r6o-editor .r6o-arrow:after {
background-color: #333;
border: 3px white solid;
}
.r6o-editor .r6o-editor-inner .r6o-widget {
border: 3px white solid;
}
.r6o-editor .r6o-editor-inner {
background-color: transparent !important;
}
.r6o-footer {
display: none;
}
div {
color: white;
}
@ -295,5 +329,183 @@ $cancelEditingSubmission = function ()
font-weight: bold;
}
</style>
#here-now {
position: fixed;
bottom: 0;
left: 0;
font-size: 1.5vh;
color: white;
padding: 15px;
z-index: 2;
background-color:#333;
border-radius: 1rem;
border: 3px solid white;
margin: 5pt;
}
#question {
font-size: 3vh;
width: 3vh;
height: 3vh;
position: fixed;
bottom: 0;
right: 0;
background-color: #333;
color: #fff;
border: 3px solid white;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
padding: 15px;
margin: 10px;
}
#question img {
max-width: 100%;
height: auto;
display: block;
}
#modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 3;
}
#banner {
max-width: 100%;
}
#modal {
display: none;
font-size: 18pt;
position: fixed;
top: 50%;
opacity: 0;
transition: opacity 0.3s ease-in-out;
left: 50%;
transform: translate(-50%, -50%);
justify-content: center;
border-radius: 1rem;
box-shadow: 0 0 10px 10px #000;
background-color: #333;
align-items: center;
z-index: 4;
width: 80%;
height: 80%;
overflow-y: auto;
}
#modal-content {
position: relative;
padding: 33px;
text-align: center;
max-height: 100%;
font-size: 15pt;
}
@keyframes animateBg {
0% {
background-position: 0 0;
}
100% {
background-position: 100% 0;
}
}
.rainbow {
background: linear-gradient(90deg, #f1672b, #fea419, #efd515, #89d842, #35c2f9, #9b5fe0, #ff68cf, #f1672b, #fea419, #efd515, #89d842, #35c2f9, #9b5fe0, #ff68cf, #f1672b, #fea419, #efd515, #89d842, #35c2f9, #9b5fe0, #ff68cf, #f1672b);
background-size: 300% 100%;
-webkit-background-clip: text;
animation: animateBg 5s infinite linear;
background-clip: text;
color: transparent;
}
.profile-container {
display:flex;
align-items: center;
}
.profileimg {
flex: 0 0 20%; /* 10% of the container's width, not flexible, not growing, not shrinking */
max-height: 100%;
}
.profileimg img {
width: 100%; /* Make sure the image fills its container */
height: auto; /* Maintain aspect ratio */
border-radius: 50%;
}
.profile {
flex: 1; /* Take up remaining space */
padding-left: 1rem; /* Add some space between image and text */
}
#close-button {
position: absolute;
font-size: 3rem;
top: 15px;
right: 30px;
cursor: pointer;
width: 3vh;
height: 3vh;
background-color: #333;
color: #fff;
border: 3px solid white;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
padding: 15px;
margin: 10px;
}
#open-button {
cursor: pointer;
}
#mobile-banner {
display:none;
}
@media screen and (max-width: 1081px) and (-webkit-min-device-pixel-ratio: 2) {
#viewer {
width: 100vw;
height: 66vh;
border-bottom: 3px white solid;
}
#modal-content {
font-size: 20pt;
}
#container {
display: flex;
height: 100vh;
align-items: center;
justify-content: center;
}
#mobile-banner {
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
height: 27vh;
overflow: hidden;
background-color: #444;
}
#mobile-banner img {
max-width: 90%;
max-height: 100%;
}
}</style>
</div>

View File

@ -5,15 +5,9 @@ use App\Services\MatcherService;
use Illuminate\Database\Eloquent\Collection;
use function Livewire\Volt\{state, mount};
state(['participants' => Collection::class,
'render' => false]);
state(['password']);
state(['participants' => Collection::class]);
mount(function ($password) {
$this->password = $password;
if ($this->password == env('ADMIN_PASSWORD')) {
$this->render = true;
}
mount(function () {
$this->participants = Participant::all();
});
@ -28,7 +22,6 @@ $runMatch = function () {
?>
<div class="p-6 bg-gray-100 min-h-screen">
@if($render)
<div class="max-w-7xl mx-auto">
<div class="flex justify-between items-center mb-6">
@ -95,8 +88,8 @@ $runMatch = function () {
<td class="px-6 py-4 whitespace-nowrap text-sm">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
{{ $participant->is_desperate ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800' }}">
{{ $participant->is_desperate ? 'YES' : 'No' }}
{{ $participant->desperate ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800' }}">
{{ $participant->desperate ? 'YES' : 'No' }}
</span>
</td>
@ -109,5 +102,4 @@ $runMatch = function () {
</table>
</div>
@endif
@endif
</div>

View File

@ -1,71 +1 @@
<?php
use App\Models\Participant;
use App\Services\PostService;
use Illuminate\Support\Facades\Log;
use function Livewire\Volt\{state, mount, rules};
state(['token']);
state([
'participant' => null,
]);
mount(function ($token)
{
$this->token = $token;
if ($this->token) {
$this->participant = Participant::findByToken($this->token);
}
});
$confirmWithdrawal = function ()
{
$this->js(<<<'JS'
if (confirm('Are you sure you want to withdraw from the event? This action cannot be undone.')) {
$wire.call('withdraw');
}
JS);
};
$withdraw = function ()
{
if($this->participant->withdraw()) {
$this->js('alert("Withdrawal complete")');
return $this->redirect('/', navigate: true);
}
else {
$this->js('alert("Something went wrong")');
return null;
}
};
?>
<div>
<h1>Withdraw from the event?</h1>
<p>Withdrawing is permanent, once you withdraw you cannot rejoin.</p>
<button
wire:click="confirmWithdrawal"
class=""
>
Withdraw
</button>
<style>
body {
background-color: #021202;
margin: 10pt;
font-family: sans-serif;
background-image: url("../img/christmas-bg.png");
background-size: 140px;
color: white;
overflow: hidden;
}
a {
color: white;
font-weight: bold;
}</style>
</div>

View File

@ -9,328 +9,24 @@
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<style>
.user-profile {
display: flex;
align-items: center;
padding: 15px;
background-color: rgba(0,0,0,0.3);
border-radius: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
max-width: 100%;
margin: 15px;
}
.other-content {
display: block;
padding: 15px;
background-color: rgba(0,0,0,0.3);
border-radius: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
max-width: 100%;
margin: 15px;
}
/* Add spacing below the main prompt text */
.prompt-content {
margin-bottom: 10px;
}
/* Reset H2 margin for better spacing */
.prompt-content h2 {
margin-top: 5px;
margin-bottom: 5px;
}
.avatar-container {
width: 60px;
height: 60px;
border-radius: 50%;
overflow: hidden;
margin-right: 15px;
background-color: #ccc;
flex-shrink: 0;
}
.avatar-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
.username-container {
flex-grow: 1;
min-width: 0;
}
.username-container h1 {
margin: 0;
font-size: 1.2em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
body {
background-color: #021202;
margin: 10pt;
font-family: sans-serif;
background-image: url("./img/christmas-bg.png");
background-size: 140px;
color: white;
overflow: hidden;
}
h2, h3 {
color: #ca494c
}
#viewer {
width: 100vw;
height: 100vh;
background-color: #222;
background-repeat: no-repeat;
background-size: 400px;
background-position: center;
image-rendering: pixelated;
}
.r6o-editor {
z-index: 3;
height: auto;
}
.r6o-widget.comment {
font-size: 1.5em;
background-color: #333
}
.r6o-editor .r6o-arrow:after {
background-color: #333;
border: 3px white solid;
}
.r6o-editor .r6o-editor-inner .r6o-widget {
border: 3px white solid;
}
.r6o-editor .r6o-editor-inner {
background-color: transparent !important;
}
.r6o-footer {
display: none;
}
div {
color: white;
}
a {
color: white;
font-weight: bold;
}
#here-now {
position: fixed;
bottom: 0;
left: 0;
font-size: 1.5vh;
color: white;
padding: 15px;
z-index: 2;
background-color:#333;
border-radius: 1rem;
border: 3px solid white;
margin: 5pt;
}
#question {
font-size: 3vh;
width: 3vh;
height: 3vh;
position: fixed;
bottom: 0;
right: 0;
background-color: #333;
color: #fff;
border: 3px solid white;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
padding: 15px;
margin: 10px;
}
#question img {
max-width: 100%;
height: auto;
display: block;
}
#modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 3;
}
#banner {
max-width: 100%;
}
#modal {
display: none;
font-size: 18pt;
position: fixed;
top: 50%;
opacity: 0;
transition: opacity 0.3s ease-in-out;
left: 50%;
transform: translate(-50%, -50%);
justify-content: center;
border-radius: 1rem;
box-shadow: 0 0 10px 10px #000;
background-color: #333;
align-items: center;
z-index: 4;
width: 80%;
height: 80%;
overflow-y: auto;
}
#modal-content {
position: relative;
padding: 33px;
text-align: center;
max-height: 100%;
font-size: 15pt;
}
@keyframes animateBg {
0% {
background-position: 0 0;
}
100% {
background-position: 100% 0;
}
}
.rainbow {
background: linear-gradient(90deg, #f1672b, #fea419, #efd515, #89d842, #35c2f9, #9b5fe0, #ff68cf, #f1672b, #fea419, #efd515, #89d842, #35c2f9, #9b5fe0, #ff68cf, #f1672b, #fea419, #efd515, #89d842, #35c2f9, #9b5fe0, #ff68cf, #f1672b);
background-size: 300% 100%;
-webkit-background-clip: text;
animation: animateBg 5s infinite linear;
background-clip: text;
color: transparent;
}
.profile-container {
display:flex;
align-items: center;
}
.profileimg {
flex: 0 0 20%; /* 10% of the container's width, not flexible, not growing, not shrinking */
max-height: 100%;
}
.profileimg img {
width: 100%; /* Make sure the image fills its container */
height: auto; /* Maintain aspect ratio */
border-radius: 50%;
}
.profile {
flex: 1; /* Take up remaining space */
padding-left: 1rem; /* Add some space between image and text */
}
#close-button {
position: absolute;
font-size: 3rem;
top: 15px;
right: 30px;
cursor: pointer;
width: 3vh;
height: 3vh;
background-color: #333;
color: #fff;
border: 3px solid white;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
padding: 15px;
margin: 10px;
}
#open-button {
cursor: pointer;
}
#mobile-banner {
display:none;
}
@media screen and (max-width: 1081px) and (-webkit-min-device-pixel-ratio: 2) {
#viewer {
width: 100vw;
height: 66vh;
border-bottom: 3px white solid;
}
#modal-content {
font-size: 20pt;
}
#container {
display: flex;
height: 100vh;
align-items: center;
justify-content: center;
}
#mobile-banner {
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
height: 27vh;
overflow: hidden;
background-color: #444;
}
#mobile-banner img {
max-width: 90%;
max-height: 100%;
}
}</style>
</head>
<body>
<body style="background-color: black; color: white; ">
<div>
<h1>Sketchers United's Secret Sketcher Event 2025</h1>
<p>To sign up please DM anything to <a href ="https://sketchersunited.org/users/277827">this bot</a> to get a sign in link</p>
<hr>
<h2>FAQ</h2>
<h3>How does it work</h3>
<h1>Sketchers United Secret Santa</h1>
<p>To sign up please DM <a href ="https://sketchersunited.org/users/646464646464">this user</a> to get a sign in link</p>
<h1>FAQ</h1>
<h2>How does it work</h2>
<p>Lalalala</p>
<h3>How long do I have to do it</h3>
<h2>How long do I have to do it</h2>
<p>Lalalala</p>
<h3>Can I withdraw</h3>
<h2>Can I withdraw</h2>
<p>Lalalala</p>
<h3>Can I change who I'm drawing for </h3>
<h2>Can I change who I'm drawing for </h2>
<p>Lalalala</p>
<h3>Any other questions </h3>
<h2>Any other questions </h2>
<p>Lalalala</p>
</div>
</body>
</html>

View File

@ -8,6 +8,6 @@ Route::get('/', function () {
})->name('home');
Volt::route('start', 'pages.start')->name('start');
Volt::route('table/{password}', 'pages.table')->name('table');
Volt::route('table', 'pages.table')->name('table');
Volt::route('profile/{token}', 'pages.profile')->name('profile');
Volt::route('withdraw/{token}', 'pages.withdraw')->name('withdraw');
Volt::route('withdraw', 'pages.withdraw')->name('withdraw');