diff --git a/api/bootstrap.php b/api/bootstrap.php
index 1b8268f..d0147d4 100644
--- a/api/bootstrap.php
+++ b/api/bootstrap.php
@@ -30,4 +30,10 @@ function env(string $key): mixed {
defined('YII_DEBUG') or define('YII_DEBUG', filter_var(env('YII_DEBUG') ?? false, FILTER_VALIDATE_BOOLEAN));
defined('YII_ENV') or define('YII_ENV', env('YII_ENV') ?? 'prod');
+if (YII_ENV === 'dev') {
+ ini_set('display_errors', '1');
+ ini_set('display_startup_errors', '1');
+ error_reporting(E_ALL);
+}
+
require __DIR__ . '/vendor/yiisoft/yii2/Yii.php';
diff --git a/api/commands/PokemonImportController.php b/api/commands/PokemonImportController.php
index 9decd53..10e5d1b 100644
--- a/api/commands/PokemonImportController.php
+++ b/api/commands/PokemonImportController.php
@@ -46,8 +46,13 @@ public function actionFetch(): int
foreach ($data['results'] as $pokemon) {
try {
$species = new PokemonSpecies();
+ $url = $pokemon['url'];
+ $imageId = basename($url);
+
$species->name = $pokemon['name'];
- $species->url = $pokemon['url'];
+ $species->url = $url;
+ $species->image = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/' . $imageId . '.svg';
+
if (!$species->save()) {
$this->stderr($pokemon['name'] . " (FAILED): " . json_encode($species->errors, JSON_THROW_ON_ERROR) . "\n");
} else {
diff --git a/api/config/web.php b/api/config/web.php
index bf5942d..9aac4ce 100644
--- a/api/config/web.php
+++ b/api/config/web.php
@@ -1,5 +1,7 @@
'@vendor/npm-asset',
],
'components' => [
+ 'response' => [
+ 'format' => \yii\web\Response::FORMAT_JSON,
+ ],
'request' => [
// !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
'cookieValidationKey' => 'my-secret-key',
+ 'enableCsrfCookie' => false,
+ 'enableCsrfValidation' => false,
+ 'parsers' => ['application/json' => JsonParser::class],
],
'cache' => [
'class' => FileCache::class,
],
'user' => [
'identityClass' => User::class,
- 'enableAutoLogin' => true,
+ 'loginUrl' => null,
],
'mailer' => [
'class' => \yii\symfonymailer\Mailer::class,
@@ -40,8 +48,19 @@
'targets' => [
[
'class' => FileTarget::class,
+ 'logFile' => '@runtime/logs/error.log',
'levels' => ['error', 'warning'],
],
+ [
+ 'class' => FileTarget::class,
+ 'logFile' => '@runtime/logs/info.log',
+ 'levels' => ['info'],
+ ],
+ [
+ 'class' => FileTarget::class,
+ 'logFile' => '@runtime/logs/debug.log',
+ 'levels' => ['trace'],
+ ],
],
],
'db' => $db,
diff --git a/api/controllers/AuthController.php b/api/controllers/AuthController.php
new file mode 100644
index 0000000..952b582
--- /dev/null
+++ b/api/controllers/AuthController.php
@@ -0,0 +1,88 @@
+load(Yii::$app->request->getBodyParams(), '');
+
+ if (!$isCorrectBody)
+ {
+ throw new BadRequestHttpException('Request body is malformed');
+ }
+
+ $isValid = $loginForm->validate();
+
+ if (!$isValid)
+ {
+ return $loginForm;
+ }
+
+ $user = User::find()->where([
+ 'username' => $loginForm->username
+ ])->one();
+
+ if (!$user)
+ {
+ throw new ServerErrorHttpException('User no longer exists');
+ }
+
+ $isLoggedIn = Yii::$app->user->login($user);
+
+ if (!$isLoggedIn)
+ {
+ throw new ServerErrorHttpException('Failed to login user');
+ }
+
+ return null;
+ }
+
+ public function behaviors(): array
+ {
+ return ArrayHelper::merge(parent::behaviors(), [
+ 'authenticator' => [
+ 'except' => ['login']
+ ]
+ ]);
+ }
+
+ /**
+ * Destroys a user session, and logs the user out of the application.
+ *
+ * @return null
+ * @throws ServerErrorHttpException Failed to log out user
+ */
+ public function actionLogout(): null
+ {
+ $isLoggedOut = Yii::$app->user->logout();
+ if (!$isLoggedOut)
+ {
+ throw new ServerErrorHttpException('Failed to log out user');
+ }
+ return null;
+ }
+}
diff --git a/api/controllers/BoxesController.php b/api/controllers/BoxesController.php
index e316a09..4cb5fda 100644
--- a/api/controllers/BoxesController.php
+++ b/api/controllers/BoxesController.php
@@ -18,4 +18,15 @@ class BoxesController extends ActiveController
* @var string The model class associated with this controller.
*/
public $modelClass = Box::class;
+
+ /**
+ * Returns the total number of boxes belonging to this user
+ *
+ * @param int $userId
+ * @return int
+ */
+ public function actionUsersCount(int $userId): int
+ {
+ return $this->modelClass::find()->where(['user_id' => $userId])->count();
+ }
}
diff --git a/api/controllers/CategoriesController.php b/api/controllers/CategoriesController.php
index e1767d4..d42dd0b 100644
--- a/api/controllers/CategoriesController.php
+++ b/api/controllers/CategoriesController.php
@@ -3,6 +3,7 @@
namespace app\controllers;
use app\models\Category;
+use app\models\Team;
use app\rest\ActiveController;
/**
@@ -18,4 +19,38 @@ class CategoriesController extends ActiveController
* @var string The model class associated with this controller.
*/
public $modelClass = Category::class;
+ public string $teamClass = Team::class;
+
+ /**
+ * Returns the total number of categories belonging to this user
+ *
+ * @param int $userId
+ * @return int
+ */
+ public function actionUsersCount(int $userId): int // todo: move this to UsersController
+ {
+ return $this->modelClass::find()->where(['user_id' => $userId])->count();
+ }
+
+ /**
+ * Returns the total number of teams in this category
+ *
+ * @param int $categoryId
+ * @return int
+ */
+ public function actionTeamsCount(int $categoryId): int
+ {
+ return $this->teamClass::find()->where(['category_id' => $categoryId])->count();
+ }
+
+ /**
+ * Returns the teams in this category
+ *
+ * @param int $categoryId
+ * @return array
+ */
+ public function actionTeamsIndex(int $categoryId): array
+ {
+ return $this->teamClass::find()->where(['category_id' => $categoryId])->all();
+ }
}
diff --git a/api/controllers/HealthController.php b/api/controllers/HealthController.php
index 72bf4e0..ee8d098 100644
--- a/api/controllers/HealthController.php
+++ b/api/controllers/HealthController.php
@@ -3,8 +3,10 @@
namespace app\controllers;
use Yii;
+use yii\filters\AccessControl;
use yii\filters\Cors;
use app\rest\Controller;
+use yii\helpers\ArrayHelper;
use yii\web\Response;
/**
@@ -14,6 +16,22 @@
*/
class HealthController extends Controller
{
+ // todo: make a second version that doesnt require login
+ public function behaviors(): array
+ {
+ return ArrayHelper::merge(parent::behaviors(), [
+ 'access' => [
+ 'class' => AccessControl::class,
+ 'rules' => [
+ [
+ 'roles' => ['@'],
+ 'allow' => true,
+ ],
+ ],
+ ],
+ ]);
+ }
+
/**
* Simple server health check.
*
diff --git a/api/controllers/PokemonInstancesController.php b/api/controllers/PokemonInstancesController.php
index bd1cfdf..3aeb725 100644
--- a/api/controllers/PokemonInstancesController.php
+++ b/api/controllers/PokemonInstancesController.php
@@ -3,7 +3,7 @@
namespace app\controllers;
use app\models\PokemonInstance;
-use yii\rest\ActiveController;
+use app\rest\ActiveController;
/**
* Class PokemonInstancesController
@@ -12,7 +12,7 @@
*
* @see PokemonInstance
*/
-class PokemonInstancesController extends activeController
+class PokemonInstancesController extends ActiveController
{
/**
* @var string The model class associated with this controller.
diff --git a/api/controllers/PokemonSpeciesController.php b/api/controllers/PokemonSpeciesController.php
index 7022944..25b3161 100644
--- a/api/controllers/PokemonSpeciesController.php
+++ b/api/controllers/PokemonSpeciesController.php
@@ -3,7 +3,8 @@
namespace app\controllers;
use app\models\PokemonSpecies;
-use yii\rest\ActiveController;
+use app\rest\ActiveController;
+use yii\helpers\ArrayHelper;
/**
* Class PokemonSpeciesController
@@ -18,4 +19,23 @@ class PokemonSpeciesController extends ActiveController
* @var string The model class associated with this controller.
*/
public $modelClass = PokemonSpecies::class;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function actions(): array
+ {
+ return ArrayHelper::merge(parent::actions(), [
+ 'index' => [
+ 'pagination' => [
+ 'pageSizeLimit' => [1,10000],
+ ]
+ ]
+ ]);
+ }
+
+ public function actionCount(): int
+ {
+ return $this->modelClass::find()->count();
+ }
}
diff --git a/api/controllers/TeamsController.php b/api/controllers/TeamsController.php
index 1cddb8b..871a171 100644
--- a/api/controllers/TeamsController.php
+++ b/api/controllers/TeamsController.php
@@ -2,9 +2,12 @@
namespace app\controllers;
+use app\models\PokemonInstance;
+use app\models\PokemonSpecies;
use app\models\Team;
use app\rest\ActiveController;
use Yii;
+use yii\db\ActiveQuery;
/**
* Class TeamsController
@@ -19,4 +22,41 @@ class TeamsController extends ActiveController
* @var string The model class associated with this controller.
*/
public $modelClass = Team::class;
+ public string $pokemonInstanceClass = PokemonInstance::class;
+ public string $pokemonSpeciesClass = PokemonSpecies::class;
+
+ /**
+ * Returns the total number of teams belonging to a user
+ *
+ * @param int $userId
+ * @return int
+ */
+ public function actionUsersCount(int $userId): int // todo: move this to UsersController
+ {
+ return $this->modelClass::find()->where(['user_id' => $userId])->count();
+ }
+
+ /**
+ * Returns the teams within a category
+ *
+ * @param int $categoryId
+ * @return array
+ */
+ public function actionCategoryView(int $categoryId): array
+ {
+ return $this->modelClass::find()->where(['category_id' => $categoryId])->all();
+ }
+
+ /**
+ * Returns the pokemon instances + species within a team
+ *
+ * @param int $teamId
+ * @return array
+ */
+ public function actionPokemonIndex(int $teamId): array // todo: fix me
+ {
+ $instances = $this->pokemonInstanceClass::find()->where(['team_id' => $teamId])->all();
+ $species = array_map(fn($instance) => $this->pokemonSpeciesClass::find()->where(['id' => $instance->pokemon_species_id])->one(), $instances);
+ return ['instances' => $instances, 'species' => $species];
+ }
}
diff --git a/api/controllers/UsersController.php b/api/controllers/UsersController.php
index 70957c7..cf716fb 100644
--- a/api/controllers/UsersController.php
+++ b/api/controllers/UsersController.php
@@ -2,6 +2,7 @@
namespace app\controllers;
+use app\models\forms\SignupForm;
use app\rest\ActiveController;
use app\models\User;
@@ -18,4 +19,13 @@ class UsersController extends ActiveController
* @var string The model class associated with this controller.
*/
public $modelClass = User::class;
+
+ public function actions(): array
+ {
+ $actions = parent::actions();
+
+ $actions['create']['modelClass'] = SignupForm::class;
+
+ return $actions;
+ }
}
diff --git a/api/db/ActiveRecord.php b/api/db/ActiveRecord.php
new file mode 100644
index 0000000..37a3466
--- /dev/null
+++ b/api/db/ActiveRecord.php
@@ -0,0 +1,15 @@
+user->isGuest;
+ if (!$isGuest)
+ {
+ return Yii::$app->user->identity;
+ }
+ return null;
+ }
+}
diff --git a/api/interfaces/ActiveRecordInterface.php b/api/interfaces/ActiveRecordInterface.php
new file mode 100644
index 0000000..a825893
--- /dev/null
+++ b/api/interfaces/ActiveRecordInterface.php
@@ -0,0 +1,12 @@
+
beginPage() ?>
-
+
= Html::encode($this->title) ?>
diff --git a/api/migrations/m251014_155652_create_table_images_collections.php b/api/migrations/m251014_155652_create_table_images_collections.php
deleted file mode 100644
index 6041d0e..0000000
--- a/api/migrations/m251014_155652_create_table_images_collections.php
+++ /dev/null
@@ -1,34 +0,0 @@
-createTable('{{%images_collections}}', [
- 'id' => $this->primaryKey(),
-
- 'alt' => $this->string(255)->notNull()->defaultValue('Image Not Found'),
- 'image16' => $this->text(),
- 'image32' => $this->text(),
- 'image64' => $this->text(),
- ]);
- }
-
- /**
- * {@inheritdoc}
- */
- public function safeDown(): void
- {
- $this->dropTable('{{%images_collections}}');
- }
-}
diff --git a/api/migrations/m251016_111854_create_table_formats.php b/api/migrations/m251016_111854_create_table_formats.php
index a41ca73..a4c971a 100644
--- a/api/migrations/m251016_111854_create_table_formats.php
+++ b/api/migrations/m251016_111854_create_table_formats.php
@@ -21,6 +21,8 @@ public function safeUp(): void
'abbreviation' => $this->string(),
'max_generation' => $this->integer(),
'min_generation' => $this->integer()->notNull(),
+ 'created_at' => $this->bigInteger()->notNull(),
+ 'updated_at' => $this->bigInteger()->notNull(),
]);
}
diff --git a/api/migrations/m251020_090412_create_table_categories.php b/api/migrations/m251020_090412_create_table_categories.php
index 9beb6bd..711d2b1 100644
--- a/api/migrations/m251020_090412_create_table_categories.php
+++ b/api/migrations/m251020_090412_create_table_categories.php
@@ -16,10 +16,11 @@ public function safeUp(): void
{
$this->createTable('{{%categories}}', [
'id' => $this->primaryKey(),
- 'user_id' => $this->integer()->notNull(),
+ 'user_id' => $this->integer(),
'name' => $this->string(255)->notNull(),
- 'created_at' => $this->dateTime()->notNull(),
+ 'created_at' => $this->bigInteger()->notNull(),
+ 'updated_at' => $this->bigInteger()->notNull(),
]);
/**
diff --git a/api/migrations/m251020_091751_create_table_boxes.php b/api/migrations/m251020_091751_create_table_boxes.php
index f3cd252..7644a72 100644
--- a/api/migrations/m251020_091751_create_table_boxes.php
+++ b/api/migrations/m251020_091751_create_table_boxes.php
@@ -17,8 +17,11 @@ public function safeUp(): void
$this->createTable('{{%boxes}}', [
'id' => $this->primaryKey(),
'category_id' => $this->integer(),
+ 'user_id' => $this->integer(),
'name' => $this->string(255)->notNull(),
+ 'created_at' => $this->bigInteger()->notNull(),
+ 'updated_at' => $this->bigInteger()->notNull(),
]);
/**
@@ -33,6 +36,19 @@ public function safeUp(): void
'SET NULL',
'CASCADE'
);
+
+ /**
+ * user_id FK
+ */
+ $this->addForeignKey(
+ 'fk-boxes-user_id',
+ '{{%boxes}}',
+ 'user_id',
+ '{{%users}}',
+ 'id',
+ 'CASCADE',
+ 'SET NULL'
+ );
}
/**
@@ -41,6 +57,7 @@ public function safeUp(): void
public function safeDown(): void
{
$this->dropForeignKey('fk-boxes-category_id', '{{%boxes}}');
+ $this->dropForeignKey('fk-boxes-user_id', '{{%boxes}}');
$this->dropTable('{{%boxes}}');
}
}
diff --git a/api/migrations/m251020_092814_create_table_teams.php b/api/migrations/m251020_092814_create_table_teams.php
index 5dd45c9..b1e147f 100644
--- a/api/migrations/m251020_092814_create_table_teams.php
+++ b/api/migrations/m251020_092814_create_table_teams.php
@@ -17,8 +17,11 @@ public function safeUp(): void
$this->createTable('{{%teams}}', [
'id' => $this->primaryKey(),
'category_id' => $this->integer(),
+ 'user_id' => $this->integer(),
'name' => $this->string(255)->notNull(),
+ 'created_at' => $this->bigInteger()->notNull(),
+ 'updated_at' => $this->bigInteger()->notNull(),
]);
/**
@@ -26,13 +29,26 @@ public function safeUp(): void
*/
$this->addForeignKey(
'fk-teams-category_id',
- '{{%boxes}}',
+ '{{%teams}}',
'category_id',
'{{%categories}}',
'id',
'SET NULL',
'CASCADE'
);
+
+ /**
+ * user_id FK
+ */
+ $this->addForeignKey(
+ 'fk-teams-user_id',
+ '{{%teams}}',
+ 'user_id',
+ '{{%users}}',
+ 'id',
+ 'CASCADE',
+ 'SET NULL'
+ );
}
/**
@@ -40,7 +56,8 @@ public function safeUp(): void
*/
public function safeDown(): void
{
- $this->dropForeignKey('fk-teams-category_id', '{{%boxes}}');
+ $this->dropForeignKey('fk-teams-category_id', '{{%teams}}');
+ $this->dropForeignKey('fk-teams-user_id', '{{%teams}}');
$this->dropTable('{{%teams}}');
}
}
diff --git a/api/migrations/m251020_093000_create_table_pokemon_species.php b/api/migrations/m251020_093000_create_table_pokemon_species.php
index 63b86e6..677380b 100644
--- a/api/migrations/m251020_093000_create_table_pokemon_species.php
+++ b/api/migrations/m251020_093000_create_table_pokemon_species.php
@@ -20,24 +20,13 @@ public function safeUp(): void
{
$this->createTable('{{%pokemon_species}}', [
'id' => $this->primaryKey(),
- 'images_collection_id' => $this->integer(),
+ 'image' => $this->string(255)->defaultValue(''), // todo: add a default image
'name' => $this->string(255)->notNull(),
'url' => $this->string(255)->notNull()->unique(),
+ 'created_at' => $this->bigInteger()->notNull(),
+ 'updated_at' => $this->bigInteger()->notNull(),
]);
-
- /**
- * images_collection_id FK
- */
- $this->addForeignKey(
- 'fk-pokemon_species-images_collection_id',
- '{{%pokemon_species}}',
- 'images_collection_id',
- '{{%images_collections}}',
- 'id',
- 'CASCADE',
- 'CASCADE'
- );
}
/**
@@ -45,7 +34,6 @@ public function safeUp(): void
*/
public function safeDown(): void
{
- $this->dropForeignKey('fk-pokemon_species-images_collection_id', '{{%pokemon_species}}');
$this->dropTable('{{%pokemon_species}}');
}
}
diff --git a/api/migrations/m251020_093403_create_table_pokemon_instances.php b/api/migrations/m251020_093403_create_table_pokemon_instances.php
index 25c0482..807203a 100644
--- a/api/migrations/m251020_093403_create_table_pokemon_instances.php
+++ b/api/migrations/m251020_093403_create_table_pokemon_instances.php
@@ -24,6 +24,8 @@ public function safeUp(): void
'format_id' => $this->integer(),
'custom_name' => $this->string(255),
+ 'created_at' => $this->bigInteger()->notNull(),
+ 'updated_at' => $this->bigInteger()->notNull(),
]);
/**
diff --git a/api/migrations/m251111_144426_add_auth_columns_to_users_table.php b/api/migrations/m251111_144426_add_auth_columns_to_users_table.php
new file mode 100644
index 0000000..ce579fa
--- /dev/null
+++ b/api/migrations/m251111_144426_add_auth_columns_to_users_table.php
@@ -0,0 +1,41 @@
+addColumn('{{%users}}', 'username', $this->string()->notNull()->unique());
+ $this->addColumn('{{%users}}', 'password', $this->string()->notNull());
+ $this->addColumn('{{%users}}', 'auth_key', $this->string()->notNull()->unique());
+ $this->addColumn('{{%users}}', 'email', $this->string()->unique());
+ $this->addColumn('{{%users}}', 'created_at', $this->bigInteger()->notNull());
+ $this->addColumn('{{%users}}', 'updated_at', $this->bigInteger()->notNull());
+
+ $this->createIndex('idx-users-username', '{{%users}}', 'username', true);
+ $this->createIndex('idx-users-email', '{{%users}}', 'email', true);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function safeDown(): void
+ {
+ $this->dropIndex('idx-users-username', '{{%users}}');
+ $this->dropIndex('idx-users-email', '{{%users}}');
+
+ $this->dropColumn('{{%users}}', 'username');
+ $this->dropColumn('{{%users}}', 'password');
+ $this->dropColumn('{{%users}}', 'auth_key');
+ $this->dropColumn('{{%users}}', 'email');
+ $this->dropColumn('{{%users}}', 'created_at');
+ $this->dropColumn('{{%users}}', 'updated_at');
+ }
+}
diff --git a/api/models/Box.php b/api/models/Box.php
index 6d24ce7..afa1c1e 100644
--- a/api/models/Box.php
+++ b/api/models/Box.php
@@ -2,7 +2,8 @@
namespace app\models;
-use yii\db\ActiveRecord;
+use app\db\ActiveRecord;
+use app\validators\BlankNewItemValidator;
/**
* Class Box
@@ -22,4 +23,24 @@ public static function tableName(): string
{
return '{{%boxes}}';
}
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function tableNameSingular(): string
+ {
+ return 'box';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rules(): array
+ {
+ return [
+ [['category_id', 'user_id', 'name', 'created_at'], 'safe'],
+ [['name'], 'trim'],
+ [['name'], BlankNewItemValidator::class],
+ ];
+ }
}
diff --git a/api/models/Category.php b/api/models/Category.php
index c1081c2..ea2ba8e 100644
--- a/api/models/Category.php
+++ b/api/models/Category.php
@@ -2,7 +2,7 @@
namespace app\models;
-use yii\db\ActiveRecord;
+use app\validators\BlankNewItemValidator;
/**
* Class Category
@@ -14,7 +14,7 @@
* @property string $name
* @property string $created_at
*/
-class Category extends ActiveRecord
+class Category extends \app\db\ActiveRecord
{
/**
* {@inheritdoc}
@@ -23,4 +23,24 @@ public static function tableName(): string
{
return '{{%categories}}';
}
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function tableNameSingular(): string
+ {
+ return 'category';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rules(): array
+ {
+ return [
+ [['user_id', 'name', 'created_at'], 'safe'],
+ [['name'], 'trim'],
+ [['name'], BlankNewItemValidator::class],
+ ];
+ }
}
diff --git a/api/models/Format.php b/api/models/Format.php
index 7483246..d8fdf8d 100644
--- a/api/models/Format.php
+++ b/api/models/Format.php
@@ -2,7 +2,7 @@
namespace app\models;
-use yii\db\ActiveRecord;
+use app\db\ActiveRecord;
/**
* Class Format
@@ -24,4 +24,12 @@ public static function tableName(): string
{
return '{{%formats}}';
}
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function tableNameSingular(): string
+ {
+ return 'format';
+ }
}
diff --git a/api/models/ImagesCollection.php b/api/models/ImagesCollection.php
deleted file mode 100644
index 573e05d..0000000
--- a/api/models/ImagesCollection.php
+++ /dev/null
@@ -1,27 +0,0 @@
- $token]);
}
/**
- * Finds user by username
- *
- * @param string $username
- * @return static|null
+ * {@inheritdoc}
*/
- public static function findByUsername($username)
+ public function behaviors(): array
{
- foreach (self::$users as $user) {
- if (strcasecmp($user['username'], $username) === 0) {
- return new static($user);
- }
- }
+ return [
+ 'timestamp' => ['class' => TimestampBehavior::class],
+ ];
+ }
- return null;
+ /**
+ * {@inheritdoc}
+ */
+ public function fields(): array
+ {
+ return [
+ 'id'
+ ];
}
/**
* {@inheritdoc}
*/
- public function getId()
+ public function rules(): array
{
- return $this->id;
+ return [
+ [['username', 'email'], 'trim'],
+ [['email', 'username'], EmptyStringValidator::class],
+ 'passwordRequired' => [['!password'], 'required'],
+ [['username', '!auth_key'], 'required'],
+ [['username', 'email', '!password', '!auth_key'], 'string', 'max' => 255],
+ [['email'], 'email'],
+ [['username', 'email'], 'unique'],
+ ];
}
/**
* {@inheritdoc}
*/
- public function getAuthKey()
+ public function getId(): int|string
{
- return $this->authKey;
+ return $this->id;
}
/**
* {@inheritdoc}
*/
- public function validateAuthKey($authKey)
+ public function getAuthKey(): ?string
{
- return $this->authKey === $authKey;
+ return $this->auth_key;
}
/**
- * Validates password
- *
- * @param string $password password to validate
- * @return boolean if password provided is valid for current user
+ * {@inheritdoc}
*/
- public function validatePassword($password)
+ public function validateAuthKey($authKey): ?bool
{
- return $this->password === $password;
+ return $this->auth_key === $authKey;
}
}
diff --git a/api/models/forms/LoginForm.php b/api/models/forms/LoginForm.php
new file mode 100644
index 0000000..fefbbf9
--- /dev/null
+++ b/api/models/forms/LoginForm.php
@@ -0,0 +1,94 @@
+hasErrors()) {
+ return;
+ }
+
+ $isExists = User::find()
+ ->where([
+ 'username' => $this->$attribute
+ ])
+ ->exists();
+
+ if (!$isExists) {
+ $this->addError(
+ $attribute,
+ YII_DEBUG ? 'Username doesn\'t match a user in the database.' : 'Username and password don\'t match',
+ );
+ }
+ }
+
+ /**
+ * Validate that the password (once hashed) matches the user record in the
+ * database, identified by the username.
+ *
+ * @param string $attribute Name of the attribute being validated.
+ * @return void
+ */
+ public function validatePassword(string $attribute): void
+ {
+ if ($this->hasErrors()) {
+ return;
+ }
+
+ $user = User::find()
+ ->where([
+ 'username' => $this->username,
+ ])
+ ->one();
+
+ $isPasswordCorrect = Yii::$app->security->validatePassword($this->$attribute, $user->password);
+
+ if (!$isPasswordCorrect) {
+ $this->addError(
+ $attribute,
+ YII_DEBUG ? 'Password doesn\'t match the specified user in the database.' : 'Username and password don\'t match',
+ );
+ }
+ }
+}
diff --git a/api/models/forms/SignupForm.php b/api/models/forms/SignupForm.php
new file mode 100644
index 0000000..d300ffa
--- /dev/null
+++ b/api/models/forms/SignupForm.php
@@ -0,0 +1,72 @@
+auth_key === null) {
+ $this->auth_key = Yii::$app->security->generateRandomString(32);
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ * @throws Exception
+ */
+ public function afterValidate(): void
+ {
+ parent::afterValidate();
+
+ if ($this->password === null && !$this->hasErrors()) {
+ $this->password = Yii::$app->security->generatePasswordHash($this->new_password);
+ $this->validate(['password'], false);
+ }
+ }
+
+ /**
+ * todo
+ */
+
+}
diff --git a/api/rest/ActiveController.php b/api/rest/ActiveController.php
index 9956ec2..0cc10e8 100644
--- a/api/rest/ActiveController.php
+++ b/api/rest/ActiveController.php
@@ -2,7 +2,15 @@
namespace app\rest;
+use app\filters\auth\CookieAuth;
+use yii\filters\auth\CompositeAuth;
+use yii\filters\auth\QueryParamAuth;
+use yii\filters\ContentNegotiator;
use yii\filters\Cors;
+use yii\filters\VerbFilter;
+use yii\helpers\ArrayHelper;
+use yii\rest\Serializer;
+use yii\web\Response;
/**
* Class ActiveController
@@ -13,11 +21,50 @@
*/
class ActiveController extends \yii\rest\ActiveController
{
+ /**
+ * {@inheritdoc}
+ */
+ public $serializer = [
+ 'class' => Serializer::class,
+ 'collectionEnvelope' => 'items',
+ ];
+
/**
* {@inheritdoc}
*/
public function behaviors(): array
{
- return ['cors' => ['class' => Cors::class]];
+ return [
+ 'contentNegotiator' => [
+ 'class' => ContentNegotiator::class,
+ 'formats' => [
+ 'application/json' => Response::FORMAT_JSON,
+ ],
+ ],
+ 'cors' => [
+ 'class' => Cors::class,
+ 'cors' => [
+ 'Origin' => ['http://localhost:5173'],
+ 'Access-Control-Request-Method' => [
+ 'GET', 'HEAD', 'OPTIONS',
+ 'DELETE', 'POST', 'PUT'
+ ],
+ 'Access-Control-Request-Headers' => ['Content-Type'],
+ 'Access-Control-Allow-Credentials' => true,
+ 'Access-Control-Max-Age' => 3600,
+ ]
+ ],
+ 'authenticator' => [
+ 'class' => CompositeAuth::class,
+ 'authMethods' => [
+ CookieAuth::class,
+ QueryParamAuth::class,
+ ],
+ ],
+ 'verbFilter' => [
+ 'class' => VerbFilter::class,
+ 'actions' => $this->verbs(),
+ ],
+ ];
}
}
diff --git a/api/rest/Controller.php b/api/rest/Controller.php
index f4f2aaa..bb57d65 100644
--- a/api/rest/Controller.php
+++ b/api/rest/Controller.php
@@ -2,7 +2,15 @@
namespace app\rest;
+use app\filters\auth\CookieAuth;
+use yii\filters\auth\CompositeAuth;
+use yii\filters\auth\QueryParamAuth;
+use yii\filters\ContentNegotiator;
use yii\filters\Cors;
+use yii\filters\VerbFilter;
+use yii\helpers\ArrayHelper;
+use yii\rest\Serializer;
+use yii\web\Response;
/**
* Class Controller
@@ -13,11 +21,50 @@
*/
class Controller extends \yii\rest\Controller
{
+ /**
+ * @inheritdoc
+ */
+ public $serializer = [
+ 'class' => Serializer::class,
+ 'collectionEnvelope' => 'items',
+ ];
+
/**
* {@inheritdoc}
*/
public function behaviors(): array
{
- return ['cors' => ['class' => Cors::class]];
+ return [
+ 'contentNegotiator' => [
+ 'class' => ContentNegotiator::class,
+ 'formats' => [
+ 'application/json' => Response::FORMAT_JSON,
+ ],
+ ],
+ 'cors' => [
+ 'class' => Cors::class,
+ 'cors' => [
+ 'Origin' => ['http://localhost:5173'],
+ 'Access-Control-Request-Method' => [
+ 'GET', 'HEAD', 'OPTIONS',
+ 'DELETE', 'POST', 'PUT'
+ ],
+ 'Access-Control-Request-Headers' => ['Content-Type'],
+ 'Access-Control-Allow-Credentials' => true,
+ 'Access-Control-Max-Age' => 3600,
+ ]
+ ],
+ 'authenticator' => [
+ 'class' => CompositeAuth::class,
+ 'authMethods' => [
+ QueryParamAuth::class,
+ CookieAuth::class
+ ]
+ ],
+ 'verbFilter' => [
+ 'class' => VerbFilter::class,
+ 'actions' => $this->verbs(),
+ ],
+ ];
}
}
diff --git a/api/validators/BlankNewItemValidator.php b/api/validators/BlankNewItemValidator.php
new file mode 100644
index 0000000..b19fea1
--- /dev/null
+++ b/api/validators/BlankNewItemValidator.php
@@ -0,0 +1,37 @@
+$attribute === '')
+ {
+ $tableName = $model->tableNameSingular();
+ $userId = Yii::$app->user->id;
+ $count = $model::find()->where(['user_id' => $userId])->count();
+ $model->$attribute = "Unnamed $tableName $count";
+ }
+ }
+}
diff --git a/api/validators/EmptyStringValidator.php b/api/validators/EmptyStringValidator.php
new file mode 100644
index 0000000..b951eb1
--- /dev/null
+++ b/api/validators/EmptyStringValidator.php
@@ -0,0 +1,28 @@
+$attribute === '')
+ {
+ $model->$attribute = null;
+ }
+ }
+}
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
index 73a0fc0..b75d833 100644
--- a/frontend/eslint.config.js
+++ b/frontend/eslint.config.js
@@ -1,18 +1,18 @@
-import js from "@eslint/js";
-import globals from "globals";
-import reactHooks from "eslint-plugin-react-hooks";
-import reactRefresh from "eslint-plugin-react-refresh";
-import tseslint from "typescript-eslint";
-import { defineConfig, globalIgnores } from "eslint/config";
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
- globalIgnores(["dist"]),
+ globalIgnores(['dist']),
{
- files: ["**/*.{ts,tsx}"],
+ files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
- reactHooks.configs["recommended-latest"],
+ reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
@@ -20,8 +20,10 @@ export default defineConfig([
globals: globals.browser,
},
rules: {
- "no-unused-vars": "off",
- "@typescript-eslint/no-unused-vars": "off",
+ 'no-unused-vars': 'off',
+ '@typescript-eslint/no-unused-vars': 'off',
+ '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/no-unused-expressions': 'off',
},
},
-]);
+])
diff --git a/frontend/index.html b/frontend/index.html
index 072a57e..bb735bc 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -8,6 +8,7 @@
-
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 2ec71a1..7282391 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -9,7 +9,9 @@
"version": "0.0.0",
"dependencies": {
"react": "^19.1.1",
- "react-dom": "^19.1.1"
+ "react-dom": "^19.1.1",
+ "react-router-dom": "^7.9.5",
+ "react-select": "^5.10.2"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
@@ -27,6 +29,145 @@
"vite": "npm:rolldown-vite@7.1.14"
}
},
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.5",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz",
@@ -61,6 +202,120 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@emotion/babel-plugin": {
+ "version": "11.13.5",
+ "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
+ "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.16.7",
+ "@babel/runtime": "^7.18.3",
+ "@emotion/hash": "^0.9.2",
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/serialize": "^1.3.3",
+ "babel-plugin-macros": "^3.1.0",
+ "convert-source-map": "^1.5.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-root": "^1.1.0",
+ "source-map": "^0.5.7",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/cache": {
+ "version": "11.14.0",
+ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
+ "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/sheet": "^1.4.0",
+ "@emotion/utils": "^1.4.2",
+ "@emotion/weak-memoize": "^0.4.0",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+ "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
+ "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/react": {
+ "version": "11.14.0",
+ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
+ "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.13.5",
+ "@emotion/cache": "^11.14.0",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
+ "@emotion/utils": "^1.4.2",
+ "@emotion/weak-memoize": "^0.4.0",
+ "hoist-non-react-statics": "^3.3.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/serialize": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
+ "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/hash": "^0.9.2",
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/unitless": "^0.10.0",
+ "@emotion/utils": "^1.4.2",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@emotion/sheet": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
+ "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
+ "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
+ "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@emotion/utils": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
+ "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/weak-memoize": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
+ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
+ "license": "MIT"
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
@@ -218,6 +473,31 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+ "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+ "license": "MIT"
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -270,6 +550,41 @@
"url": "https://github.com/sponsors/nzakas"
}
},
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz",
@@ -847,11 +1162,16 @@
"undici-types": "~7.16.0"
}
},
+ "node_modules/@types/parse-json": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
+ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
+ "license": "MIT"
+ },
"node_modules/@types/react": {
"version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
@@ -867,6 +1187,15 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/react-transition-group": {
+ "version": "4.4.12",
+ "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
+ "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
@@ -1215,6 +1544,21 @@
"dev": true,
"license": "Python-2.0"
},
+ "node_modules/babel-plugin-macros": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
+ "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "cosmiconfig": "^7.0.0",
+ "resolve": "^1.19.0"
+ },
+ "engines": {
+ "node": ">=10",
+ "npm": ">=6"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1250,7 +1594,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -1300,6 +1643,46 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
+ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/cosmiconfig": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+ "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/cosmiconfig/node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1319,14 +1702,12 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "dev": true,
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -1357,11 +1738,29 @@
"node": ">=8"
}
},
+ "node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -1634,6 +2033,12 @@
"node": ">=8"
}
},
+ "node_modules/find-root": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
+ "license": "MIT"
+ },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -1687,6 +2092,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -1730,6 +2144,27 @@
"node": ">=8"
}
},
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -1744,7 +2179,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
@@ -1767,6 +2201,27 @@
"node": ">=0.8.19"
}
},
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "license": "MIT"
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -1807,6 +2262,12 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -1820,6 +2281,18 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -1827,6 +2300,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "license": "MIT"
+ },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -2126,6 +2605,12 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "license": "MIT"
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -2149,6 +2634,24 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/memoize-one": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+ "license": "MIT"
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -2190,7 +2693,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@@ -2219,6 +2721,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -2273,7 +2784,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
@@ -2282,6 +2792,24 @@
"node": ">=6"
}
},
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -2302,11 +2830,25 @@
"node": ">=8"
}
},
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -2377,6 +2919,17 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -2429,11 +2982,111 @@
"react": "^19.2.0"
}
},
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/react-router": {
+ "version": "7.9.5",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz",
+ "integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.9.5",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.5.tgz",
+ "integrity": "sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.9.5"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/react-select": {
+ "version": "5.10.2",
+ "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz",
+ "integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.0",
+ "@emotion/cache": "^11.4.0",
+ "@emotion/react": "^11.8.1",
+ "@floating-ui/dom": "^1.0.1",
+ "@types/react-transition-group": "^4.4.0",
+ "memoize-one": "^6.0.0",
+ "prop-types": "^15.6.0",
+ "react-transition-group": "^4.3.0",
+ "use-isomorphic-layout-effect": "^1.2.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/react-transition-group": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -2534,6 +3187,12 @@
"node": ">=10"
}
},
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -2557,6 +3216,15 @@
"node": ">=8"
}
},
+ "node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2580,6 +3248,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/stylis": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
+ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
+ "license": "MIT"
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -2593,6 +3267,18 @@
"node": ">=8"
}
},
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -2743,6 +3429,20 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-isomorphic-layout-effect": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz",
+ "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/vite": {
"name": "rolldown-vite",
"version": "7.1.14",
diff --git a/frontend/package.json b/frontend/package.json
index 280f597..aee147b 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -11,7 +11,9 @@
},
"dependencies": {
"react": "^19.1.1",
- "react-dom": "^19.1.1"
+ "react-dom": "^19.1.1",
+ "react-router-dom": "^7.9.5",
+ "react-select": "^5.10.2"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 7e48e0f..2030491 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,22 +1,38 @@
-import { useState } from "react";
-
-export default function BackendTest() {
- const [response, setResponse] = useState("");
-
- async function testBackend() {
- try {
- const res = await fetch("http://127.0.0.1:8000/health/check");
- const json = await res.json();
- setResponse(JSON.stringify(json));
- } catch (err) {
- console.error(err);
- }
- }
+import { Routes, Route, Link } from 'react-router-dom'
+import Home from './pages/Home'
+import Test from './pages/Test.tsx'
+import Categories from './pages/categories/Categories.tsx'
+import Category from './pages/categories/Category.tsx'
+import Teams from './pages/teams/Teams.tsx'
+import Team from './pages/teams/Team.tsx'
+function App() {
return (
-
-
{response}
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
- );
+ )
}
+
+export default App
+/* todo:
+ - add caching layer to categories, teams, etc. index calls
+ - make full login page wireframe
+ - fetch moves for pokemon on team page
+ - fix rest of pages to use the new default naming middleware that categories now uses to reduce api calls
+ - add menu pages for teams, boxes, etc.
+ - start abstracting out repeated api calls and functions to reduce code duplication
+ - add a styling palette and start to beautify
+*/
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index bef5202..ec904f5 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -1,10 +1,12 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './index.css'
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import { BrowserRouter } from 'react-router-dom'
import App from './App.tsx'
-createRoot(document.getElementById('root')!).render(
-
-
- ,
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+
+
)
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
new file mode 100644
index 0000000..fb8cf9a
--- /dev/null
+++ b/frontend/src/pages/Home.tsx
@@ -0,0 +1,22 @@
+export default function Home() {
+ const BASE_URL = 'http://127.0.0.1:8000'
+ async function loginDev() {
+ await fetch(`${BASE_URL}/auth/login`, {
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/json',
+ },
+ body: JSON.stringify({
+ username: 'test',
+ password: 'test',
+ }),
+ })
+ }
+
+ return (
+
+
Home Page
+
+
+ )
+}
diff --git a/frontend/src/pages/Test.tsx b/frontend/src/pages/Test.tsx
new file mode 100644
index 0000000..e28fe75
--- /dev/null
+++ b/frontend/src/pages/Test.tsx
@@ -0,0 +1,26 @@
+import { useState } from 'react'
+
+const BASE_URL = 'http://127.0.0.1:8000'
+
+export default function Test() {
+ const [health, setHealth] = useState('')
+
+ async function healthCheck() {
+ try {
+ const json = await (
+ await fetch(`${BASE_URL}/health/check`, { credentials: 'include' })
+ ).json()
+
+ setHealth(JSON.stringify(json))
+ } catch (err) {
+ console.error(err)
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/frontend/src/pages/categories/Categories.tsx b/frontend/src/pages/categories/Categories.tsx
new file mode 100644
index 0000000..bdd02a6
--- /dev/null
+++ b/frontend/src/pages/categories/Categories.tsx
@@ -0,0 +1,82 @@
+import { useEffect, useState } from 'react'
+import { Link } from 'react-router-dom'
+import CategoryMenu from './CategoryMenu.tsx'
+import { createPortal } from 'react-dom'
+
+export default function Categories() {
+ const [categories, setCategories] = useState<{ button: any }[]>([])
+ const [isCategoryMenuVisible, setIsCategoryMenuVisible] = useState(false)
+ const [categoryMenuPosition, setCategoryMenuPosition] = useState<{ x: number; y: number }>({
+ x: 0,
+ y: 0,
+ })
+ const BASE_URL = 'http://127.0.0.1:8000'
+ const portalRoot = document.getElementById('portal-root')
+
+ async function createNewCategory() {
+ const userId: number = 1 // (placeholder) todo: get userId of currently logged in user / localstorage
+
+ await fetch(`${BASE_URL}/categories/create`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ user_id: userId,
+ name: '', // let server allocate a default name
+ created_at: '2007-09-24 00:00:00', // todo: get current date in this format
+ }),
+ })
+ .then(res => res.json())
+ .then(data => {
+ console.log(data)
+ })
+ .catch(err => console.log(err))
+
+ await loadCategories()
+ }
+
+ async function loadCategories() {
+ const json = await (await fetch(`${BASE_URL}/categories/index`)).json()
+ const category = json.items.map((item: any) => {
+ return {
+ button: (
+
+ ),
+ }
+ })
+ setCategories(category)
+ }
+
+ const showCategoryMenu = (c: any) => {
+ c.preventDefault()
+ setIsCategoryMenuVisible(true)
+ setCategoryMenuPosition({ x: c.pageX, y: c.pageY })
+ }
+
+ const hideCategoryMenu = (c: any) => {
+ c.preventDefault()
+ setIsCategoryMenuVisible(false)
+ }
+
+ useEffect(() => {
+ loadCategories().then()
+ }, [])
+
+ return (
+
+
+ {isCategoryMenuVisible &&
+ createPortal(
+
,
+ portalRoot ?? document.body
+ )}
+
+ {categories.map(category => category.button)}
+
+
+
+ )
+}
diff --git a/frontend/src/pages/categories/Category.tsx b/frontend/src/pages/categories/Category.tsx
new file mode 100644
index 0000000..52f8c7e
--- /dev/null
+++ b/frontend/src/pages/categories/Category.tsx
@@ -0,0 +1,15 @@
+import { useParams } from 'react-router-dom'
+import Teams from '../teams/Teams.tsx'
+
+export default function Category() {
+ const { categoryId } = useParams()
+
+ return (
+
+
category {categoryId}
+
+ {/* just using the test page for now, todo: replace with proper page */}
+
+
+ )
+}
diff --git a/frontend/src/pages/categories/CategoryMenu.tsx b/frontend/src/pages/categories/CategoryMenu.tsx
new file mode 100644
index 0000000..ea3c185
--- /dev/null
+++ b/frontend/src/pages/categories/CategoryMenu.tsx
@@ -0,0 +1,29 @@
+export default function CategoryMenu({ x, y }: { x: number; y: number }) {
+ return (
+
+ )
+}
diff --git a/frontend/src/pages/teams/Team.tsx b/frontend/src/pages/teams/Team.tsx
new file mode 100644
index 0000000..f835af6
--- /dev/null
+++ b/frontend/src/pages/teams/Team.tsx
@@ -0,0 +1,135 @@
+import { useParams } from 'react-router-dom'
+import { useEffect, useState } from 'react'
+import Select from 'react-select'
+
+const BASE_URL = 'http://127.0.0.1:8000'
+
+type PokemonInstance = {
+ id: number
+ box_id?: number
+ team_id?: number
+ pokemon_species_id: number
+ custom_name: string
+ format_id?: number
+}
+
+type PokemonSpecies = {
+ id: number
+ image: number
+ name: string
+ url: string
+}
+export default function Team() {
+ const [pokemon, setPokemon] = useState<{ button: any }[]>([])
+ const { teamId } = useParams()
+
+ const [dropdownItems, setDropdownItems] = useState<{ value: number | null; label: string }[]>([
+ {
+ value: null,
+ label: 'Select A Pokemon',
+ },
+ ])
+ const [selectedItem, setSelectedItem] = useState<{ value: number | null; label: string }>({
+ value: null,
+ label: 'Select A Pokemon',
+ })
+
+ async function loadDropdownItems() {
+ try {
+ const json = await (await fetch(`${BASE_URL}/pokemon-species/index/?per-page=1000`)).json()
+ const items = json.items.map((item: PokemonSpecies) => {
+ return {
+ value: item.id,
+ label: item.name,
+ }
+ })
+ setDropdownItems(items)
+ } catch (err) {
+ console.error(err)
+ }
+ }
+
+ async function loadPokemon() {
+ const json = await (await fetch(`${BASE_URL}/teams/pokemon-index?teamId=${teamId}`)).json()
+
+ const species: { image: string; name: string }[] = json.species.map((item: PokemonSpecies) => {
+ return {
+ image: item.image,
+ name: item.name,
+ }
+ })
+
+ const instance: { id: number }[] = json.instances.map((item: PokemonInstance) => {
+ return {
+ id: item.id,
+ }
+ })
+
+ const pokemon = instance.map((item, index) => {
+ const id = item.id
+ const imageUrl = species[index].image
+ const name = species[index].name
+
+ const imageTag =
+ return {
+ button: ,
+ }
+ })
+
+ setPokemon(pokemon)
+ }
+
+ async function addPokemon() {
+ if (!selectedItem.value) return
+ await fetch(`${BASE_URL}/pokemon-instances/create`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ // todo: add functionality for all of these columns that are currently using test values:
+ body: JSON.stringify({
+ team_id: teamId,
+ box_id: null, // todo: check if this is a box or team page
+ pokemon_species_id: selectedItem.value,
+ format_id: null,
+ custom_name: 'unnamed',
+ // created_at: '2007-09-24 00:00:00',
+ }),
+ })
+ loadPokemon().then()
+ }
+
+ useEffect(() => {
+ loadDropdownItems().then()
+ }, [])
+
+ useEffect(() => {
+ loadPokemon().then()
+ }, [])
+
+ return (
+
+
+
+
+
{pokemon.map(p => p.button)}
+
+ )
+}
diff --git a/frontend/src/pages/teams/Teams.tsx b/frontend/src/pages/teams/Teams.tsx
new file mode 100644
index 0000000..553f7cb
--- /dev/null
+++ b/frontend/src/pages/teams/Teams.tsx
@@ -0,0 +1,67 @@
+import { Link, useParams } from 'react-router-dom'
+import { useEffect, useState } from 'react'
+
+export default function Teams() {
+ const BASE_URL = 'http://127.0.0.1:8000'
+ const { categoryId } = useParams()
+ const [teams, setTeams] = useState<{ button: any }[]>([])
+
+ async function loadTeams() {
+ const json = await (
+ await fetch(`${BASE_URL}/categories/teams-index?categoryId=${categoryId}`)
+ ).json()
+ const teams = json.map((item: any) => {
+ return {
+ id: item.id,
+ name: item.name,
+ button: (
+
+ ),
+ }
+ })
+ setTeams(teams)
+ }
+
+ async function createNewTeam() {
+ const userId: number = 1 // (placeholder) todo: get userId of currently logged in user / localstorage
+
+ await fetch(`${BASE_URL}/teams/create`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ category_id: categoryId,
+ user_id: userId,
+ name: '',
+ // created_at: '2007-09-24 00:00:00',
+ }),
+ })
+ .then(res => res.json())
+ .then(data => {
+ console.log(data)
+ })
+ .catch(err => console.log(err))
+
+ await loadTeams()
+ }
+
+ useEffect(() => {
+ loadTeams().then()
+ }, [])
+
+ return (
+
+
teams in category {categoryId}
+
+
+ {teams.map(category => category.button)}
+
+
+
+ )
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index ac9e0cf..13217ea 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -3,14 +3,14 @@ import react from '@vitejs/plugin-react-swc'
// https://vite.dev/config/
export default defineConfig({
- plugins: [react()],
- server: {
- proxy: {
- '/api': {
- target: 'http://localhost:8000', // 👈 your Yii server URL
- changeOrigin: true,
- secure: false,
- },
- },
+ plugins: [react()],
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8000',
+ changeOrigin: true,
+ secure: false,
+ },
},
-});
+ },
+})