Working palette editor

This commit is contained in:
Docker VM 2024-07-28 18:06:05 -04:00
parent 0cbd691cc7
commit 9876743bcf
32 changed files with 920 additions and 227 deletions

View File

@ -11,7 +11,8 @@ public static function parseMessage($message)
Log::debug('TwitchHelper::parseMessage', ['message' => $message]);
// Match commands like !place A 1 red, !p A 1 red, !paint A 1 red
$commandPattern = '/^(?:!place|!p|!paint)\s([A-P])\s(\d{1,2})\s(\w+)$/i';
// Updated regex to handle hyphenated color names
$commandPattern = '/^(?:!place|!p|!paint)\s([A-P])\s(\d{1,2})\s([\w-]+)$/i';
if (preg_match($commandPattern, $message, $matches)) {
Log::debug('TwitchHelper::parseMessage - Command matched', ['matches' => $matches]);

View File

@ -0,0 +1,61 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\GameSession;
use App\Models\Palette;
use App\Models\CurrentState;
class AdministrationController extends Controller
{
public function index()
{
$palettes = Palette::all();
$currentGame = GameSession::whereNull('ended_at')->with('palette')->latest()->first();
$currentState = $currentGame ? CurrentState::where('game_session_id', $currentGame->id)->with('color')->get() : [];
return view('administration.index', compact('palettes', 'currentGame', 'currentState'));
}
public function changePalette(Request $request)
{
$request->validate([
'palette_id' => 'required|exists:palettes,id',
]);
$currentGame = GameSession::whereNull('ended_at')->latest()->first();
if ($currentGame) {
$currentGame->palette_id = $request->palette_id;
$currentGame->save();
return redirect()->route('administration.index')->with('success', 'Palette changed successfully.');
}
return redirect()->route('administration.index')->with('error', 'No active game session.');
}
public function createGame(Request $request)
{
$request->validate([
'session_name' => 'required|string|max:255',
'palette_id' => 'required|exists:palettes,id',
]);
// End the current game session if one is active
$currentGame = GameSession::whereNull('ended_at')->latest()->first();
if ($currentGame) {
$currentGame->update(['ended_at' => now()]);
}
// Create the new game session
$newGame = GameSession::create([
'session_name' => $request->session_name,
'palette_id' => $request->palette_id,
]);
// Reset the board for the new game session
CurrentState::where('game_session_id', $newGame->id)->delete();
return redirect()->route('administration.index')->with('success', 'New game session created and board reset.');
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\CommandHistory;
use App\Models\CurrentState;
use App\Models\GameSession;
use App\Models\PaletteColor;
use App\Models\TwitchUser;
class CommandController extends Controller
{
public function parseChatMessage(Request $request)
{
$request->validate([
'username' => 'required|string',
'x' => 'required|string',
'y' => 'required|string',
'color' => 'required|string',
]);
$user = TwitchUser::firstOrCreate(['username' => $request->username]);
$currentGame = GameSession::whereNull('ended_at')->latest()->first();
if (!$currentGame) {
return response()->json(['error' => 'No active game session'], 404);
}
$color = PaletteColor::where('hex_value', $request->color)->first();
if (!$color) {
return response()->json(['error' => 'Invalid color'], 400);
}
$command = CommandHistory::create([
'twitch_user_id' => $user->id,
'game_session_id' => $currentGame->id,
'x' => $request->x,
'y' => $request->y,
'palette_color_id' => $color->id,
'timestamp' => now(),
]);
CurrentState::updateOrCreate(
['game_session_id' => $currentGame->id, 'x' => $request->x, 'y' => $request->y],
['palette_color_id' => $color->id, 'updated_by' => $user->id, 'timestamp' => now()]
);
// Logic to check if the current state matches the winning state
// ...
return response()->json($command);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\CurrentState;
use App\Models\GameSession;
class CurrentStateController extends Controller
{
public function index()
{
$currentGame = GameSession::whereNull('ended_at')->latest()->first();
if (!$currentGame) {
return response()->json(['error' => 'No active game session'], 404);
}
$currentState = CurrentState::where('game_session_id', $currentGame->id)->with('color')->get();
return response()->json($currentState);
}
}

View File

@ -4,33 +4,53 @@
use Illuminate\Http\Request;
use App\Models\GameSession;
use App\Models\Palette;
use App\Models\WinningState;
class GameSessionController extends Controller
{
public function start(Request $request)
{
$sessionName = $request->input('session_name');
$session = GameSession::create(['session_name' => $sessionName]);
$request->validate([
'session_name' => 'required|string|max:255',
'palette_id' => 'required|exists:palettes,id',
'image_path' => 'required|string'
]);
return response()->json($session);
$gameSession = GameSession::create([
'session_name' => $request->session_name,
'palette_id' => $request->palette_id,
'image_path' => $request->image_path,
]);
// Logic to initialize the winning state based on the provided image
// ...
return response()->json($gameSession);
}
public function end(Request $request)
public function end(Request $request, $id)
{
$sessionId = $request->input('session_id');
$session = GameSession::find($sessionId);
$gameSession = GameSession::findOrFail($id);
$gameSession->update(['ended_at' => now()]);
if ($session) {
$session->update(['ended_at' => now()]);
return response()->json($session);
}
return response()->json(['error' => 'Session not found'], 404);
return response()->json($gameSession);
}
public function current()
{
$session = GameSession::whereNull('ended_at')->latest()->first();
return response()->json($session);
$currentGame = GameSession::whereNull('ended_at')->latest()->first();
return response()->json($currentGame);
}
public function getCurrentGameSession()
{
$gameSession = GameSession::whereNull('ended_at')->latest()->first();
if (!$gameSession) {
$gameSession = GameSession::create(['session_name' => 'Session ' . now(), 'palette_id' => 1, 'image_path' => '']);
}
return $gameSession;
}
}

View File

@ -5,12 +5,19 @@
use Illuminate\Http\Request;
use App\Models\Palette;
use App\Models\PaletteColor;
use Illuminate\Support\Facades\Storage;
class PaletteController extends Controller
{
public function index()
public function index(Request $request)
{
$palettes = Palette::all();
// Check if it's an AJAX request
if ($request->ajax()) {
return response()->json($palettes);
}
return view('palette-editor', compact('palettes'));
}
@ -57,6 +64,10 @@ public function updateColor(Request $request, Palette $palette, PaletteColor $co
public function deleteColor(Palette $palette, PaletteColor $color)
{
if ($palette->name === 'default-colors') {
return response()->json(['message' => 'Cannot delete colors from the default palette'], 403);
}
$color->delete();
return response()->json(['message' => 'Color deleted']);
}
@ -65,4 +76,72 @@ public function getColors(Palette $palette)
{
return response()->json($palette->colors);
}
public function export(Palette $palette)
{
$palette->load('colors'); // Load the colors relationship
$fileName = $palette->name . '.pal';
$content = $palette->colors->map(function($color) {
return [
'name' => $color->name,
'hex_value' => $color->hex_value
];
})->toArray();
$jsonContent = json_encode($content, JSON_PRETTY_PRINT); // Format JSON for readability
return response($jsonContent, 200)
->header('Content-Type', 'application/json')
->header('Content-Disposition', 'attachment; filename="'.$fileName.'"');
}
public function import(Request $request)
{
$request->validate([
'file' => 'required|file|mimes:json,txt'
]);
$file = $request->file('file');
$content = json_decode(file_get_contents($file), true);
$paletteName = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
$palette = Palette::firstOrCreate(['name' => $paletteName]);
foreach ($content as $color) {
PaletteColor::updateOrCreate(
['palette_id' => $palette->id, 'name' => $color['name']],
['hex_value' => $color['hex_value']]
);
}
return response()->json(['message' => 'Palette imported successfully', 'palette' => $palette]);
}
public function rename(Request $request, Palette $palette)
{
if ($palette->name === 'default-colors') {
return response()->json(['message' => 'Cannot rename the default palette'], 403);
}
$request->validate([
'name' => 'required|unique:palettes,name',
]);
$palette->name = $request->name;
$palette->save();
return response()->json($palette);
}
public function delete(Palette $palette)
{
if ($palette->name === 'default-colors') {
return response()->json(['message' => 'Cannot delete the default palette'], 403);
}
$palette->colors()->delete();
$palette->delete();
return response()->json(['message' => 'Palette deleted']);
}
}

View File

@ -7,7 +7,8 @@
use App\Models\TwitchUser;
use App\Models\CommandHistory;
use App\Models\CurrentState;
use App\Models\GameSession;
use App\Models\PaletteColor;
use App\Http\Controllers\GameSessionController;
use Illuminate\Support\Facades\Log;
class TwitchController extends Controller
@ -17,7 +18,7 @@ public function parseChatMessage(Request $request)
Log::info("Entered parseChatMessage");
$message = $request->input('message');
$username = $request->input('username');
$gameSession = $this->getCurrentGameSession();
$gameSession = app(GameSessionController::class)->getCurrentGameSession();
Log::debug('TwitchController::parseChatMessage', [
'message' => $message,
@ -34,67 +35,74 @@ public function parseChatMessage(Request $request)
return response()->json(['error' => 'No message, username, or valid game session provided'], 400);
}
$user = TwitchUser::firstOrCreate(['twitch_username' => $username]);
$user = TwitchUser::firstOrCreate(['username' => $username]);
$parsedCommand = TwitchHelper::parseMessage($message);
Log::debug('TwitchController::parseChatMessage - Parsed Command', ['parsedCommand' => $parsedCommand]);
if ($parsedCommand) {
$color = $this->getColorFromName($parsedCommand['color'], $gameSession->palette_id);
if (!$color) {
return response()->json(['error' => 'Invalid color'], 400);
}
CommandHistory::create([
'user_id' => $user->id,
'twitch_user_id' => $user->id,
'command' => $parsedCommand['command'],
'x' => $parsedCommand['x'] ?? null,
'y' => $parsedCommand['y'] ?? null,
'color' => $parsedCommand['color'] ?? null,
'palette_color_id' => $color->id,
'game_session_id' => $gameSession->id,
]);
switch ($parsedCommand['command']) {
case 'place':
$this->handlePlaceCommand($parsedCommand, $user->id);
$this->handlePlaceCommand($parsedCommand, $user->id, $gameSession->id, $color->id);
break;
// Add cases for other commands like 'row', 'column', 'fill'
}
return response()->json($parsedCommand);
return response()->json([
'command' => $parsedCommand,
'color' => [
'hex_value' => $color->hex_value
]
]);
}
Log::debug('TwitchController::parseChatMessage - Invalid command', ['message' => $message]);
return response()->json(['error' => 'Invalid command'], 400);
}
private function handlePlaceCommand($parsedCommand, $userId)
private function handlePlaceCommand($parsedCommand, $userId, $gameSessionId, $colorId)
{
Log::debug('TwitchController::handlePlaceCommand', ['command' => $parsedCommand]);
$x = $parsedCommand['x'];
$y = $parsedCommand['y'];
$color = $parsedCommand['color'];
$currentState = CurrentState::where('x', $x)->where('y', $y)->first();
$currentState = CurrentState::where('x', $x)
->where('y', $y)
->where('game_session_id', $gameSessionId)
->first();
Log::debug('TwitchController::handlePlaceCommand - Retrieved Current State', ['currentState' => $currentState]);
if ($currentState) {
$currentState->update(['color' => $color, 'updated_by' => $userId]);
$currentState->update(['palette_color_id' => $colorId, 'updated_by' => $userId, 'updated_at' => now()]);
} else {
CurrentState::create(['x' => $x, 'y' => $y, 'color' => $color, 'updated_by' => $userId]);
CurrentState::create([
'x' => $x,
'y' => $y,
'palette_color_id' => $colorId,
'updated_by' => $userId,
'updated_at' => now(),
'game_session_id' => $gameSessionId,
]);
}
}
private function getCurrentGameSession()
private function getColorFromName($colorName, $paletteId)
{
$gameSession = GameSession::whereNull('ended_at')->latest()->first();
if (!$gameSession) {
$gameSession = GameSession::create(['session_name' => 'Session ' . now()]);
}
return $gameSession;
}
public function getCurrentState()
{
$currentState = CurrentState::all();
return response()->json($currentState);
return PaletteColor::where('palette_id', $paletteId)->where('name', $colorName)->first();
}
}

View File

@ -9,7 +9,22 @@ class CommandHistory extends Model
{
use HasFactory;
protected $table = 'command_history';
protected $table = 'command_history'; // Specify the correct table name
protected $fillable = ['user_id', 'command', 'x', 'y', 'color', 'game_session_id'];
protected $fillable = ['twitch_user_id', 'game_session_id', 'x', 'y', 'palette_color_id', 'timestamp'];
public function user()
{
return $this->belongsTo(TwitchUser::class);
}
public function gameSession()
{
return $this->belongsTo(GameSession::class);
}
public function color()
{
return $this->belongsTo(PaletteColor::class);
}
}

View File

@ -11,5 +11,20 @@ class CurrentState extends Model
protected $table = 'current_state';
protected $fillable = ['x', 'y', 'color', 'updated_by'];
protected $fillable = ['game_session_id', 'x', 'y', 'palette_color_id', 'updated_by', 'updated_at'];
public function gameSession()
{
return $this->belongsTo(GameSession::class);
}
public function color()
{
return $this->belongsTo(PaletteColor::class, 'palette_color_id');
}
public function updatedBy()
{
return $this->belongsTo(TwitchUser::class, 'updated_by');
}
}

View File

@ -9,7 +9,25 @@ class GameSession extends Model
{
use HasFactory;
protected $fillable = ['session_name', 'ended_at'];
protected $fillable = ['session_name', 'palette_id', 'image_path', 'ended_at'];
protected $dates = ['ended_at'];
public function palette()
{
return $this->belongsTo(Palette::class);
}
public function commands()
{
return $this->hasMany(CommandHistory::class);
}
public function currentState()
{
return $this->hasMany(CurrentState::class);
}
public function winningState()
{
return $this->hasMany(WinningState::class);
}
}

View File

@ -9,5 +9,10 @@ class TwitchUser extends Model
{
use HasFactory;
protected $fillable = ['twitch_username'];
protected $fillable = ['username'];
public function commands()
{
return $this->hasMany(CommandHistory::class);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class WinningState extends Model
{
use HasFactory;
protected $fillable = ['game_session_id', 'x', 'y', 'palette_color_id'];
public function gameSession()
{
return $this->belongsTo(GameSession::class);
}
public function color()
{
return $this->belongsTo(PaletteColor::class);
}
}

View File

@ -1,36 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddEndedAtToGameSessionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('game_sessions', function (Blueprint $table) {
if (!Schema::hasColumn('game_sessions', 'ended_at')) {
$table->timestamp('ended_at')->nullable();
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('game_sessions', function (Blueprint $table) {
if (Schema::hasColumn('game_sessions', 'ended_at')) {
$table->dropColumn('ended_at');
}
});
}
}

View File

@ -10,7 +10,7 @@ public function up()
{
Schema::create('twitch_users', function (Blueprint $table) {
$table->id();
$table->string('twitch_username')->unique();
$table->string('username')->unique();
$table->timestamps();
});
}

View File

@ -4,21 +4,19 @@
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSourceImagesTable extends Migration
class CreatePalettesTable extends Migration
{
public function up()
{
Schema::create('source_images', function (Blueprint $table) {
Schema::create('palettes', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->json('pixel_state');
$table->json('palette');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('source_images');
Schema::dropIfExists('palettes');
}
}

View File

@ -4,21 +4,10 @@
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePalettesTable extends Migration
class CreatePaletteColorsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('palettes', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
Schema::create('palette_colors', function (Blueprint $table) {
$table->id();
$table->foreignId('palette_id')->constrained()->onDelete('cascade');
@ -65,14 +54,8 @@ public function up()
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('palette_colors');
Schema::dropIfExists('palettes');
}
}

View File

@ -11,6 +11,9 @@ public function up()
Schema::create('game_sessions', function (Blueprint $table) {
$table->id();
$table->string('session_name');
$table->foreignId('palette_id')->constrained('palettes')->onDelete('cascade')->default(1);
$table->string('image_path')->nullable();
$table->timestamp('ended_at')->nullable();
$table->timestamps();
});
}

View File

@ -6,30 +6,19 @@
class CreateCommandHistoryTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('command_history', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('twitch_users')->onDelete('cascade');
$table->string('command');
$table->string('x')->nullable();
$table->string('y')->nullable();
$table->string('color')->nullable();
$table->foreignId('twitch_user_id')->constrained('twitch_users')->onDelete('cascade');
$table->foreignId('game_session_id')->constrained('game_sessions')->onDelete('cascade');
$table->string('x');
$table->string('y');
$table->foreignId('palette_color_id')->constrained('palette_colors')->onDelete('cascade');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('command_history');

View File

@ -6,28 +6,19 @@
class CreateCurrentStateTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('current_state', function (Blueprint $table) {
$table->id();
$table->foreignId('game_session_id')->constrained('game_sessions')->onDelete('cascade');
$table->string('x');
$table->string('y');
$table->string('color');
$table->foreignId('palette_color_id')->constrained('palette_colors')->onDelete('cascade');
$table->foreignId('updated_by')->constrained('twitch_users')->onDelete('cascade');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('current_state');

View File

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateWinningStateTable extends Migration
{
public function up()
{
Schema::create('winning_state', function (Blueprint $table) {
$table->id();
$table->foreignId('game_session_id')->constrained('game_sessions')->onDelete('cascade');
$table->string('x');
$table->string('y');
$table->foreignId('palette_color_id')->constrained('palette_colors')->onDelete('cascade');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('winning_state');
}
}

View File

View File

@ -1,6 +1,17 @@
.color-picker {
.pickr {
width: 100%;
height: 36px;
}
.pickr .pcr-button {
border: 1px solid black;
border-radius: 4px;
width: 100%;
height: 36px;
}
#rename-palette-name {
border-radius: 25px;
text-align: center;
vertical-align: middle;
}

View File

View File

@ -1,39 +1,12 @@
function colorCell(cellName, colorName) {
console.log(`colorCell called with: ${cellName}, ${colorName}`);
// Map of valid named colors to their hex values
const validColors = {
"white": "#FFFFFF",
"lightGray": "#E4E4E4",
"mediumGray": "#888888",
"darkGray": "#222222",
"pink": "#FFA7D1",
"red": "#E50000",
"orange": "#E59500",
"brown": "#A06A42",
"yellow": "#E5D900",
"lightGreen": "#94E044",
"green": "#02BE01",
"cyan": "#00D3DD",
"blue": "#0083C7",
"darkBlue": "#0000EA",
"purple": "#CF6EE4",
"darkPurple": "#820080",
"black": "#000000"
};
// Check if the provided color name is valid
if (!validColors[colorName]) {
console.warn(`Invalid color name: ${colorName}. Please use a valid named color.`);
return;
}
function colorCell(cellName, hexValue) {
console.log(`colorCell called with: ${cellName}, ${hexValue}`);
// Find the cell using its class name
const cell = document.querySelector(`.cell.${cellName}`);
// If the cell exists, change its background color to the corresponding hex value
// If the cell exists, change its background color to the provided hex value
if (cell) {
cell.style.backgroundColor = validColors[colorName];
cell.style.backgroundColor = hexValue;
} else {
console.warn(`Cell ${cellName} not found.`);
}
@ -84,7 +57,11 @@ document.addEventListener('DOMContentLoaded', function() {
.then(response => response.json())
.then(data => {
data.forEach(pixel => {
colorCell(`${pixel.x}${pixel.y}`, pixel.color);
if (pixel.color && pixel.color.hex_value) {
colorCell(`${pixel.x}${pixel.y}`, pixel.color.hex_value);
} else {
console.warn(`No color found for pixel at ${pixel.x}${pixel.y}`);
}
});
})
.catch(error => {

View File

@ -1,14 +1,45 @@
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM fully loaded and parsed.');
const paletteSelector = document.getElementById('palette-selector');
const newPaletteButton = document.getElementById('new-palette');
const newPaletteForm = document.getElementById('new-palette-form');
const createPaletteForm = document.getElementById('create-palette-form');
const addColorForm = document.getElementById('add-color-form');
const createColorForm = document.getElementById('create-color-form');
const paletteColorsDiv = document.getElementById('palette-colors');
const colorEditorsDiv = document.getElementById('color-editors');
const addColorButton = document.getElementById('add-color');
const savePaletteButton = document.getElementById('save-palette');
const exportPaletteButton = document.getElementById('export-palette');
const importPaletteButton = document.getElementById('import-palette');
const importFileInput = document.getElementById('import-file');
const renamePaletteButton = document.getElementById('rename-palette');
const renamePaletteNameInput = document.getElementById('rename-palette-name');
const deletePaletteButton = document.getElementById('delete-palette');
const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'), {});
const confirmDeleteCheckbox = document.getElementById('confirmDeleteCheckbox');
const confirmDeleteButton = document.getElementById('confirmDeleteButton');
// CSRF token
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
// Logging element states
console.log('paletteSelector:', paletteSelector);
console.log('newPaletteButton:', newPaletteButton);
console.log('newPaletteForm:', newPaletteForm);
console.log('createPaletteForm:', createPaletteForm);
console.log('addColorForm:', addColorForm);
console.log('createColorForm:', createColorForm);
console.log('colorEditorsDiv:', colorEditorsDiv);
console.log('addColorButton:', addColorButton);
console.log('savePaletteButton:', savePaletteButton);
console.log('deletePaletteButton:', deletePaletteButton);
console.log('csrfToken:', csrfToken);
if (!paletteSelector || !newPaletteButton || !newPaletteForm || !createPaletteForm || !addColorForm || !createColorForm || !colorEditorsDiv || !addColorButton || !savePaletteButton || !exportPaletteButton || !importPaletteButton || !importFileInput || !renamePaletteButton || !renamePaletteNameInput || !deletePaletteButton) {
console.error('Missing required elements on the page.');
return;
}
newPaletteButton.addEventListener('click', function() {
newPaletteForm.style.display = 'block';
@ -17,28 +48,40 @@ document.addEventListener('DOMContentLoaded', function() {
createPaletteForm.addEventListener('submit', function(event) {
event.preventDefault();
const formData = new FormData(createPaletteForm);
console.log('Creating new palette with data:', formData);
fetch('/palette-editor', {
method: 'POST',
body: formData
body: formData,
headers: {
'X-CSRF-TOKEN': csrfToken
}
})
.then(response => response.json())
.then(data => {
alert('Palette created: ' + data.name);
paletteSelector.innerHTML += `<option value="${data.id}">${data.name}</option>`;
console.log('Palette created:', data);
fetchPalettes().then(() => {
const newOption = document.createElement('option');
newOption.value = data.id;
newOption.text = data.name;
paletteSelector.add(newOption);
paletteSelector.value = data.id;
loadPaletteColors(data.id);
});
newPaletteForm.style.display = 'none';
createPaletteForm.reset();
})
.catch(error => {
console.error('Error:', error);
console.error('Error creating palette:', error);
});
});
paletteSelector.addEventListener('change', function() {
const paletteId = paletteSelector.value;
console.log('Palette selected:', paletteId);
if (paletteId) {
loadPaletteColors(paletteId);
checkPaletteRestrictions();
} else {
paletteColorsDiv.innerHTML = '';
addColorForm.style.display = 'none';
colorEditorsDiv.innerHTML = '';
}
@ -51,26 +94,38 @@ document.addEventListener('DOMContentLoaded', function() {
savePaletteButton.addEventListener('click', function() {
const paletteId = createColorForm.getAttribute('data-palette-id');
const colorEditors = document.querySelectorAll('.color-editor');
const colorUpdates = [];
colorEditors.forEach(editor => {
const colorId = editor.getAttribute('data-color-id');
const colorName = editor.querySelector('.color-name').value;
const colorHex = editor.querySelector('.color-hex-input').value;
colorUpdates.push({
colorId,
colorName,
colorHex
});
});
console.log('Saving palette with ID:', paletteId);
colorUpdates.forEach(({colorId, colorName, colorHex}) => {
console.log(`Saving color: ${colorName} (${colorHex}) with ID: ${colorId}`);
if (colorId) {
// Update existing color
fetch(`/palette-editor/${paletteId}/colors/${colorId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({ name: colorName, hex_value: colorHex })
})
.then(response => response.json())
.then(data => {
alert('Color updated: ' + data.name);
console.log('Color updated:', data);
})
.catch(error => {
console.error('Error:', error);
console.error('Error updating color:', error);
});
} else {
// Add new color
@ -78,27 +133,185 @@ document.addEventListener('DOMContentLoaded', function() {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({ name: colorName, hex_value: colorHex })
})
.then(response => response.json())
.then(data => {
alert('Color added: ' + data.name);
console.log('Color added:', data);
editor.setAttribute('data-color-id', data.id);
})
.catch(error => {
console.error('Error:', error);
console.error('Error adding color:', error);
});
}
});
});
exportPaletteButton.addEventListener('click', function() {
const paletteId = paletteSelector.value;
console.log('Exporting palette with ID:', paletteId);
if (paletteId) {
fetch(`/palette-editor/${paletteId}/export`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.blob();
})
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `${paletteSelector.options[paletteSelector.selectedIndex].text}.pal`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
})
.catch(error => {
console.error('Error exporting palette:', error);
});
} else {
alert('Please select a palette to export.');
}
});
importPaletteButton.addEventListener('click', function() {
importFileInput.click();
});
importFileInput.addEventListener('change', function(event) {
const file = event.target.files[0];
if (file) {
const formData = new FormData();
formData.append('file', file);
console.log('Importing palette from file:', file.name);
fetch('/palette-editor/import', {
method: 'POST',
body: formData,
headers: {
'X-CSRF-TOKEN': csrfToken
}
})
.then(response => response.json())
.then(data => {
console.log('Palette imported successfully', data);
const newOption = document.createElement('option');
newOption.value = data.palette.id;
newOption.text = data.palette.name;
paletteSelector.add(newOption);
paletteSelector.value = data.palette.id;
loadPaletteColors(data.palette.id);
})
.catch(error => {
console.error('Error importing palette:', error);
});
}
});
renamePaletteButton.addEventListener('click', function() {
const paletteId = paletteSelector.value;
const newName = renamePaletteNameInput.value.trim();
console.log('Renaming palette ID:', paletteId, 'to:', newName);
if (!paletteId) {
alert('Please select a palette to rename.');
return;
}
if (!newName) {
alert('Please enter a new name for the palette.');
return;
}
fetch(`/palette-editor/${paletteId}/rename`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({ name: newName })
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('Palette renamed:', data);
fetchPalettes().then(() => {
const option = Array.from(paletteSelector.options).find(option => option.value == paletteId);
if (option) {
option.text = newName;
paletteSelector.value = paletteId;
}
loadPaletteColors(paletteId);
});
})
.catch(error => {
console.error('Error renaming palette:', error);
});
});
deletePaletteButton.addEventListener('click', function() {
const paletteId = paletteSelector.value;
console.log('Preparing to delete palette ID:', paletteId);
if (!paletteId) {
alert('Please select a palette to delete.');
return;
}
// Show modal
deleteModal.show();
});
confirmDeleteCheckbox.addEventListener('change', function() {
confirmDeleteButton.disabled = !confirmDeleteCheckbox.checked;
});
confirmDeleteButton.addEventListener('click', function() {
const paletteId = paletteSelector.value;
console.log('Deleting palette with ID:', paletteId);
if (paletteId && confirmDeleteCheckbox.checked) {
fetch(`/palette-editor/${paletteId}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': csrfToken
}
})
.then(response => response.json())
.then(data => {
console.log('Palette deleted:', data);
fetchPalettes().then(() => {
const option = Array.from(paletteSelector.options).find(option => option.value == paletteId);
if (option) {
paletteSelector.removeChild(option);
}
const defaultPaletteOption = Array.from(paletteSelector.options).find(option => option.text === 'default-colors');
if (defaultPaletteOption) {
defaultPaletteOption.selected = true;
loadPaletteColors(defaultPaletteOption.value);
} else {
paletteSelector.value = '';
colorEditorsDiv.innerHTML = '';
}
});
deleteModal.hide();
})
.catch(error => {
console.error('Error deleting palette:', error);
});
}
});
function loadPaletteColors(paletteId) {
console.log('Loading colors for palette ID:', paletteId);
fetch(`/palette-editor/${paletteId}/colors`)
.then(response => response.json())
.then(data => {
paletteColorsDiv.innerHTML = '<h2>Colors in Palette</h2>';
console.log('Loaded palette colors:', data);
colorEditorsDiv.innerHTML = '';
data.forEach(color => {
addColorEditor(color.id, color.name, color.hex_value);
@ -107,11 +320,12 @@ document.addEventListener('DOMContentLoaded', function() {
createColorForm.setAttribute('data-palette-id', paletteId);
})
.catch(error => {
console.error('Error:', error);
console.error('Error loading palette colors:', error);
});
}
function addColorEditor(colorId = '', colorName = '', colorHex = '#ffffff') {
console.log(`Adding color editor for color: ${colorName} (${colorHex}) with ID: ${colorId}`);
const editor = document.createElement('div');
editor.className = 'color-editor col-md-2 mb-3';
editor.setAttribute('data-color-id', colorId);
@ -135,21 +349,26 @@ document.addEventListener('DOMContentLoaded', function() {
const deleteButton = editor.querySelector('.delete-color');
deleteButton.addEventListener('click', function() {
const paletteId = createColorForm.getAttribute('data-palette-id');
if (paletteId === '1') {
alert('Cannot delete colors from the default-colors palette.');
return;
}
console.log('Deleting color ID:', colorId, 'from palette ID:', paletteId);
if (colorId) {
const paletteId = createColorForm.getAttribute('data-palette-id');
fetch(`/palette-editor/${paletteId}/colors/${colorId}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
'X-CSRF-TOKEN': csrfToken
}
})
.then(response => response.json())
.then(data => {
alert('Color deleted: ' + data.message);
console.log('Color deleted:', data.message);
colorEditorsDiv.removeChild(editor);
})
.catch(error => {
console.error('Error:', error);
console.error('Error deleting color:', error);
});
} else {
colorEditorsDiv.removeChild(editor);
@ -184,8 +403,49 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
function fetchPalettes() {
console.log('Fetching palettes from the server');
return fetch('/palette-editor')
.then(response => response.json())
.then(data => {
console.log('Fetched palettes:', data);
paletteSelector.innerHTML = '';
data.forEach(palette => {
const option = document.createElement('option');
option.value = palette.id;
option.text = palette.name;
paletteSelector.add(option);
});
})
.catch(error => {
console.error('Error fetching palettes:', error);
});
}
function checkPaletteRestrictions() {
const paletteId = paletteSelector.value;
const isDefaultPalette = paletteId === '1';
console.log('Checking restrictions for palette ID:', paletteId, 'isDefaultPalette:', isDefaultPalette);
deletePaletteButton.disabled = isDefaultPalette;
addColorButton.disabled = isDefaultPalette;
savePaletteButton.disabled = isDefaultPalette;
if (isDefaultPalette) {
document.querySelectorAll('.delete-color').forEach(button => {
button.disabled = true;
});
} else {
document.querySelectorAll('.delete-color').forEach(button => {
button.disabled = false;
});
}
}
// Load default colors on page load
window.onload = function() {
console.log('Page loaded, selecting default palette if available');
const defaultPaletteOption = Array.from(paletteSelector.options).find(option => option.text === 'default-colors');
if (defaultPaletteOption) {
defaultPaletteOption.selected = true;

View File

@ -46,7 +46,8 @@ function connectWebSocket() {
console.log('Chat Message:', chatMessage);
console.log('Username:', username);
const commandPattern = /^(?:!place|!p|!paint)\s+([A-P])\s*(\d{1,2})\s+(\w+)$/i;
// Update the regular expression to handle color names with hyphens
const commandPattern = /^(?:!place|!p|!paint)\s+([A-P])\s*(\d{1,2})\s+([\w-]+)$/i;
const commandMatch = chatMessage.match(commandPattern);
console.log('Command Match:', commandMatch);
if (commandMatch) {
@ -114,9 +115,9 @@ function sendPlaceCommand(chatMessage, username, x, y, color) {
.then(response => response.json())
.then(data => {
console.log('Server Response:', data);
if (data && data.command === 'place') {
if (data && data.command.command === 'place') {
console.log('Parsed Place Command from server:', data);
colorCell(`${data.x}${data.y}`, data.color);
colorCell(`${data.command.x}${data.command.y}`, data.color.hex_value);
} else {
console.error('Invalid command in server response.');
}

View File

@ -0,0 +1,87 @@
@extends('layouts.administration-layout')
@section('title', 'Administration')
@section('content')
<div class="container mt-5">
<h1 class="mb-4">Administration Dashboard</h1>
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
@if(session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
<div class="card bg-secondary text-light mb-4">
<div class="card-header bg-dark">
<h2>Current Game</h2>
</div>
<div class="card-body">
@if($currentGame)
<p><strong>Session Name:</strong> {{ $currentGame->session_name }}</p>
<p><strong>Current Palette:</strong> {{ $currentGame->palette ? $currentGame->palette->name : 'None' }}</p>
<div class="mt-3">
<h3>Current State</h3>
<div class="grid-container">
@foreach($currentState as $state)
<div class="grid-item" style="background-color: {{ $state->color->hex_value }};">
({{ $state->x }}, {{ $state->y }})
</div>
@endforeach
</div>
</div>
@else
<p>No active game session.</p>
@endif
</div>
</div>
<div class="card bg-secondary text-light mb-4">
<div class="card-header bg-dark">
<h2>Change Palette</h2>
</div>
<div class="card-body">
<form action="{{ route('administration.changePalette') }}" method="POST">
@csrf
<div class="form-group">
<label for="palette_id_change">Select Palette:</label>
<select id="palette_id_change" name="palette_id" class="form-control bg-dark text-light" required>
@foreach($palettes as $palette)
<option value="{{ $palette->id }}" {{ $currentGame && $currentGame->palette_id == $palette->id ? 'selected' : '' }}>
{{ $palette->name }}
</option>
@endforeach
</select>
</div>
<button type="submit" class="btn btn-primary btn-block">Change Palette</button>
</form>
</div>
</div>
<div class="card bg-secondary text-light mb-4">
<div class="card-header bg-dark">
<h2>Create New Game</h2>
</div>
<div class="card-body">
<form id="createGameForm" action="{{ route('administration.createGame') }}" method="POST">
@csrf
<div class="form-group">
<label for="session_name">Session Name:</label>
<input type="text" id="session_name" name="session_name" class="form-control bg-dark text-light" required>
</div>
<div class="form-group">
<label for="palette_id_create">Select Palette:</label>
<select id="palette_id_create" name="palette_id" class="form-control bg-dark text-light" required>
@foreach($palettes as $palette)
<option value="{{ $palette->id }}">{{ $palette->name }}</option>
@endforeach
</select>
</div>
<button type="submit" class="btn btn-success btn-block">Create Game</button>
</form>
</div>
</div>
</div>
@endsection

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'Administration')</title>
<!-- Bootstrap CSS CDN -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome CDN -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" rel="stylesheet">
@vite(['resources/css/app.css', 'resources/css/administration.css', 'resources/js/administration.js'])
</head>
<body class="bg-dark text-light">
<div class="container mt-5">
@yield('content')
</div>
<!-- Bootstrap JS and dependencies -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<!-- Font Awesome JS CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/js/all.min.js"></script>
</body>
</html>

View File

@ -10,6 +10,7 @@
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" rel="stylesheet">
<!-- Pickr.js CSS CDN -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/themes/classic.min.css">
<meta name="csrf-token" content="{{ csrf_token() }}">
@vite(['resources/css/app.css', 'resources/css/palette-editor.css', 'resources/js/palette-editor.js'])
</head>
<body>
@ -17,7 +18,7 @@
@yield('content')
</div>
<!-- Bootstrap JS and dependencies -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<!-- Font Awesome JS CDN -->

View File

@ -3,39 +3,78 @@
@section('title', 'Palette Editor')
@section('content')
<div class="mt-5">
<h1>Palette Editor</h1>
<div class="form-group">
<label for="palette-selector">Select a Palette:</label>
<select id="palette-selector" class="form-control">
<option value="">Select a palette</option>
@foreach($palettes as $palette)
<option value="{{ $palette->id }}">{{ $palette->name }}</option>
@endforeach
</select>
</div>
<button id="new-palette" class="btn btn-primary">New Palette</button>
<div id="palette-colors" class="mt-4"></div>
</div>
<div id="new-palette-form" style="display:none;" class="mt-5">
<h2>Create New Palette</h2>
<form id="create-palette-form">
<div class="container">
<div class="mt-5">
<h1>Palette Editor</h1>
<div class="form-group">
<label for="palette-name">Palette Name:</label>
<input type="text" id="palette-name" name="name" class="form-control" required>
<label for="palette-selector">Select a Palette:</label>
<select id="palette-selector" class="form-control">
@foreach($palettes as $palette)
<option value="{{ $palette->id }}">{{ $palette->name }}</option>
@endforeach
</select>
</div>
<button type="submit" class="btn btn-success">Create</button>
</form>
<div class="mt-3">
<button id="new-palette" class="btn btn-primary">New Palette</button>
<button id="export-palette" class="btn btn-secondary">Export Palette</button>
<button id="import-palette" class="btn btn-secondary">Import Palette</button>
<input type="file" id="import-file" style="display:none;">
<button id="rename-palette" class="btn btn-secondary">Rename Palette</button>
<input type="text" id="rename-palette-name" placeholder="New Palette Name" class="form-control d-inline-block" style="width: auto; border-radius: 0.5rem; text-align: center;">
<button id="delete-palette" class="btn btn-danger">Delete Palette</button>
</div>
</div>
<div id="new-palette-form" style="display:none;" class="mt-5">
<h2>Create New Palette</h2>
<form id="create-palette-form">
<div class="form-group">
<label for="palette-name">Palette Name:</label>
<input type="text" id="palette-name" name="name" class="form-control" required>
</div>
<button type="submit" class="btn btn-success">Create</button>
</form>
</div>
<div id="add-color-form" style="display:none;" class="mt-5">
<h2>Add Color to Palette</h2>
<button id="add-color" class="btn btn-success mb-3"><i class="fas fa-plus"></i> Add Color</button>
<form id="create-color-form">
<div id="color-editors" class="row"></div>
</form>
</div>
<button id="save-palette" class="btn btn-success" style="position: fixed; bottom: 20px; right: 20px;">Save</button>
</div>
<div id="add-color-form" style="display:none;" class="mt-5">
<h2>Add Color to Palette</h2>
<button id="add-color" class="btn btn-success mb-3"><i class="fas fa-plus"></i> Add Color</button>
<form id="create-color-form">
<div id="color-editors" class="row"></div>
</form>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">Confirm Delete</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete this palette? This action cannot be undone.</p>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="confirmDeleteCheckbox">
<label class="form-check-label" for="confirmDeleteCheckbox">
Confirm delete
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDeleteButton" disabled>Delete</button>
</div>
</div>
</div>
</div>
<button id="save-palette" class="btn btn-success" style="position: fixed; bottom: 20px; right: 20px;">Save</button>
@endsection
@section('scripts')
@vite(['resources/js/palette-editor.js'])
@endsection

View File

@ -5,26 +5,44 @@
use App\Http\Controllers\TwitchController;
use App\Http\Controllers\GameSessionController;
use App\Http\Controllers\PaletteController;
use App\Http\Controllers\CommandController;
use App\Http\Controllers\CurrentStateController;
use App\Http\Controllers\AdministrationController;
// Administration routes
Route::get('/administration', [AdministrationController::class, 'index'])->name('administration.index');
Route::post('/administration/reset-board', [AdministrationController::class, 'resetBoard'])->name('administration.resetBoard');
Route::post('/administration/change-palette', [AdministrationController::class, 'changePalette'])->name('administration.changePalette');
Route::post('/administration/create-game', [AdministrationController::class, 'createGame'])->name('administration.createGame');
// Command parsing route
Route::post('/command/parse', [CommandController::class, 'parseChatMessage']);
// Current state route
Route::get('/api/current-state', [CurrentStateController::class, 'index']);
// Palette Editor routes
Route::get('/palette-editor', [PaletteController::class, 'index']);
Route::post('/palette-editor', [PaletteController::class, 'store']);
Route::post('/palette-editor/{palette}/rename', [PaletteController::class, 'rename']);
Route::delete('/palette-editor/{palette}', [PaletteController::class, 'delete']);
Route::get('/palette-editor/{palette}/colors', [PaletteController::class, 'getColors']);
Route::post('/palette-editor/{palette}/colors', [PaletteController::class, 'addColor']);
Route::put('/palette-editor/{palette}/colors/{color}', [PaletteController::class, 'updateColor']);
Route::delete('/palette-editor/{palette}/colors/{color}', [PaletteController::class, 'deleteColor']);
Route::get('/palette-editor/{palette}/colors', [PaletteController::class, 'getColors']);
Route::get('/palette-editor/{palette}/export', [PaletteController::class, 'export']);
Route::post('/palette-editor/import', [PaletteController::class, 'import']);
// Twitch chat message parsing route
Route::post('/twitch/parse', [TwitchController::class, 'parseChatMessage']);
// Game session routes
Route::post('/game-session/start', [GameSessionController::class, 'start']);
Route::post('/game-session/end', [GameSessionController::class, 'end']);
Route::post('/game-session/end/{id}', [GameSessionController::class, 'end']);
Route::get('/game-session/current', [GameSessionController::class, 'current']);
Route::get('/api/current-state', [TwitchController::class, 'getCurrentState']);
// Static views
Route::get('/', function () {
return view('welcome');
});
@ -41,10 +59,13 @@
return view('testing');
})->middleware(['auth', 'verified'])->name('testing');
// Authenticated user profile routes
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
Route::get('/register', [ProfileController::class, 'register'])->name('register');
Route::post('/register', [ProfileController::class, 'store'])->name('register.store');
});
require __DIR__.'/auth.php';

View File

@ -7,7 +7,7 @@ export default defineConfig(({ mode }) => {
return {
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js', 'resources/css/grid.css', 'resources/js/grid.js', 'resources/js/twitch.js', 'resources/js/palette-editor.js', 'resources/css/palette-editor.css'],
input: ['resources/css/app.css', 'resources/js/app.js', 'resources/css/grid.css', 'resources/js/grid.js', 'resources/js/twitch.js', 'resources/js/palette-editor.js', 'resources/css/palette-editor.css', 'resources/css/administration.css', 'resources/js/administration.js',],
refresh: true,
}),
],