diff --git a/app/Http/Controllers/Api/EmojiAliasController.php b/app/Http/Controllers/Api/EmojiAliasController.php new file mode 100644 index 000000000..dafd90e4f --- /dev/null +++ b/app/Http/Controllers/Api/EmojiAliasController.php @@ -0,0 +1,114 @@ +when($request->get('emoji_id'), function ($query, $emojiId) { + $query->where('emoji_id', $emojiId); + }) + ->when($request->get('search'), function ($query, $search) { + $query->where('alias', 'like', "%{$search}%"); + }) + ->when($request->get('with_emoji'), function ($query) { + $query->with(['emoji' => function ($q) { + $q->with('category'); + }]); + }) + ->orderBy('alias') + ->paginate($request->get('per_page', 50)); + + return EmojiAliasResource::collection($aliases); + } + + /** + * Store a newly created resource in storage. + */ + public function store(StoreEmojiAliasRequest $request) + { + $alias = EmojiAlias::create($request->validated()); + + if ($request->get('with_emoji')) { + $alias->load(['emoji' => function ($query) { + $query->with('category'); + }]); + } + + return new EmojiAliasResource($alias); + } + + /** + * Display the specified resource. + */ + public function show(EmojiAlias $emojiAlias, Request $request) + { + $emojiAlias->load(['emoji' => function ($query) { + $query->with('category'); + }]); + + return new EmojiAliasResource($emojiAlias); + } + + /** + * Update the specified resource in storage. + */ + public function update(UpdateEmojiAliasRequest $request, EmojiAlias $emojiAlias) + { + $emojiAlias->update($request->validated()); + + if ($request->get('with_emoji')) { + $emojiAlias->load(['emoji' => function ($query) { + $query->with('category'); + }]); + } + + return new EmojiAliasResource($emojiAlias); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(EmojiAlias $emojiAlias) + { + $emojiAlias->delete(); + + return response()->json(null, 204); + } + + /** + * Search aliases using Laravel Scout. + */ + public function search(Request $request) + { + $request->validate([ + 'q' => 'required|string|min:1', + 'limit' => 'integer|min:1|max:100', + ]); + + $aliases = EmojiAlias::search($request->get('q')) + ->take($request->get('limit', 20)) + ->get(); + + // Load emoji relationships if requested + if ($request->get('with_emoji')) { + $aliases->load(['emoji' => function ($query) { + $query->with('category'); + }]); + } + + return EmojiAliasResource::collection($aliases); + } +} diff --git a/app/Http/Controllers/Api/EmojiCategoryController.php b/app/Http/Controllers/Api/EmojiCategoryController.php new file mode 100644 index 000000000..836a06752 --- /dev/null +++ b/app/Http/Controllers/Api/EmojiCategoryController.php @@ -0,0 +1,80 @@ +when($request->get('with_emojis'), function ($query) { + $query->withCount('emojis'); + }) + ->when($request->get('include_emojis'), function ($query) { + $query->with(['emojis' => function ($q) { + $q->orderBy('display_order'); + }]); + }) + ->orderBy('display_order') + ->get(); + + return EmojiCategoryResource::collection($categories); + } + + /** + * Store a newly created resource in storage. + */ + public function store(StoreEmojiCategoryRequest $request) + { + $category = EmojiCategory::create($request->validated()); + + return new EmojiCategoryResource($category); + } + + /** + * Display the specified resource. + */ + public function show(EmojiCategory $emojiCategory, Request $request) + { + $emojiCategory->load([ + 'emojis' => function ($query) use ($request) { + $query->orderBy('display_order'); + if ($request->get('with_aliases')) { + $query->with('aliases'); + } + }, + ]); + + return new EmojiCategoryResource($emojiCategory); + } + + /** + * Update the specified resource in storage. + */ + public function update(UpdateEmojiCategoryRequest $request, EmojiCategory $emojiCategory) + { + $emojiCategory->update($request->validated()); + + return new EmojiCategoryResource($emojiCategory); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(EmojiCategory $emojiCategory) + { + $emojiCategory->delete(); + + return response()->json(null, 204); + } +} diff --git a/app/Http/Controllers/Api/EmojiController.php b/app/Http/Controllers/Api/EmojiController.php new file mode 100644 index 000000000..a5e24c7be --- /dev/null +++ b/app/Http/Controllers/Api/EmojiController.php @@ -0,0 +1,119 @@ +when($request->get('category_id'), function ($query, $categoryId) { + $query->where('emoji_category_id', $categoryId); + }) + ->when($request->get('search'), function ($query, $search) { + $query->where(function ($q) use ($search) { + $q->where('emoji_shortcode', 'like', "%{$search}%") + ->orWhere('title', 'like', "%{$search}%") + ->orWhere('emoji_text', 'like', "%{$search}%"); + }); + }) + ->when($request->get('with_category'), function ($query) { + $query->with('category'); + }) + ->when($request->get('with_aliases'), function ($query) { + $query->with('aliases'); + }) + ->orderBy('display_order') + ->paginate($request->get('per_page', 50)); + + return EmojiResource::collection($emojis); + } + + /** + * Store a newly created resource in storage. + */ + public function store(StoreEmojiRequest $request) + { + $emoji = Emoji::create($request->validated()); + + if ($request->get('with_category')) { + $emoji->load('category'); + } + + return new EmojiResource($emoji); + } + + /** + * Display the specified resource. + */ + public function show(Emoji $emoji, Request $request) + { + $emoji->load([ + 'category', + 'aliases' => function ($query) { + $query->orderBy('alias'); + }, + ]); + + return new EmojiResource($emoji); + } + + /** + * Update the specified resource in storage. + */ + public function update(UpdateEmojiRequest $request, Emoji $emoji) + { + $emoji->update($request->validated()); + + if ($request->get('with_category')) { + $emoji->load('category'); + } + + return new EmojiResource($emoji); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Emoji $emoji) + { + $emoji->delete(); + + return response()->json(null, 204); + } + + /** + * Search emojis using Laravel Scout. + */ + public function search(Request $request) + { + $request->validate([ + 'q' => 'required|string|min:1', + 'limit' => 'integer|min:1|max:100', + ]); + + $emojis = Emoji::search($request->get('q')) + ->take($request->get('limit', 20)) + ->get(); + + // Load relationships if requested + if ($request->get('with_category')) { + $emojis->load('category'); + } + if ($request->get('with_aliases')) { + $emojis->load('aliases'); + } + + return EmojiResource::collection($emojis); + } +} diff --git a/app/Http/Controllers/Emoji/EmojiAliasController.php b/app/Http/Controllers/Emoji/EmojiAliasController.php new file mode 100644 index 000000000..32e661349 --- /dev/null +++ b/app/Http/Controllers/Emoji/EmojiAliasController.php @@ -0,0 +1,67 @@ +|string> + */ + public function rules(): array + { + return [ + 'emoji_id' => 'required|exists:emojis,id', + 'alias' => [ + 'required', + 'string', + 'max:255', + 'regex:/^:[a-zA-Z0-9_-]+:$/', + 'unique:emoji_aliases,alias', + Rule::notIn(\App\Models\Emoji::pluck('emoji_shortcode')->toArray()), + ], + ]; + } + + /** + * Get custom validation messages. + */ + public function messages(): array + { + return [ + 'alias.regex' => 'The alias must be in the format :name: (e.g., :happy:)', + 'alias.unique' => 'This alias is already taken.', + 'alias.not_in' => 'This alias conflicts with an existing emoji shortcode.', + ]; + } +} diff --git a/app/Http/Requests/Emoji/StoreEmojiCategoryRequest.php b/app/Http/Requests/Emoji/StoreEmojiCategoryRequest.php new file mode 100644 index 000000000..b25aaec90 --- /dev/null +++ b/app/Http/Requests/Emoji/StoreEmojiCategoryRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + 'title' => 'required|string|max:255', + 'display_order' => 'required|integer|min:0', + ]; + } +} diff --git a/app/Http/Requests/Emoji/StoreEmojiRequest.php b/app/Http/Requests/Emoji/StoreEmojiRequest.php new file mode 100644 index 000000000..3dcae9e2f --- /dev/null +++ b/app/Http/Requests/Emoji/StoreEmojiRequest.php @@ -0,0 +1,60 @@ +|string> + */ + public function rules(): array + { + return [ + 'title' => 'required|string|max:255', + 'emoji_text' => 'nullable|string|max:10', + 'emoji_shortcode' => [ + 'required', + 'string', + 'max:255', + 'regex:/^:[a-zA-Z0-9_-]+:$/', + 'unique:emojis,emoji_shortcode', + Rule::notIn(\App\Models\EmojiAlias::pluck('alias')->toArray()), + ], + 'image_url' => 'nullable|string|max:500', + 'sprite_mode' => 'boolean', + 'sprite_params' => 'nullable|array', + 'sprite_params.x' => 'required_if:sprite_mode,true|integer|min:0', + 'sprite_params.y' => 'required_if:sprite_mode,true|integer|min:0', + 'sprite_params.width' => 'required_if:sprite_mode,true|integer|min:1', + 'sprite_params.height' => 'required_if:sprite_mode,true|integer|min:1', + 'sprite_params.sheet' => 'required_if:sprite_mode,true|string|max:255', + 'emoji_category_id' => 'nullable|exists:emoji_categories,id', + 'display_order' => 'required|integer|min:0', + ]; + } + + /** + * Get custom validation messages. + */ + public function messages(): array + { + return [ + 'emoji_shortcode.regex' => 'The emoji shortcode must be in the format :name: (e.g., :smile:)', + 'emoji_shortcode.unique' => 'This shortcode is already taken.', + 'emoji_shortcode.not_in' => 'This shortcode conflicts with an existing alias.', + ]; + } +} diff --git a/app/Http/Requests/Emoji/UpdateEmojiAliasRequest.php b/app/Http/Requests/Emoji/UpdateEmojiAliasRequest.php new file mode 100644 index 000000000..9eab55a31 --- /dev/null +++ b/app/Http/Requests/Emoji/UpdateEmojiAliasRequest.php @@ -0,0 +1,51 @@ +|string> + */ + public function rules(): array + { + $aliasId = $this->route('emoji_alias')->id; + + return [ + 'emoji_id' => 'sometimes|exists:emojis,id', + 'alias' => [ + 'sometimes', + 'string', + 'max:255', + 'regex:/^:[a-zA-Z0-9_-]+:$/', + Rule::unique('emoji_aliases', 'alias')->ignore($aliasId), + Rule::notIn(\App\Models\Emoji::pluck('emoji_shortcode')->toArray()), + ], + ]; + } + + /** + * Get custom validation messages. + */ + public function messages(): array + { + return [ + 'alias.regex' => 'The alias must be in the format :name: (e.g., :happy:)', + 'alias.unique' => 'This alias is already taken.', + 'alias.not_in' => 'This alias conflicts with an existing emoji shortcode.', + ]; + } +} diff --git a/app/Http/Requests/Emoji/UpdateEmojiCategoryRequest.php b/app/Http/Requests/Emoji/UpdateEmojiCategoryRequest.php new file mode 100644 index 000000000..e671d7bcc --- /dev/null +++ b/app/Http/Requests/Emoji/UpdateEmojiCategoryRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + 'title' => 'sometimes|string|max:255', + 'display_order' => 'sometimes|integer|min:0', + ]; + } +} diff --git a/app/Http/Requests/Emoji/UpdateEmojiRequest.php b/app/Http/Requests/Emoji/UpdateEmojiRequest.php new file mode 100644 index 000000000..d4b0c3958 --- /dev/null +++ b/app/Http/Requests/Emoji/UpdateEmojiRequest.php @@ -0,0 +1,62 @@ +|string> + */ + public function rules(): array + { + $emojiId = $this->route('emoji')->id; + + return [ + 'title' => 'sometimes|string|max:255', + 'emoji_text' => 'nullable|string|max:10', + 'emoji_shortcode' => [ + 'sometimes', + 'string', + 'max:255', + 'regex:/^:[a-zA-Z0-9_-]+:$/', + Rule::unique('emojis', 'emoji_shortcode')->ignore($emojiId), + Rule::notIn(\App\Models\EmojiAlias::pluck('alias')->toArray()), + ], + 'image_url' => 'nullable|string|max:500', + 'sprite_mode' => 'sometimes|boolean', + 'sprite_params' => 'nullable|array', + 'sprite_params.x' => 'required_if:sprite_mode,true|integer|min:0', + 'sprite_params.y' => 'required_if:sprite_mode,true|integer|min:0', + 'sprite_params.width' => 'required_if:sprite_mode,true|integer|min:1', + 'sprite_params.height' => 'required_if:sprite_mode,true|integer|min:1', + 'sprite_params.sheet' => 'required_if:sprite_mode,true|string|max:255', + 'emoji_category_id' => 'nullable|exists:emoji_categories,id', + 'display_order' => 'sometimes|integer|min:0', + ]; + } + + /** + * Get custom validation messages. + */ + public function messages(): array + { + return [ + 'emoji_shortcode.regex' => 'The emoji shortcode must be in the format :name: (e.g., :smile:)', + 'emoji_shortcode.unique' => 'This shortcode is already taken.', + 'emoji_shortcode.not_in' => 'This shortcode conflicts with an existing alias.', + ]; + } +} diff --git a/app/Http/Resources/EmojiAliasResource.php b/app/Http/Resources/EmojiAliasResource.php new file mode 100644 index 000000000..ca39cbce9 --- /dev/null +++ b/app/Http/Resources/EmojiAliasResource.php @@ -0,0 +1,25 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'alias' => $this->alias, + 'emoji' => new EmojiResource($this->whenLoaded('emoji')), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/app/Http/Resources/EmojiCategoryResource.php b/app/Http/Resources/EmojiCategoryResource.php new file mode 100644 index 000000000..bfaa7df29 --- /dev/null +++ b/app/Http/Resources/EmojiCategoryResource.php @@ -0,0 +1,27 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'display_order' => $this->display_order, + 'emojis_count' => $this->whenCounted('emojis'), + 'emojis' => EmojiResource::collection($this->whenLoaded('emojis')), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/app/Http/Resources/EmojiResource.php b/app/Http/Resources/EmojiResource.php new file mode 100644 index 000000000..a6163632c --- /dev/null +++ b/app/Http/Resources/EmojiResource.php @@ -0,0 +1,33 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'emoji_text' => $this->emoji_text, + 'emoji_shortcode' => $this->emoji_shortcode, + 'image_url' => $this->image_url, + 'sprite_mode' => $this->sprite_mode, + 'sprite_params' => $this->sprite_params, + 'display_order' => $this->display_order, + 'category' => new EmojiCategoryResource($this->whenLoaded('category')), + 'aliases' => EmojiAliasResource::collection($this->whenLoaded('aliases')), + 'aliases_count' => $this->whenCounted('aliases'), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/app/Models/Emoji.php b/app/Models/Emoji.php new file mode 100644 index 000000000..672b88dc4 --- /dev/null +++ b/app/Models/Emoji.php @@ -0,0 +1,83 @@ + */ + use HasFactory, Searchable; + + /** + * The table associated with the model. + * + * Note: Explicitly set because Laravel's pluralization doesn't handle "emoji" correctly + * (it's a Japanese loanword where singular and plural forms are the same). + * + * @var string + */ + protected $table = 'emojis'; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'title', + 'emoji_text', + 'emoji_shortcode', + 'image_url', + 'sprite_mode', + 'sprite_params', + 'emoji_category_id', + 'display_order', + ]; + + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'sprite_mode' => 'boolean', + 'sprite_params' => 'array', + ]; + + /** + * Get the category that owns the emoji. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function category() + { + return $this->belongsTo(EmojiCategory::class, 'emoji_category_id'); + } + + /** + * Get the aliases for the emoji. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function aliases() + { + return $this->hasMany(EmojiAlias::class); + } + + /** + * Get the indexable data array for the model. + * + * @return array + */ + public function toSearchableArray(): array + { + return [ + 'id' => $this->id, + 'emoji_shortcode' => $this->emoji_shortcode, + 'emoji_text' => $this->emoji_text, + ]; + } +} diff --git a/app/Models/EmojiAlias.php b/app/Models/EmojiAlias.php new file mode 100644 index 000000000..df5a35712 --- /dev/null +++ b/app/Models/EmojiAlias.php @@ -0,0 +1,47 @@ + */ + use HasFactory, Searchable; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'emoji_id', + 'alias', + ]; + + /** + * Get the emoji that owns the alias. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function emoji() + { + return $this->belongsTo(Emoji::class); + } + + /** + * Get the indexable data array for the model. + * + * @return array + */ + public function toSearchableArray(): array + { + return [ + 'id' => $this->id, + 'alias' => $this->alias, + 'emoji_id' => $this->emoji_id, + ]; + } +} diff --git a/app/Models/EmojiCategory.php b/app/Models/EmojiCategory.php new file mode 100644 index 000000000..1826a24e4 --- /dev/null +++ b/app/Models/EmojiCategory.php @@ -0,0 +1,32 @@ + */ + use HasFactory; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'title', + 'display_order', + ]; + + /** + * Get the emojis for the category. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function emojis() + { + return $this->hasMany(Emoji::class); + } +} diff --git a/app/Policies/EmojiAliasPolicy.php b/app/Policies/EmojiAliasPolicy.php new file mode 100644 index 000000000..ede89bb53 --- /dev/null +++ b/app/Policies/EmojiAliasPolicy.php @@ -0,0 +1,65 @@ + + */ +class EmojiAliasFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'emoji_id' => Emoji::factory(), + 'alias' => ':' . $this->faker->unique()->word() . ':', + ]; + } +} diff --git a/database/factories/EmojiCategoryFactory.php b/database/factories/EmojiCategoryFactory.php new file mode 100644 index 000000000..c4593f55c --- /dev/null +++ b/database/factories/EmojiCategoryFactory.php @@ -0,0 +1,24 @@ + + */ +class EmojiCategoryFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'title' => $this->faker->randomElement(['Smileys & Emotion', 'People & Body', 'Animals & Nature', 'Food & Drink', 'Travel & Places', 'Activities', 'Objects', 'Symbols', 'Flags']), + 'display_order' => $this->faker->numberBetween(1, 100), + ]; + } +} diff --git a/database/factories/EmojiFactory.php b/database/factories/EmojiFactory.php new file mode 100644 index 000000000..12bf6e3e7 --- /dev/null +++ b/database/factories/EmojiFactory.php @@ -0,0 +1,64 @@ + + */ +class EmojiFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $emojis = ['😊', '😃', '😄', '😁', '😆', '😅', 'ðŸĪĢ', '😂', '🙂', '🙃', '😉', '😊', '😇', 'ðŸĨ°', '😍', 'ðŸĪĐ', '😘', '😗', '😚', '😙', 'ðŸĨē', '😋', '😛', '😜', 'ðŸĪŠ', '😝', 'ðŸĪ‘', 'ðŸĪ—', 'ðŸĪ­', 'ðŸĪŦ', 'ðŸĪ”']; + $hasEmoji = $this->faker->boolean(80); // 80% chance of having unicode emoji + + return [ + 'title' => $this->faker->words(2, true), + 'emoji_text' => $hasEmoji ? $this->faker->randomElement($emojis) : null, + 'emoji_shortcode' => ':' . $this->faker->unique()->word() . ':', + 'image_url' => !$hasEmoji ? '/emojis/custom/' . $this->faker->unique()->word() . '.png' : null, + 'sprite_mode' => false, + 'sprite_params' => null, + 'emoji_category_id' => EmojiCategory::factory(), + 'display_order' => $this->faker->numberBetween(1, 100), + ]; + } + + /** + * Indicate that the emoji uses sprite mode. + */ + public function withSprite(): static + { + return $this->state(fn (array $attributes) => [ + 'sprite_mode' => true, + 'sprite_params' => [ + 'x' => $this->faker->numberBetween(0, 500), + 'y' => $this->faker->numberBetween(0, 500), + 'width' => 32, + 'height' => 32, + 'sheet' => 'emoji-sheet-' . $this->faker->numberBetween(1, 5) . '.png', + ], + 'image_url' => null, + 'emoji_text' => null, + ]); + } + + /** + * Indicate that the emoji is a custom image. + */ + public function customImage(): static + { + return $this->state(fn (array $attributes) => [ + 'emoji_text' => null, + 'image_url' => '/emojis/custom/' . $this->faker->unique()->word() . '.png', + ]); + } +} diff --git a/database/migrations/2025_07_01_200735_create_emoji_categories_table.php b/database/migrations/2025_07_01_200735_create_emoji_categories_table.php new file mode 100644 index 000000000..22738b87d --- /dev/null +++ b/database/migrations/2025_07_01_200735_create_emoji_categories_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('title'); + $table->integer('display_order')->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('emoji_categories'); + } +}; diff --git a/database/migrations/2025_07_01_200744_create_emojis_table.php b/database/migrations/2025_07_01_200744_create_emojis_table.php new file mode 100644 index 000000000..61822822a --- /dev/null +++ b/database/migrations/2025_07_01_200744_create_emojis_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('title'); + $table->string('emoji_text')->nullable(); + $table->string('emoji_shortcode')->unique(); + $table->string('image_url')->nullable(); + $table->boolean('sprite_mode')->default(false); + $table->json('sprite_params')->nullable(); + $table->foreignId('emoji_category_id')->nullable()->constrained('emoji_categories')->nullOnDelete(); + $table->integer('display_order')->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('emojis'); + } +}; diff --git a/database/migrations/2025_07_01_200755_create_emoji_aliases_table.php b/database/migrations/2025_07_01_200755_create_emoji_aliases_table.php new file mode 100644 index 000000000..98a0e2344 --- /dev/null +++ b/database/migrations/2025_07_01_200755_create_emoji_aliases_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('emoji_id')->constrained('emojis')->cascadeOnDelete(); + $table->string('alias')->unique(); + $table->timestamps(); + + // Composite index for performance when joining + $table->index(['emoji_id', 'alias']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('emoji_aliases'); + } +}; diff --git a/database/seeders/EmojiAliasSeeder.php b/database/seeders/EmojiAliasSeeder.php new file mode 100644 index 000000000..6cc9e5c3a --- /dev/null +++ b/database/seeders/EmojiAliasSeeder.php @@ -0,0 +1,16 @@ + ['title' => 'Smileys', 'display_order' => 1], + 'People' => ['title' => 'People', 'display_order' => 2], + 'Nature' => ['title' => 'Nature', 'display_order' => 3], + 'Food' => ['title' => 'Food', 'display_order' => 4], + 'Objects' => ['title' => 'Objects', 'display_order' => 5], + ]; + + $categoryModels = []; + foreach ($categories as $key => $data) { + $categoryModels[$key] = EmojiCategory::create($data); + } + + // Define emojis with their categories and aliases + $emojis = [ + // Smileys + [ + 'category' => 'Smileys', + 'emojis' => [ + ['title' => 'Grinning', 'emoji_text' => '😀', 'emoji_shortcode' => ':grinning:', 'aliases' => [':grin:']], + ['title' => 'Smile', 'emoji_text' => '😊', 'emoji_shortcode' => ':smile:', 'aliases' => [':blush:', ':happy:']], + ['title' => 'Laughing', 'emoji_text' => '😂', 'emoji_shortcode' => ':joy:', 'aliases' => [':laughing:', ':lol:']], + ['title' => 'Wink', 'emoji_text' => '😉', 'emoji_shortcode' => ':wink:', 'aliases' => [';)', ':winking:']], + ['title' => 'Heart Eyes', 'emoji_text' => '😍', 'emoji_shortcode' => ':heart_eyes:', 'aliases' => [':love:']], + ['title' => 'Thinking', 'emoji_text' => 'ðŸĪ”', 'emoji_shortcode' => ':thinking:', 'aliases' => [':think:', ':hmm:']], + ['title' => 'Sad', 'emoji_text' => 'ðŸ˜Ē', 'emoji_shortcode' => ':cry:', 'aliases' => [':sad:', ':tear:']], + ['title' => 'Angry', 'emoji_text' => '😠', 'emoji_shortcode' => ':angry:', 'aliases' => [':mad:', ':rage:']], + ['title' => 'Thumbs Up', 'emoji_text' => '👍', 'emoji_shortcode' => ':thumbsup:', 'aliases' => [':+1:', ':like:']], + ['title' => 'Thumbs Down', 'emoji_text' => '👎', 'emoji_shortcode' => ':thumbsdown:', 'aliases' => [':-1:', ':dislike:']], + ], + ], + // People + [ + 'category' => 'People', + 'emojis' => [ + ['title' => 'Clap', 'emoji_text' => '👏', 'emoji_shortcode' => ':clap:', 'aliases' => [':applause:']], + ['title' => 'Wave', 'emoji_text' => '👋', 'emoji_shortcode' => ':wave:', 'aliases' => [':hello:', ':bye:']], + ['title' => 'OK Hand', 'emoji_text' => '👌', 'emoji_shortcode' => ':ok_hand:', 'aliases' => [':ok:', ':perfect:']], + ['title' => 'Victory', 'emoji_text' => '✌ïļ', 'emoji_shortcode' => ':v:', 'aliases' => [':victory:', ':peace:']], + ['title' => 'Muscle', 'emoji_text' => '💊', 'emoji_shortcode' => ':muscle:', 'aliases' => [':strong:', ':flex:']], + ], + ], + // Nature + [ + 'category' => 'Nature', + 'emojis' => [ + ['title' => 'Sun', 'emoji_text' => '☀ïļ', 'emoji_shortcode' => ':sunny:', 'aliases' => [':sun:']], + ['title' => 'Fire', 'emoji_text' => 'ðŸ”Ĩ', 'emoji_shortcode' => ':fire:', 'aliases' => [':flame:', ':hot:']], + ['title' => 'Star', 'emoji_text' => '⭐', 'emoji_shortcode' => ':star:', 'aliases' => []], + ['title' => 'Rainbow', 'emoji_text' => '🌈', 'emoji_shortcode' => ':rainbow:', 'aliases' => []], + ['title' => 'Plant', 'emoji_text' => 'ðŸŒą', 'emoji_shortcode' => ':seedling:', 'aliases' => [':plant:', ':sprout:']], + ], + ], + // Food + [ + 'category' => 'Food', + 'emojis' => [ + ['title' => 'Pizza', 'emoji_text' => '🍕', 'emoji_shortcode' => ':pizza:', 'aliases' => []], + ['title' => 'Coffee', 'emoji_text' => '☕', 'emoji_shortcode' => ':coffee:', 'aliases' => [':cafe:']], + ['title' => 'Beer', 'emoji_text' => '🍚', 'emoji_shortcode' => ':beer:', 'aliases' => [':beers:']], + ['title' => 'Cake', 'emoji_text' => '🎂', 'emoji_shortcode' => ':birthday:', 'aliases' => [':cake:']], + ['title' => 'Apple', 'emoji_text' => '🍎', 'emoji_shortcode' => ':apple:', 'aliases' => []], + ], + ], + // Objects + [ + 'category' => 'Objects', + 'emojis' => [ + ['title' => 'Heart', 'emoji_text' => 'âĪïļ', 'emoji_shortcode' => ':heart:', 'aliases' => [':love_heart:', ':red_heart:']], + ['title' => 'Broken Heart', 'emoji_text' => '💔', 'emoji_shortcode' => ':broken_heart:', 'aliases' => []], + ['title' => 'Alarm Clock', 'emoji_text' => '⏰', 'emoji_shortcode' => ':alarm_clock:', 'aliases' => [':alarm:']], + ['title' => 'Check Mark', 'emoji_text' => '✅', 'emoji_shortcode' => ':white_check_mark:', 'aliases' => [':check:', ':done:']], + ['title' => 'X Mark', 'emoji_text' => '❌', 'emoji_shortcode' => ':x:', 'aliases' => [':cross:', ':no:']], + ], + ], + ]; + + // Insert emojis + $displayOrder = 0; + foreach ($emojis as $categoryData) { + $category = $categoryModels[$categoryData['category']]; + + foreach ($categoryData['emojis'] as $emojiData) { + $displayOrder++; + + $emoji = Emoji::create([ + 'title' => $emojiData['title'], + 'emoji_text' => $emojiData['emoji_text'], + 'emoji_shortcode' => $emojiData['emoji_shortcode'], + 'image_url' => null, + 'sprite_mode' => false, + 'sprite_params' => null, + 'emoji_category_id' => $category->id, + 'display_order' => $displayOrder, + ]); + + // Create aliases + foreach ($emojiData['aliases'] as $alias) { + EmojiAlias::create([ + 'emoji_id' => $emoji->id, + 'alias' => $alias, + ]); + } + } + } + } +} diff --git a/docs/docs/api/_category_.json b/docs/docs/api/_category_.json new file mode 100644 index 000000000..aad13ed53 --- /dev/null +++ b/docs/docs/api/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "API", + "position": 8, + "link": { + "type": "generated-index", + "description": "Complete API documentation for all endpoints" + } +} diff --git a/docs/docs/api/emojis/_category_.json b/docs/docs/api/emojis/_category_.json new file mode 100644 index 000000000..5741077c9 --- /dev/null +++ b/docs/docs/api/emojis/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Emoji", + "position": 2, + "link": { + "type": "generated-index", + "description": "API endpoints for managing emojis, categories, and aliases" + } +} diff --git a/docs/docs/api/emojis/aliases.md b/docs/docs/api/emojis/aliases.md new file mode 100644 index 000000000..f007eed65 --- /dev/null +++ b/docs/docs/api/emojis/aliases.md @@ -0,0 +1,456 @@ +--- +title: Emoji Aliases API +sidebar_position: 2 +--- + +# Emoji Aliases API + +The Emoji Aliases API allows you to manage alternative shortcodes for emojis, enabling multiple text triggers to map to the same emoji (e.g., `:happy:`, `:joy:`, `:lol:` all pointing to 😂). + +## Base URL + +``` +/api/emoji/aliases +``` + +## Endpoints + +### List Aliases + +Get a paginated list of emoji aliases with optional filtering and relationship loading. + +```http +GET /api/emoji/aliases +``` + +#### Query Parameters + +| Parameter | Type | Description | +|--------------|---------|----------------------------------------| +| `emoji_id` | integer | Filter by emoji ID | +| `search` | string | Search in alias text | +| `with_emoji` | boolean | Include emoji and category information | +| `page` | integer | Page number (default: 1) | +| `per_page` | integer | Items per page (default: 50, max: 100) | + +#### Example Request + +```bash +curl -X GET "https://api.example.com/api/emoji/aliases?emoji_id=1&with_emoji=1" \ + -H "Accept: application/json" +``` + +#### Example Response + +```json +{ + "data": [ + { + "id": 1, + "alias": ":grin:", + "emoji": { + "id": 1, + "title": "Grinning Face", + "emoji_text": "😀", + "emoji_shortcode": ":grinning:", + "image_url": null, + "sprite_mode": false, + "sprite_params": null, + "display_order": 1, + "category": { + "id": 1, + "title": "Smileys", + "display_order": 1, + "emojis_count": null, + "emojis": null, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + }, + "aliases": null, + "aliases_count": null, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + }, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + }, + { + "id": 2, + "alias": ":happy:", + "emoji": { + "id": 1, + "title": "Grinning Face", + "emoji_text": "😀", + "emoji_shortcode": ":grinning:", + "image_url": null, + "sprite_mode": false, + "sprite_params": null, + "display_order": 1, + "category": { + "id": 1, + "title": "Smileys", + "display_order": 1, + "emojis_count": null, + "emojis": null, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + }, + "aliases": null, + "aliases_count": null, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + }, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + } + ], + "links": { + "first": "https://api.example.com/api/emoji/aliases?page=1", + "last": "https://api.example.com/api/emoji/aliases?page=1", + "prev": null, + "next": null + }, + "meta": { + "current_page": 1, + "per_page": 50, + "total": 2 + } +} +``` + +--- + +### Search Aliases + +Search emoji aliases using Laravel Scout for full-text search capabilities. + +```http +GET /api/emoji/aliases/search +``` + +#### Query Parameters + +| Parameter | Type | Required | Description | +|--------------|---------|----------|-------------------------------------------| +| `q` | string | Yes | Search query (minimum 1 character) | +| `limit` | integer | No | Number of results (default: 20, max: 100) | +| `with_emoji` | boolean | No | Include emoji and category information | + +#### Example Request + +```bash +curl -X GET "https://api.example.com/api/emoji/aliases/search?q=hap&limit=5&with_emoji=1" \ + -H "Accept: application/json" +``` + +#### Example Response + +```json +{ + "data": [ + { + "id": 2, + "alias": ":happy:", + "emoji": { + "id": 1, + "title": "Grinning Face", + "emoji_text": "😀", + "emoji_shortcode": ":grinning:", + "image_url": null, + "sprite_mode": false, + "sprite_params": null, + "display_order": 1, + "category": { + "id": 1, + "title": "Smileys", + "display_order": 1, + "emojis_count": null, + "emojis": null, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + }, + "aliases": null, + "aliases_count": null, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + }, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + } + ] +} +``` + +--- + +### Get Alias + +Get a specific emoji alias by ID with full relationship data. + +```http +GET /api/emoji/aliases/{id} +``` + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|---------|--------------| +| `id` | integer | The alias ID | + +#### Example Request + +```bash +curl -X GET "https://api.example.com/api/emoji/aliases/1" \ + -H "Accept: application/json" +``` + +#### Example Response + +```json +{ + "data": { + "id": 1, + "alias": ":grin:", + "emoji": { + "id": 1, + "title": "Grinning Face", + "emoji_text": "😀", + "emoji_shortcode": ":grinning:", + "image_url": null, + "sprite_mode": false, + "sprite_params": null, + "display_order": 1, + "category": { + "id": 1, + "title": "Smileys", + "display_order": 1, + "emojis_count": null, + "emojis": null, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + }, + "aliases": null, + "aliases_count": null, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + }, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + } +} +``` + +--- + +### Create Alias + +Create a new alias for an existing emoji. + +```http +POST /api/emoji/aliases +``` + +#### Request Body + +| Field | Type | Required | Description | +|------------|---------|----------|------------------------------------------------------| +| `emoji_id` | integer | Yes | The emoji ID this alias points to | +| `alias` | string | Yes | The alias in `:name:` format (max 255 chars, unique) | + +#### Example Request + +```bash +curl -X POST "https://api.example.com/api/emoji/aliases" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d '{ + "emoji_id": 1, + "alias": ":cheerful:" + }' +``` + +#### Example Response + +```json +{ + "data": { + "id": 3, + "alias": ":cheerful:", + "emoji": null, + "created_at": "2025-01-01T12:00:00Z", + "updated_at": "2025-01-01T12:00:00Z" + } +} +``` + +--- + +### Update Alias + +Update an existing emoji alias. + +```http +PUT /api/emoji/aliases/{id} +PATCH /api/emoji/aliases/{id} +``` + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|---------|--------------| +| `id` | integer | The alias ID | + +#### Request Body + +| Field | Type | Required | Description | +|------------|---------|----------|------------------------------------------------------| +| `emoji_id` | integer | No | The emoji ID this alias points to | +| `alias` | string | No | The alias in `:name:` format (max 255 chars, unique) | + +#### Example Request + +```bash +curl -X PATCH "https://api.example.com/api/emoji/aliases/3" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d '{ + "alias": ":joyful:" + }' +``` + +#### Example Response + +```json +{ + "data": { + "id": 3, + "alias": ":joyful:", + "emoji": null, + "created_at": "2025-01-01T12:00:00Z", + "updated_at": "2025-01-01T12:30:00Z" + } +} +``` + +--- + +### Delete Alias + +Delete an emoji alias. The associated emoji remains unchanged. + +```http +DELETE /api/emoji/aliases/{id} +``` + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|---------|--------------| +| `id` | integer | The alias ID | + +#### Example Request + +```bash +curl -X DELETE "https://api.example.com/api/emoji/aliases/3" \ + -H "Accept: application/json" +``` + +#### Example Response + +``` +HTTP/1.1 204 No Content +``` + +## Error Responses + +### 404 Not Found + +```json +{ + "message": "No query results for model [App\\Models\\EmojiAlias] 999" +} +``` + +### 422 Validation Error + +```json +{ + "message": "The given data was invalid.", + "errors": { + "emoji_id": ["The selected emoji id is invalid."], + "alias": [ + "The alias must be in the format :name: (e.g., :happy:)", + "This alias is already taken.", + "This alias conflicts with an existing emoji shortcode." + ] + } +} +``` + +### 400 Search Validation Error + +```json +{ + "message": "The given data was invalid.", + "errors": { + "q": ["The q field is required."], + "limit": ["The limit must not be greater than 100."] + } +} +``` + +## Use Cases + +Aliases are useful for creating alternative ways to access the same emoji: + +### 1. Multiple Language Support + +```json +{ + "emoji_shortcode": ":smile:", + "aliases": [":sonrisa:", ":sourire:", ":įŽ‘éĄ”:"] +} +``` + +### 2. Slack-style Flexibility + +```json +{ + "emoji_shortcode": ":thumbsup:", + "aliases": [":+1:", ":like:", ":approve:"] +} +``` + +### 3. Legacy Compatibility + +```json +{ + "emoji_shortcode": ":laughing:", + "aliases": [":lol:", ":rofl:", ":-D"] +} +``` + +### 4. Common Misspellings + +```json +{ + "emoji_shortcode": ":receive:", + "aliases": [":recieve:", ":recive:"] +} +``` + +## Validation Rules + +- **alias**: Must be unique across all aliases and cannot conflict with any emoji shortcode +- **Format**: Must follow the `:name:` pattern using letters, numbers, underscores, and hyphens +- **emoji_id**: Must reference an existing emoji +- **Cross-table uniqueness**: Aliases cannot use the same text as any emoji's primary shortcode + +## Notes + +- Aliases are returned ordered by `alias` alphabetically +- When an emoji is deleted, all its aliases are automatically deleted (cascade) +- Search functionality uses Laravel Scout for fast full-text search +- Maximum search results per request is 100 +- Aliases can be reassigned to different emojis by updating the `emoji_id` +- The same alias text cannot exist more than once in the system diff --git a/docs/docs/api/emojis/categories.md b/docs/docs/api/emojis/categories.md new file mode 100644 index 000000000..bcde66a41 --- /dev/null +++ b/docs/docs/api/emojis/categories.md @@ -0,0 +1,282 @@ +--- +title: Emoji Categories API +sidebar_position: 3 +--- + +# Emoji Categories API + +The Emoji Categories API allows you to manage emoji categories used to organize emojis in the editor interface. + +## Base URL + +``` +/api/emoji/categories +``` + +## Endpoints + +### List Categories + +Get a list of all emoji categories. + +```http +GET /api/emoji/categories +``` + +#### Query Parameters + +| Parameter | Type | Description | +|------------------|---------|-------------------------------------------| +| `with_emojis` | boolean | Include emoji count for each category | +| `include_emojis` | boolean | Include full emoji data for each category | + +#### Example Request + +```bash +curl -X GET "https://api.example.com/api/emoji/categories?with_emojis=1" \ + -H "Accept: application/json" +``` + +#### Example Response + +```json +{ + "data": [ + { + "id": 1, + "title": "Smileys", + "display_order": 1, + "emojis_count": 15, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + }, + { + "id": 2, + "title": "People", + "display_order": 2, + "emojis_count": 8, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + } + ] +} +``` + +--- + +### Get Category + +Get a specific emoji category by ID. + +```http +GET /api/emoji/categories/{id} +``` + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|---------|-----------------| +| `id` | integer | The category ID | + +#### Query Parameters + +| Parameter | Type | Description | +|----------------|---------|-------------------------------------------| +| `with_aliases` | boolean | Include emoji aliases when loading emojis | + +#### Example Request + +```bash +curl -X GET "https://api.example.com/api/emoji/categories/1?with_aliases=1" \ + -H "Accept: application/json" +``` + +#### Example Response + +```json +{ + "data": { + "id": 1, + "title": "Smileys", + "display_order": 1, + "emojis": [ + { + "id": 1, + "title": "Grinning", + "emoji_text": "😀", + "emoji_shortcode": ":grinning:", + "image_url": null, + "sprite_mode": false, + "sprite_params": null, + "display_order": 1, + "aliases": [ + { + "id": 1, + "alias": ":grin:", + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + } + ], + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + } + ], + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + } +} +``` + +--- + +### Create Category + +Create a new emoji category. + +```http +POST /api/emoji/categories +``` + +#### Request Body + +| Field | Type | Required | Description | +|-----------------|---------|----------|-------------------------------| +| `title` | string | Yes | Category name (max 255 chars) | +| `display_order` | integer | Yes | Display order (minimum 0) | + +#### Example Request + +```bash +curl -X POST "https://api.example.com/api/emoji/categories" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d '{ + "title": "Custom Category", + "display_order": 10 + }' +``` + +#### Example Response + +```json +{ + "data": { + "id": 3, + "title": "Custom Category", + "display_order": 10, + "emojis_count": null, + "emojis": null, + "created_at": "2025-01-01T12:00:00Z", + "updated_at": "2025-01-01T12:00:00Z" + } +} +``` + +--- + +### Update Category + +Update an existing emoji category. + +```http +PUT /api/emoji/categories/{id} +PATCH /api/emoji/categories/{id} +``` + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|---------|-----------------| +| `id` | integer | The category ID | + +#### Request Body + +| Field | Type | Required | Description | +|-----------------|---------|----------|-------------------------------| +| `title` | string | No | Category name (max 255 chars) | +| `display_order` | integer | No | Display order (minimum 0) | + +#### Example Request + +```bash +curl -X PATCH "https://api.example.com/api/emoji/categories/3" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d '{ + "title": "Updated Category Name" + }' +``` + +#### Example Response + +```json +{ + "data": { + "id": 3, + "title": "Updated Category Name", + "display_order": 10, + "emojis_count": null, + "emojis": null, + "created_at": "2025-01-01T12:00:00Z", + "updated_at": "2025-01-01T12:30:00Z" + } +} +``` + +--- + +### Delete Category + +Delete an emoji category. Associated emojis will have their category_id set to null. + +```http +DELETE /api/emoji/categories/{id} +``` + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|---------|-----------------| +| `id` | integer | The category ID | + +#### Example Request + +```bash +curl -X DELETE "https://api.example.com/api/emoji/categories/3" \ + -H "Accept: application/json" +``` + +#### Example Response + +``` +HTTP/1.1 204 No Content +``` + +## Error Responses + +### 404 Not Found + +```json +{ + "message": "No query results for model [App\\Models\\EmojiCategory] 999" +} +``` + +### 422 Validation Error + +```json +{ + "message": "The given data was invalid.", + "errors": { + "title": ["The title field is required."], + "display_order": ["The display order must be at least 0."] + } +} +``` + +## Notes + +- Categories are returned ordered by `display_order` ascending +- When a category is deleted, associated emojis remain but their `emoji_category_id` is set to null +- The `emojis_count` field is only included when `with_emojis=1` parameter is used +- The `emojis` field is only included when `include_emojis=1` parameter is used diff --git a/docs/docs/api/emojis/emojis.md b/docs/docs/api/emojis/emojis.md new file mode 100644 index 000000000..9488b36c9 --- /dev/null +++ b/docs/docs/api/emojis/emojis.md @@ -0,0 +1,458 @@ +--- +title: Emoji API +sidebar_position: 1 +--- + +# Emoji API + +The Emoji API allows you to manage individual emojis, including Unicode emojis, custom images, and CSS sprite-based emojis. + +## Base URL + +``` +/api/emoji/emojis +``` + +## Endpoints + +### List Emoji + +Get a paginated list of emojis with optional filtering and relationship loading. + +```http +GET /api/emoji/emojis +``` + +#### Query Parameters + +| Parameter | Type | Description | +|-----------------|---------|-------------------------------------------| +| `category_id` | integer | Filter by category ID | +| `search` | string | Search in shortcode, title, or emoji text | +| `with_category` | boolean | Include category information | +| `with_aliases` | boolean | Include emoji aliases | +| `page` | integer | Page number (default: 1) | +| `per_page` | integer | Items per page (default: 50, max: 100) | + +#### Example Request + +```bash +curl -X GET "https://api.example.com/api/emoji/emojis?category_id=1&with_aliases=1&per_page=25" \ + -H "Accept: application/json" +``` + +#### Example Response + +```json +{ + "data": [ + { + "id": 1, + "title": "Grinning Face", + "emoji_text": "😀", + "emoji_shortcode": ":grinning:", + "image_url": null, + "sprite_mode": false, + "sprite_params": null, + "display_order": 1, + "category": { + "id": 1, + "title": "Smileys", + "display_order": 1, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + }, + "aliases": [ + { + "id": 1, + "alias": ":grin:", + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + } + ], + "aliases_count": 1, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + } + ], + "links": { + "first": "https://api.example.com/api/emoji/emojis?page=1", + "last": "https://api.example.com/api/emoji/emojis?page=3", + "prev": null, + "next": "https://api.example.com/api/emoji/emojis?page=2" + }, + "meta": { + "current_page": 1, + "per_page": 25, + "total": 67 + } +} +``` + +--- + +### Search Emoji + +Search emojis using Laravel Scout for full-text search capabilities. + +```http +GET /api/emoji/emojis/search +``` + +#### Query Parameters + +| Parameter | Type | Required | Description | +|-----------------|---------|----------|-------------------------------------------| +| `q` | string | Yes | Search query (minimum 1 character) | +| `limit` | integer | No | Number of results (default: 20, max: 100) | +| `with_category` | boolean | No | Include category information | +| `with_aliases` | boolean | No | Include emoji aliases | + +#### Example Request + +```bash +curl -X GET "https://api.example.com/api/emoji/emojis/search?q=smile&limit=10&with_category=1" \ + -H "Accept: application/json" +``` + +#### Example Response + +```json +{ + "data": [ + { + "id": 1, + "title": "Smiling Face", + "emoji_text": "😊", + "emoji_shortcode": ":smile:", + "image_url": null, + "sprite_mode": false, + "sprite_params": null, + "display_order": 2, + "category": { + "id": 1, + "title": "Smileys", + "display_order": 1, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + }, + "aliases": null, + "aliases_count": null, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + } + ] +} +``` + +--- + +### Get Emoji + +Get a specific emoji by ID with full relationship data. + +```http +GET /api/emoji/emojis/{id} +``` + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|---------|--------------| +| `id` | integer | The emoji ID | + +#### Example Request + +```bash +curl -X GET "https://api.example.com/api/emoji/emojis/1" \ + -H "Accept: application/json" +``` + +#### Example Response + +```json +{ + "data": { + "id": 1, + "title": "Grinning Face", + "emoji_text": "😀", + "emoji_shortcode": ":grinning:", + "image_url": null, + "sprite_mode": false, + "sprite_params": null, + "display_order": 1, + "category": { + "id": 1, + "title": "Smileys", + "display_order": 1, + "emojis_count": null, + "emojis": null, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + }, + "aliases": [ + { + "id": 1, + "alias": ":grin:", + "emoji": null, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + } + ], + "aliases_count": null, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" + } +} +``` + +--- + +### Create Emoji + +Create a new emoji. Supports Unicode emojis, custom images, and CSS sprites. + +```http +POST /api/emoji/emojis +``` + +#### Request Body + +| Field | Type | Required | Description | +|------------------------|---------|-------------|---------------------------------------------------------| +| `title` | string | Yes | Human-readable name (max 255 chars) | +| `emoji_text` | string | No | Unicode emoji or text emoticon (max 10 chars) | +| `emoji_shortcode` | string | Yes | Primary shortcode in `:name:` format (unique) | +| `image_url` | string | No | Path to custom image (max 500 chars) | +| `sprite_mode` | boolean | No | Whether to use CSS sprite mode (default: false) | +| `sprite_params` | object | No | Sprite parameters (required if sprite_mode is true) | +| `sprite_params.x` | integer | Conditional | X coordinate (required if sprite_mode is true) | +| `sprite_params.y` | integer | Conditional | Y coordinate (required if sprite_mode is true) | +| `sprite_params.width` | integer | Conditional | Sprite width (required if sprite_mode is true) | +| `sprite_params.height` | integer | Conditional | Sprite height (required if sprite_mode is true) | +| `sprite_params.sheet` | string | Conditional | Sprite sheet filename (required if sprite_mode is true) | +| `emoji_category_id` | integer | No | Category ID (must exist) | +| `display_order` | integer | Yes | Display order within category (minimum 0) | + +#### Example Requests + +**Unicode Emoji:** +```bash +curl -X POST "https://api.example.com/api/emoji/emojis" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d '{ + "title": "Heart Eyes", + "emoji_text": "😍", + "emoji_shortcode": ":heart_eyes:", + "emoji_category_id": 1, + "display_order": 5 + }' +``` + +**Custom Image Emoji:** +```bash +curl -X POST "https://api.example.com/api/emoji/emojis" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d '{ + "title": "Party Parrot", + "emoji_shortcode": ":partyparrot:", + "image_url": "/emojis/custom/partyparrot.gif", + "emoji_category_id": 2, + "display_order": 1 + }' +``` + +**CSS Sprite Emoji:** +```bash +curl -X POST "https://api.example.com/api/emoji/emojis" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d '{ + "title": "Custom Sprite", + "emoji_shortcode": ":custom_sprite:", + "sprite_mode": true, + "sprite_params": { + "x": 32, + "y": 64, + "width": 32, + "height": 32, + "sheet": "emoji-sheet-1.png" + }, + "emoji_category_id": 3, + "display_order": 10 + }' +``` + +#### Example Response + +```json +{ + "data": { + "id": 25, + "title": "Heart Eyes", + "emoji_text": "😍", + "emoji_shortcode": ":heart_eyes:", + "image_url": null, + "sprite_mode": false, + "sprite_params": null, + "display_order": 5, + "category": null, + "aliases": null, + "aliases_count": null, + "created_at": "2025-01-01T12:00:00Z", + "updated_at": "2025-01-01T12:00:00Z" + } +} +``` + +--- + +### Update Emoji + +Update an existing emoji. + +```http +PUT /api/emoji/emojis/{id} +PATCH /api/emoji/emojis/{id} +``` + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|---------|--------------| +| `id` | integer | The emoji ID | + +#### Request Body + +All fields from the create endpoint are supported, but all are optional for updates. + +#### Example Request + +```bash +curl -X PATCH "https://api.example.com/api/emoji/emojis/25" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d '{ + "title": "Love Eyes", + "display_order": 6 + }' +``` + +#### Example Response + +```json +{ + "data": { + "id": 25, + "title": "Love Eyes", + "emoji_text": "😍", + "emoji_shortcode": ":heart_eyes:", + "image_url": null, + "sprite_mode": false, + "sprite_params": null, + "display_order": 6, + "category": null, + "aliases": null, + "aliases_count": null, + "created_at": "2025-01-01T12:00:00Z", + "updated_at": "2025-01-01T12:30:00Z" + } +} +``` + +--- + +### Delete Emoji + +Delete an emoji. Associated aliases will be automatically deleted. + +```http +DELETE /api/emoji/emojis/{id} +``` + +#### Path Parameters + +| Parameter | Type | Description | +|-----------|---------|--------------| +| `id` | integer | The emoji ID | + +#### Example Request + +```bash +curl -X DELETE "https://api.example.com/api/emoji/emojis/25" \ + -H "Accept: application/json" +``` + +#### Example Response + +``` +HTTP/1.1 204 No Content +``` + +## Error Responses + +### 422 Validation Error + +```json +{ + "message": "The given data was invalid.", + "errors": { + "emoji_shortcode": [ + "The emoji shortcode must be in the format :name: (e.g., :smile:)", + "This shortcode is already taken." + ], + "sprite_params.x": [ + "The sprite params.x field is required when sprite mode is true." + ] + } +} +``` + +### 400 Search Validation Error + +```json +{ + "message": "The given data was invalid.", + "errors": { + "q": ["The q field is required."], + "limit": ["The limit must not be greater than 100."] + } +} +``` + +## Emoji Types + +The API supports three types of emojis: + +### 1. Unicode Emoji +- Have `emoji_text` field set to the Unicode character +- `image_url` and `sprite_params` are null +- `sprite_mode` is false + +### 2. Custom Image Emoji +- Have `image_url` field set to the image path +- `emoji_text` and `sprite_params` are null +- `sprite_mode` is false + +### 3. CSS Sprite Emoji +- Have `sprite_mode` set to true +- Have `sprite_params` object with position and dimensions +- `emoji_text` and `image_url` are null + +## Validation Rules + +- **emoji_shortcode**: Must be unique across all emojis and cannot conflict with any alias +- **Aliases**: Cannot conflict with any existing emoji shortcode +- **Format**: All shortcodes and aliases must follow the `:name:` pattern +- **Sprite params**: Required when `sprite_mode` is true +- **Category**: Must reference an existing category if provided + +## Notes + +- Emoji are returned ordered by `display_order` ascending +- When an emoji is deleted, all associated aliases are automatically deleted (cascade) +- Search functionality uses Laravel Scout for fast full-text search +- The `aliases_count` field is only included when aliases are not loaded +- Maximum search results per request is 100 diff --git a/docs/docs/api/overview.md b/docs/docs/api/overview.md new file mode 100644 index 000000000..e6798bbb9 --- /dev/null +++ b/docs/docs/api/overview.md @@ -0,0 +1,163 @@ +--- +sidebar_position: 1 +title: Overview +--- + +# API Overview + +Welcome to the API documentation. This section covers all available REST API endpoints for interacting with the application programmatically. + +## Base URL + +All API endpoints are prefixed with `/api/` and use the following base URL: + +``` +https://your-domain.com/api/ +``` + +## Authentication + +The API uses Laravel Sanctum for authentication. You can authenticate using: + +### 1. Session-based Authentication (Web) +For web applications using the same domain, authentication is handled via Laravel's built-in session management. + +### 2. Token-based Authentication (API) +For external applications or mobile apps, use API tokens: + +```bash +# Get your user token +curl -X POST https://your-domain.com/api/login \ + -H "Content-Type: application/json" \ + -d '{"email": "user@example.com", "password": "password"}' +``` + +Include the token in the Authorization header: + +```bash +Authorization: Bearer your-token-here +``` + +## Response Format + +All API responses follow a consistent JSON format: + +### Success Response +```json +{ + "data": { + "id": 1, + "title": "Example" + } +} +``` + +### Collection Response +```json +{ + "data": [ + { + "id": 1, + "title": "Example 1" + }, + { + "id": 2, + "title": "Example 2" + } + ], + "links": { + "first": "https://api.example.com/emoji/emojis?page=1", + "last": "https://api.example.com/emoji/emojis?page=10", + "prev": null, + "next": "https://api.example.com/emoji/emojis?page=2" + }, + "meta": { + "current_page": 1, + "per_page": 50, + "total": 500 + } +} +``` + +### Error Response +```json +{ + "message": "Validation failed", + "errors": { + "title": ["The title field is required."] + } +} +``` + +## HTTP Status Codes + +| Code | Description | +|------|--------------------------------------------| +| 200 | OK - Request successful | +| 201 | Created - Resource created successfully | +| 204 | No Content - Resource deleted successfully | +| 400 | Bad Request - Invalid request data | +| 401 | Unauthorized - Authentication required | +| 403 | Forbidden - Insufficient permissions | +| 404 | Not Found - Resource not found | +| 422 | Unprocessable Entity - Validation failed | +| 500 | Internal Server Error - Server error | + +## Rate Limiting + +API requests are rate-limited to prevent abuse: + +- **Authenticated users**: 60 requests per minute +- **Unauthenticated users**: 20 requests per minute + +Rate limit headers are included in responses: + +``` +X-RateLimit-Limit: 60 +X-RateLimit-Remaining: 59 +X-RateLimit-Reset: 1640995200 +``` + +## Pagination + +List endpoints support pagination using query parameters: + +- `page` - Page number (default: 1) +- `per_page` - Items per page (default: 50, max: 100) + +Example: +``` +GET /api/emoji/emojis?page=2&per_page=25 +``` + +## Filtering and Searching + +Many endpoints support filtering and searching: + +### Query Parameters +- `search` - Text search across relevant fields +- `category_id` - Filter by category ID +- `emoji_id` - Filter by emoji ID (for aliases) + +### Relationship Loading +Use these parameters to include related data: + +- `with_category` - Include category information +- `with_emojis` - Include emoji list (for categories) +- `with_aliases` - Include alias list (for emojis) +- `with_emoji` - Include emoji information (for aliases) + +Example: +``` +GET /api/emoji/emojis?search=smile&with_category=1&with_aliases=1 +``` + +## Versioning + +The current API version is v1. Future versions will be available at: + +``` +/api/v2/endpoint +``` + +Breaking changes will always result in a new API version. diff --git a/docs/docs/contributing.md b/docs/docs/contributing.md index b40ff39c8..81269b86a 100644 --- a/docs/docs/contributing.md +++ b/docs/docs/contributing.md @@ -1,5 +1,5 @@ --- -sidebar_position: 2 +sidebar_position: 9 title: Contributing --- diff --git a/docs/docs/legacy/_category_.json b/docs/docs/legacy/_category_.json index 5ad3faba9..dc0363268 100644 --- a/docs/docs/legacy/_category_.json +++ b/docs/docs/legacy/_category_.json @@ -1,10 +1,10 @@ { "label": "Legacy System", - "position": 6, + "position": 7, "link": { "type": "generated-index", "title": "Legacy System Documentation", "description": "Documentation and analysis of the legacy TorrentPier v2.x system that is being modernized with Laravel.", "keywords": ["legacy", "torrentpier", "migration"] } -} \ No newline at end of file +} diff --git a/docs/docs/migration/_category_.json b/docs/docs/migration/_category_.json index 02de71f40..f9a40fede 100644 --- a/docs/docs/migration/_category_.json +++ b/docs/docs/migration/_category_.json @@ -1,8 +1,8 @@ { "label": "Migration Guide", - "position": 5, + "position": 6, "link": { "type": "generated-index", "description": "Guides for migrating from TorrentPier v2.x to the new Laravel version." } -} \ No newline at end of file +} diff --git a/docs/docs/models/_category_.json b/docs/docs/models/_category_.json new file mode 100644 index 000000000..586b76377 --- /dev/null +++ b/docs/docs/models/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Models", + "position": 5, + "link": { + "type": "generated-index", + "description": "Eloquent Models documentation for the application" + } +} diff --git a/docs/docs/models/emoji-alias.md b/docs/docs/models/emoji-alias.md new file mode 100644 index 000000000..f2fe927fb --- /dev/null +++ b/docs/docs/models/emoji-alias.md @@ -0,0 +1,183 @@ +--- +sidebar_position: 2 +title: EmojiAlias +--- + +# EmojiAlias Model + +The `EmojiAlias` model represents additional text aliases for an emoji, allowing multiple shortcodes to map to the same emoji (e.g., `:happy:`, `:joy:`, `:lol:` all mapping to 😂). + +## Model Properties + +### Table Name +- `emoji_aliases` + +### Fillable Fields +- `emoji_id` - Foreign key to the associated emoji +- `alias` - Alternative shortcode (e.g., `:happy:`) - unique + +### Timestamps +- `created_at` +- `updated_at` + +## Traits + +### Searchable (Laravel Scout) +The model uses Laravel Scout for alias search functionality. + +```php +public function toSearchableArray() +{ + return [ + 'id' => $this->id, + 'alias' => $this->alias, + 'emoji_id' => $this->emoji_id, + ]; +} +``` + +## Relationships + +### Belongs To: Emoji + +```php +public function emoji(): BelongsTo +{ + return $this->belongsTo(Emoji::class); +} +``` + +## Usage Examples + +### Creating Aliases + +```php +use App\Models\EmojiAlias; +use App\Models\Emoji; + +// Create an alias for an existing emoji +$emoji = Emoji::where('emoji_shortcode', ':joy:')->first(); + +$alias = EmojiAlias::create([ + 'emoji_id' => $emoji->id, + 'alias' => ':lol:' +]); + +// Create multiple aliases via the emoji relationship +$emoji->aliases()->createMany([ + ['alias' => ':laughing:'], + ['alias' => ':rofl:'], + ['alias' => ':lmao:'] +]); +``` + +### Retrieving Aliases + +```php +// Find emoji by alias +$alias = EmojiAlias::where('alias', ':lol:')->first(); +$emoji = $alias->emoji; + +// Get all aliases for an emoji +$emoji = Emoji::find(1); +$aliases = $emoji->aliases; + +// Search for aliases +$results = EmojiAlias::search(':hap')->get(); +``` + +### Working with Emoji Through Alias + +```php +// Get the emoji details from an alias +$alias = EmojiAlias::with('emoji')->where('alias', ':lol:')->first(); + +echo $alias->emoji->title; // "Laughing" +echo $alias->emoji->emoji_text; // "😂" +echo $alias->emoji->emoji_shortcode; // ":joy:" +``` + +## Database Schema + +```sql +CREATE TABLE emoji_aliases ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + emoji_id BIGINT UNSIGNED NOT NULL, + alias VARCHAR(255) NOT NULL UNIQUE, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_emoji_id_alias (emoji_id, alias), + FOREIGN KEY (emoji_id) REFERENCES emojis(id) ON DELETE CASCADE +); +``` + +## Factory + +The model includes a factory for testing: + +```php +use App\Models\EmojiAlias; +use App\Models\Emoji; + +// Create an alias with a new emoji +$alias = EmojiAlias::factory()->create(); + +// Create an alias for an existing emoji +$emoji = Emoji::factory()->create(); +$alias = EmojiAlias::factory()->create([ + 'emoji_id' => $emoji->id, + 'alias' => ':custom-alias:' +]); + +// Create multiple aliases +$aliases = EmojiAlias::factory() + ->count(5) + ->for($emoji) + ->create(); +``` + +## Performance Considerations + +- The `alias` field has a unique index for fast lookups during text replacement +- The composite index on `(emoji_id, alias)` optimizes join queries +- Cascade delete ensures aliases are removed when an emoji is deleted +- Scout integration enables fast alias searching + +## Validation Considerations + +When implementing the emoji management system, consider these validation rules: + +1. **Uniqueness Across Tables** - An alias should not match any existing `emoji_shortcode` +2. **Format Validation** - Aliases should follow the `:name:` format +3. **Reserved Keywords** - Certain aliases might be reserved for system use + +Example validation in a request class: + +```php +public function rules() +{ + return [ + 'alias' => [ + 'required', + 'string', + 'regex:/^:[a-zA-Z0-9_-]+:$/', + 'unique:emoji_aliases,alias', + Rule::notIn(Emoji::pluck('emoji_shortcode')->toArray()), + ], + ]; +} +``` + +## Use Cases + +1. **Slack-style Flexibility** - Users can type `:+1:`, `:thumbsup:`, or `:like:` for 👍 +2. **Legacy Support** - Map old emoticon codes to new emoji system +3. **Localization** - Different languages can have their own aliases +4. **User Preferences** - Users could potentially create personal aliases + +## Notes + +- Aliases are automatically deleted when their parent emoji is deleted (cascade) +- Each alias must be unique across the entire system +- The system should validate that an alias doesn't conflict with any emoji shortcode +- Consider implementing a maximum number of aliases per emoji for performance diff --git a/docs/docs/models/emoji-category.md b/docs/docs/models/emoji-category.md new file mode 100644 index 000000000..1eaa22996 --- /dev/null +++ b/docs/docs/models/emoji-category.md @@ -0,0 +1,117 @@ +--- +sidebar_position: 3 +title: EmojiCategory +--- + +# EmojiCategory Model + +The `EmojiCategory` model represents a category for grouping emojis in the editor (e.g., "Smileys", "Animals", "Food"). + +## Model Properties + +### Table Name +- `emoji_categories` + +### Fillable Fields +- `title` - Category name (e.g., "Smileys & Emotion") +- `display_order` - Integer defining the order of the category in the editor + +### Timestamps +- `created_at` +- `updated_at` + +## Relationships + +### Has Many: Emoji + +```php +public function emojis(): HasMany +{ + return $this->hasMany(Emoji::class); +} +``` + +Each category can contain multiple emojis. + +## Usage Examples + +### Creating a Category + +```php +use App\Models\EmojiCategory; + +$category = EmojiCategory::create([ + 'title' => 'Smileys & Emotion', + 'display_order' => 1 +]); +``` + +### Retrieving Categories with Emoji + +```php +// Get all categories ordered by display order +$categories = EmojiCategory::orderBy('display_order')->get(); + +// Get category with all its emojis +$category = EmojiCategory::with('emojis')->find(1); + +// Get category with emojis ordered +$category = EmojiCategory::with(['emojis' => function ($query) { + $query->orderBy('display_order'); +}])->find(1); +``` + +### Accessing Related Emoji + +```php +$category = EmojiCategory::find(1); + +// Access emojis via dynamic property +foreach ($category->emojis as $emoji) { + echo $emoji->emoji_shortcode; +} + +// Query emojis with additional constraints +$activeEmojis = $category->emojis() + ->whereNotNull('emoji_text') + ->get(); +``` + +## Database Schema + +```sql +CREATE TABLE emoji_categories ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + display_order INTEGER NOT NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_display_order (display_order) +); +``` + +## Factory + +The model includes a factory for testing: + +```php +use App\Models\EmojiCategory; + +// Create a single category +$category = EmojiCategory::factory()->create(); + +// Create multiple categories +$categories = EmojiCategory::factory()->count(5)->create(); + +// Create with specific attributes +$category = EmojiCategory::factory()->create([ + 'title' => 'Custom Emoji', + 'display_order' => 10 +]); +``` + +## Notes + +- The `display_order` field is indexed for performance when ordering categories +- Categories can be soft-deleted if needed in future implementations +- When an emoji category is deleted, associated emojis will have their `emoji_category_id` set to null (not cascade delete) diff --git a/docs/docs/models/emoji.md b/docs/docs/models/emoji.md new file mode 100644 index 000000000..7cced9e4c --- /dev/null +++ b/docs/docs/models/emoji.md @@ -0,0 +1,232 @@ +--- +sidebar_position: 1 +title: Emoji +--- + +# Emoji Model + +The `Emoji` model represents an individual emoji, which can be a Unicode emoji (😊), legacy text emoticon (:-)), or a custom image. + +## Model Properties + +### Table Name +- `emojis` + +### Fillable Fields +- `title` - Human-readable name (e.g., "Smile", "Thumbs Up") +- `emoji_text` - Unicode character or text emoticon (nullable) +- `emoji_shortcode` - Primary shortcode (e.g., `:smile:`) - unique +- `image_url` - Path to custom image (nullable) +- `sprite_mode` - Boolean flag for CSS sprite usage +- `sprite_params` - JSON field for sprite parameters +- `emoji_category_id` - Foreign key to category (nullable) +- `display_order` - Integer for ordering within category + +### Casts +- `sprite_mode` → boolean +- `sprite_params` → array + +### Timestamps +- `created_at` +- `updated_at` + +## Traits + +### Searchable (Laravel Scout) +The model uses Laravel Scout for full-text search functionality. + +```php +public function toSearchableArray() +{ + return [ + 'id' => $this->id, + 'emoji_shortcode' => $this->emoji_shortcode, + 'emoji_text' => $this->emoji_text, + ]; +} +``` + +## Relationships + +### Belongs To: Category + +```php +public function category(): BelongsTo +{ + return $this->belongsTo(EmojiCategory::class, 'emoji_category_id'); +} +``` + +### Has Many: Aliases + +```php +public function aliases(): HasMany +{ + return $this->hasMany(EmojiAlias::class); +} +``` + +## Usage Examples + +### Creating Emoji + +```php +use App\Models\Emoji; +use App\Models\EmojiCategory; + +// Create a Unicode emoji +$emoji = Emoji::create([ + 'title' => 'Grinning Face', + 'emoji_text' => '😀', + 'emoji_shortcode' => ':grinning:', + 'emoji_category_id' => $category->id, + 'display_order' => 1 +]); + +// Create a custom image emoji +$customEmoji = Emoji::create([ + 'title' => 'Party Parrot', + 'emoji_shortcode' => ':partyparrot:', + 'image_url' => '/emojis/custom/partyparrot.gif', + 'emoji_category_id' => $category->id, + 'display_order' => 2 +]); + +// Create a sprite-based emoji +$spriteEmoji = Emoji::create([ + 'title' => 'Custom Sprite', + 'emoji_shortcode' => ':custom:', + 'sprite_mode' => true, + 'sprite_params' => [ + 'x' => 32, + 'y' => 64, + 'width' => 32, + 'height' => 32, + 'sheet' => 'emoji-sheet-1.png' + ], + 'emoji_category_id' => $category->id, + 'display_order' => 3 +]); +``` + +### Retrieving Emoji + +```php +// Find by shortcode (remember it's unique) +$emoji = Emoji::where('emoji_shortcode', ':smile:')->first(); + +// Get all emojis with their aliases +$emojis = Emoji::with('aliases')->get(); + +// Get emojis in a specific category +$categoryEmojis = Emoji::where('emoji_category_id', $categoryId) + ->orderBy('display_order') + ->get(); + +// Get only Unicode emojis +$unicodeEmojis = Emoji::whereNotNull('emoji_text') + ->whereNull('image_url') + ->get(); + +// Get only custom image emojis +$customEmojis = Emoji::whereNull('emoji_text') + ->whereNotNull('image_url') + ->get(); +``` + +### Working with Aliases + +```php +$emoji = Emoji::find(1); + +// Access aliases +foreach ($emoji->aliases as $alias) { + echo $alias->alias; // e.g., ":happy:", ":joy:" +} + +// Add a new alias +$emoji->aliases()->create([ + 'alias' => ':new-alias:' +]); +``` + +### Search Integration + +```php +// Search for emojis using Scout +$results = Emoji::search(':smile')->get(); +$results = Emoji::search('😊')->get(); + +// Update search index +$emoji->searchable(); // Add to index +$emoji->unsearchable(); // Remove from index +``` + +## Database Schema + +```sql +CREATE TABLE emojis ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + emoji_text VARCHAR(255) NULL, + emoji_shortcode VARCHAR(255) NOT NULL UNIQUE, + image_url VARCHAR(255) NULL, + sprite_mode BOOLEAN DEFAULT FALSE, + sprite_params JSON NULL, + emoji_category_id BIGINT UNSIGNED NULL, + display_order INTEGER NOT NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_display_order (display_order), + INDEX idx_emoji_category_id (emoji_category_id), + FOREIGN KEY (emoji_category_id) REFERENCES emoji_categories(id) ON DELETE SET NULL +); +``` + +## Factory + +The model includes a factory with useful states: + +```php +use App\Models\Emoji; + +// Create a random emoji +$emoji = Emoji::factory()->create(); + +// Create a custom image emoji +$customEmoji = Emoji::factory()->customImage()->create(); + +// Create a sprite-based emoji +$spriteEmoji = Emoji::factory()->withSprite()->create(); + +// Create multiple emojis with a category +$emojis = Emoji::factory() + ->count(10) + ->for(EmojiCategory::factory()) + ->create(); +``` + +## Performance Considerations + +- The `emoji_shortcode` field has a unique index for fast lookups during text replacement +- The `display_order` field is indexed for efficient ordering +- The `emoji_category_id` field is indexed for category-based queries +- Scout integration provides full-text search capabilities + +## Future Enhancements + +The following helper methods should be implemented in service classes: + +1. **Text Replacement Helper** - Get all possible text triggers (shortcode + aliases) +2. **Render Helper** - Determine render method (unicode, image, or sprite) +3. **Eager Loading Scope** - Efficiently load emojis with aliases for replacement engine +4. **Type Check Helper** - Determine if emoji is unicode, custom image, or sprite + +Example service method signatures: +```php +// EmojiService +public function getAllTriggers(Emoji $emoji): array; +public function getRenderData(Emoji $emoji): array; +public function loadForReplacement(): Collection; +public function isCustomEmoji(Emoji $emoji): bool; +``` diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index b97067b6d..f4dee7578 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -81,9 +81,14 @@ const config: Config = { }, items: [ {to: '/blog/welcome', label: 'Blog', position: 'left'}, + { + to: '/docs/api/overview', + label: 'API', + position: 'left', + }, { type: 'docSidebar', - sidebarId: 'tutorialSidebar', + sidebarId: 'docsSidebar', position: 'left', label: 'Documentation', }, diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 289713975..eeaeb996e 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -14,7 +14,7 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; */ const sidebars: SidebarsConfig = { // By default, Docusaurus generates a sidebar from the docs folder structure - tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], + docsSidebar: [{type: 'autogenerated', dirName: '.'}], // But you can create a sidebar manually /* diff --git a/docs/static/img/logos.svg b/docs/static/img/logo.svg similarity index 100% rename from docs/static/img/logos.svg rename to docs/static/img/logo.svg diff --git a/routes/api.php b/routes/api.php index ccc387f23..618a824b0 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,8 +1,25 @@ user(); })->middleware('auth:sanctum'); + +// Emoji API Routes +Route::prefix('emoji')->group(function () { + // Emoji - search routes must come before resource routes + Route::get('emojis/search', [EmojiController::class, 'search'])->name('emojis.search'); + Route::apiResource('emojis', EmojiController::class); + + // Emoji Aliases - search routes must come before resource routes + Route::get('aliases/search', [EmojiAliasController::class, 'search'])->name('aliases.search'); + Route::apiResource('aliases', EmojiAliasController::class); + + // Emoji Categories + Route::apiResource('categories', EmojiCategoryController::class); +});