feat(emoji): implement comprehensive emoji management system (#2025)

* feat(emoji): implement comprehensive emoji management system

Add complete emoji management functionality with API and web interfaces

Features:
* Emoji management with categories, aliases, and search capabilities
* Full REST API with CRUD operations for emojis, categories, and aliases
* Laravel Scout integration for emoji search functionality
* Sprite support for emoji display optimization
* Hierarchical emoji categorization system
* Multiple alias support per emoji for enhanced discoverability

Technical Implementation:
* Models: Emoji, EmojiCategory, EmojiAlias with proper relationships
* Controllers: API and web controllers with resource routing
* Validation: Form request classes for data integrity
* Authorization: Policy classes for access control
* Resources: API resource classes for consistent JSON responses
* Database: Migrations with foreign key constraints and indexes
* Testing: Model factories and seeders for development/testing

API Features:
* RESTful endpoints: /api/emoji/{emojis,categories,aliases}
* Search endpoints with Laravel Scout integration
* Pagination and filtering capabilities
* Relationship eager loading via query parameters
* Authentication via Laravel Sanctum

Documentation:
* Complete API documentation with examples and response formats
* Model relationship documentation
* Development setup and contribution guidelines

Database Schema:
* emoji_categories: id, title, description, display_order
* emojis: id, title, emoji_text, emoji_shortcode, image_url, sprite_mode, sprite_params, category_id, display_order
* emoji_aliases: id, alias, emoji_id

Files Added:
* 8 controllers (API + web interfaces)
* 6 form request validation classes
* 3 API resource classes
* 3 model classes with relationships
* 3 authorization policy classes
* 3 model factories for testing
* 3 database migrations
* 3 database seeders
* 9 documentation files
* Updated API routes

* fix(emoji): correct comment casing for Unicode emoji in EmojiFactory

Updated the comment in the EmojiFactory to use consistent casing for "Unicode" in the emoji definition method. This change improves code clarity and maintains consistency in documentation style.

* refactor(emoji): rename variables for clarity in EmojiAlias and EmojiCategory controllers

Updated variable names in the EmojiAliasController and EmojiCategoryController to improve code readability. Changed instances of `$emojiAlias` and `$emojiCategory` to `$alias` and `$category`, respectively, in the show, update, and destroy methods. Additionally, updated the UpdateEmojiAliasRequest to reflect the new variable naming convention. This enhances consistency and clarity across the codebase.

* feat(emoji): implement search request validation for emojis and aliases

Added new form request classes, SearchEmojiRequest and SearchEmojiAliasRequest, to handle validation for search queries in the Emoji and EmojiAlias controllers. Updated the search methods in both controllers to utilize these new request classes, improving code organization and validation consistency. Additionally, refactored the loading of relationships in the EmojiController for better readability.

* feat(emoji): add request validation for emoji and alias indexing

Introduced new form request classes, IndexEmojiRequest and IndexEmojiAliasRequest, to handle validation for indexing requests in the Emoji and EmojiAlias controllers. Updated the index methods in both controllers to utilize these new request classes, enhancing validation and code organization. This change improves the overall structure and maintainability of the emoji management system.
This commit is contained in:
Yury Pikhtarev 2025-07-02 17:49:45 +04:00 committed by GitHub
commit df135a2b41
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 4600 additions and 7 deletions

View file

@ -0,0 +1,111 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Emoji\IndexEmojiAliasRequest;
use App\Http\Requests\Emoji\SearchEmojiAliasRequest;
use App\Http\Requests\Emoji\StoreEmojiAliasRequest;
use App\Http\Requests\Emoji\UpdateEmojiAliasRequest;
use App\Http\Resources\EmojiAliasResource;
use App\Models\EmojiAlias;
use Illuminate\Http\Request;
class EmojiAliasController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(IndexEmojiAliasRequest $request)
{
$aliases = EmojiAlias::query()
->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 $alias, Request $request)
{
$alias->load(['emoji' => function ($query) {
$query->with('category');
}]);
return new EmojiAliasResource($alias);
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateEmojiAliasRequest $request, EmojiAlias $alias)
{
$alias->update($request->validated());
if ($request->get('with_emoji')) {
$alias->load(['emoji' => function ($query) {
$query->with('category');
}]);
}
return new EmojiAliasResource($alias);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(EmojiAlias $alias)
{
$alias->delete();
return response()->json(null, 204);
}
/**
* Search aliases using Laravel Scout.
*/
public function search(SearchEmojiAliasRequest $request)
{
$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);
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Emoji\StoreEmojiCategoryRequest;
use App\Http\Requests\Emoji\UpdateEmojiCategoryRequest;
use App\Http\Resources\EmojiCategoryResource;
use App\Models\EmojiCategory;
use Illuminate\Http\Request;
class EmojiCategoryController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$categories = EmojiCategory::query()
->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 $category, Request $request)
{
$category->load([
'emojis' => function ($query) use ($request) {
$query->orderBy('display_order');
if ($request->get('with_aliases')) {
$query->with('aliases');
}
},
]);
return new EmojiCategoryResource($category);
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateEmojiCategoryRequest $request, EmojiCategory $category)
{
$category->update($request->validated());
return new EmojiCategoryResource($category);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(EmojiCategory $category)
{
$category->delete();
return response()->json(null, 204);
}
}

View file

@ -0,0 +1,114 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Emoji\IndexEmojiRequest;
use App\Http\Requests\Emoji\SearchEmojiRequest;
use App\Http\Requests\Emoji\StoreEmojiRequest;
use App\Http\Requests\Emoji\UpdateEmojiRequest;
use App\Http\Resources\EmojiResource;
use App\Models\Emoji;
use Illuminate\Http\Request;
class EmojiController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(IndexEmojiRequest $request)
{
$emojis = Emoji::query()
->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(SearchEmojiRequest $request)
{
$emojis = Emoji::search($request->get('q'))
->take($request->get('limit', 20))
->get();
// Load relationships if requested
$emojis->load(array_filter([
$request->get('with_category') ? 'category' : null,
$request->get('with_aliases') ? 'aliases' : null,
]));
return EmojiResource::collection($emojis);
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers\Emoji;
use App\Http\Controllers\Controller;
use App\Http\Requests\Emoji\StoreEmojiAliasRequest;
use App\Http\Requests\Emoji\UpdateEmojiAliasRequest;
use App\Models\EmojiAlias;
class EmojiAliasController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreEmojiAliasRequest $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(EmojiAlias $emojiAlias)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(EmojiAlias $emojiAlias)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateEmojiAliasRequest $request, EmojiAlias $emojiAlias)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(EmojiAlias $emojiAlias)
{
//
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers\Emoji;
use App\Http\Controllers\Controller;
use App\Http\Requests\Emoji\StoreEmojiCategoryRequest;
use App\Http\Requests\Emoji\UpdateEmojiCategoryRequest;
use App\Models\EmojiCategory;
class EmojiCategoryController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreEmojiCategoryRequest $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(EmojiCategory $emojiCategory)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(EmojiCategory $emojiCategory)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateEmojiCategoryRequest $request, EmojiCategory $emojiCategory)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(EmojiCategory $emojiCategory)
{
//
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers\Emoji;
use App\Http\Controllers\Controller;
use App\Http\Requests\Emoji\StoreEmojiRequest;
use App\Http\Requests\Emoji\UpdateEmojiRequest;
use App\Models\Emoji;
class EmojiController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreEmojiRequest $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(Emoji $emoji)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Emoji $emoji)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateEmojiRequest $request, Emoji $emoji)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Emoji $emoji)
{
//
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace App\Http\Requests\Emoji;
use Illuminate\Foundation\Http\FormRequest;
class IndexEmojiAliasRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'emoji_id' => 'sometimes|integer|exists:emojis,id',
'search' => 'sometimes|string|max:255',
'per_page' => 'sometimes|integer|min:1|max:100',
'with_emoji' => 'sometimes|boolean',
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
// Convert string boolean values to actual booleans
if ($this->has('with_emoji')) {
$this->merge([
'with_emoji' => filter_var($this->with_emoji, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE),
]);
}
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace App\Http\Requests\Emoji;
use Illuminate\Foundation\Http\FormRequest;
class IndexEmojiRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'category_id' => 'sometimes|integer|exists:emoji_categories,id',
'search' => 'sometimes|string|max:255',
'per_page' => 'sometimes|integer|min:1|max:100',
'with_category' => 'sometimes|boolean',
'with_aliases' => 'sometimes|boolean',
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
// Convert string boolean values to actual booleans
$booleanFields = ['with_category', 'with_aliases'];
foreach ($booleanFields as $field) {
if ($this->has($field)) {
$this->merge([
$field => filter_var($this->get($field), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE),
]);
}
}
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Emoji;
use Illuminate\Foundation\Http\FormRequest;
class SearchEmojiAliasRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'q' => 'required|string|min:1',
'limit' => 'integer|min:1|max:100',
];
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Emoji;
use Illuminate\Foundation\Http\FormRequest;
class SearchEmojiRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'q' => 'required|string|min:1',
'limit' => 'integer|min:1|max:100',
];
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace App\Http\Requests\Emoji;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreEmojiAliasRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // TODO: Add proper authorization logic
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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.',
];
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Emoji;
use Illuminate\Foundation\Http\FormRequest;
class StoreEmojiCategoryRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // TODO: Add proper authorization logic
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'display_order' => 'required|integer|min:0',
];
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace App\Http\Requests\Emoji;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreEmojiRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // TODO: Add proper authorization logic
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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.',
];
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Http\Requests\Emoji;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateEmojiAliasRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // TODO: Add proper authorization logic
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$aliasId = $this->route('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.',
];
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Emoji;
use Illuminate\Foundation\Http\FormRequest;
class UpdateEmojiCategoryRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // TODO: Add proper authorization logic
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'title' => 'sometimes|string|max:255',
'display_order' => 'sometimes|integer|min:0',
];
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace App\Http\Requests\Emoji;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateEmojiRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // TODO: Add proper authorization logic
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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.',
];
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class EmojiAliasResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
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,
];
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class EmojiCategoryResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
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,
];
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class EmojiResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
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,
];
}
}

83
app/Models/Emoji.php Normal file
View file

@ -0,0 +1,83 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
class Emoji extends Model
{
/** @use HasFactory<\Database\Factories\EmojiFactory> */
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<int, string>
*/
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<string, string>
*/
protected $casts = [
'sprite_mode' => 'boolean',
'sprite_params' => 'array',
];
/**
* Get the category that owns the emoji.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<EmojiCategory>
*/
public function category()
{
return $this->belongsTo(EmojiCategory::class, 'emoji_category_id');
}
/**
* Get the aliases for the emoji.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<EmojiAlias>
*/
public function aliases()
{
return $this->hasMany(EmojiAlias::class);
}
/**
* Get the indexable data array for the model.
*
* @return array<string, mixed>
*/
public function toSearchableArray(): array
{
return [
'id' => $this->id,
'emoji_shortcode' => $this->emoji_shortcode,
'emoji_text' => $this->emoji_text,
];
}
}

47
app/Models/EmojiAlias.php Normal file
View file

@ -0,0 +1,47 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
class EmojiAlias extends Model
{
/** @use HasFactory<\Database\Factories\EmojiAliasFactory> */
use HasFactory, Searchable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'emoji_id',
'alias',
];
/**
* Get the emoji that owns the alias.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<Emoji>
*/
public function emoji()
{
return $this->belongsTo(Emoji::class);
}
/**
* Get the indexable data array for the model.
*
* @return array<string, mixed>
*/
public function toSearchableArray(): array
{
return [
'id' => $this->id,
'alias' => $this->alias,
'emoji_id' => $this->emoji_id,
];
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class EmojiCategory extends Model
{
/** @use HasFactory<\Database\Factories\EmojiCategoryFactory> */
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'title',
'display_order',
];
/**
* Get the emojis for the category.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany<Emoji>
*/
public function emojis()
{
return $this->hasMany(Emoji::class);
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace App\Policies;
use App\Models\EmojiAlias;
use App\Models\User;
class EmojiAliasPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return true;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, EmojiAlias $emojiAlias): bool
{
return true;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, EmojiAlias $emojiAlias): bool
{
return false;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, EmojiAlias $emojiAlias): bool
{
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, EmojiAlias $emojiAlias): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, EmojiAlias $emojiAlias): bool
{
return false;
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace App\Policies;
use App\Models\EmojiCategory;
use App\Models\User;
class EmojiCategoryPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return true;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, EmojiCategory $emojiCategory): bool
{
return true;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, EmojiCategory $emojiCategory): bool
{
return false;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, EmojiCategory $emojiCategory): bool
{
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, EmojiCategory $emojiCategory): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, EmojiCategory $emojiCategory): bool
{
return false;
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace App\Policies;
use App\Models\Emoji;
use App\Models\User;
class EmojiPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return true;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Emoji $emoji): bool
{
return true;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Emoji $emoji): bool
{
return false;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Emoji $emoji): bool
{
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Emoji $emoji): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Emoji $emoji): bool
{
return false;
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace Database\Factories;
use App\Models\Emoji;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\EmojiAlias>
*/
class EmojiAliasFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'emoji_id' => Emoji::factory(),
'alias' => ':' . $this->faker->unique()->word() . ':',
];
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\EmojiCategory>
*/
class EmojiCategoryFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
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),
];
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Database\Factories;
use App\Models\EmojiCategory;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Emoji>
*/
class EmojiFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
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',
]);
}
}

View file

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('emoji_categories', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->integer('display_order')->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('emoji_categories');
}
};

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('emojis', function (Blueprint $table) {
$table->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');
}
};

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('emoji_aliases', function (Blueprint $table) {
$table->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');
}
};

View file

@ -0,0 +1,16 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class EmojiAliasSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class EmojiCategorySeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
//
}
}

View file

@ -0,0 +1,124 @@
<?php
namespace Database\Seeders;
use App\Models\Emoji;
use App\Models\EmojiAlias;
use App\Models\EmojiCategory;
use Illuminate\Database\Seeder;
class EmojiSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// Create categories
$categories = [
'Smileys' => ['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
foreach ($emojis as $categoryData) {
$category = $categoryModels[$categoryData['category']];
$displayOrder = 0;
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,
]);
}
}
}
}
}

View file

@ -0,0 +1,8 @@
{
"label": "API",
"position": 8,
"link": {
"type": "generated-index",
"description": "Complete API documentation for all endpoints"
}
}

View file

@ -0,0 +1,8 @@
{
"label": "Emoji",
"position": 2,
"link": {
"type": "generated-index",
"description": "API endpoints for managing emojis, categories, and aliases"
}
}

View file

@ -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

View file

@ -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

View file

@ -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

163
docs/docs/api/overview.md Normal file
View file

@ -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.

View file

@ -1,5 +1,5 @@
---
sidebar_position: 2
sidebar_position: 9
title: Contributing
---

View file

@ -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"]
}
}
}

View file

@ -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."
}
}
}

View file

@ -0,0 +1,8 @@
{
"label": "Models",
"position": 5,
"link": {
"type": "generated-index",
"description": "Eloquent Models documentation for the application"
}
}

View file

@ -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

View file

@ -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)

232
docs/docs/models/emoji.md Normal file
View file

@ -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;
```

View file

@ -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',
},

View file

@ -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
/*

View file

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before After
Before After

View file

@ -1,8 +1,25 @@
<?php
use App\Http\Controllers\Api\EmojiAliasController;
use App\Http\Controllers\Api\EmojiCategoryController;
use App\Http\Controllers\Api\EmojiController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/user', function (Request $request) {
return $request->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);
});

View file

@ -0,0 +1,347 @@
<?php
use App\Models\Emoji;
use App\Models\EmojiAlias;
use App\Models\EmojiCategory;
describe('Emoji Alias API Endpoints', function () {
test('can list aliases', function () {
$category = EmojiCategory::factory()->create();
$emoji = Emoji::factory()->create(['emoji_category_id' => $category->id]);
EmojiAlias::factory()->count(3)->create(['emoji_id' => $emoji->id]);
$response = $this->getJson('/api/emoji/aliases');
$response->assertOk()
->assertJsonStructure([
'data' => [
'*' => [
'id',
'alias',
'created_at',
'updated_at',
],
],
'links',
'meta',
])
->assertJsonCount(3, 'data');
});
test('can filter aliases by emoji', function () {
$category = EmojiCategory::factory()->create();
$emoji1 = Emoji::factory()->create(['emoji_category_id' => $category->id]);
$emoji2 = Emoji::factory()->create(['emoji_category_id' => $category->id]);
EmojiAlias::factory()->count(2)->create(['emoji_id' => $emoji1->id]);
EmojiAlias::factory()->count(3)->create(['emoji_id' => $emoji2->id]);
$response = $this->getJson("/api/emoji/aliases?emoji_id={$emoji1->id}");
$response->assertOk()
->assertJsonCount(2, 'data');
});
test('can search aliases', function () {
$category = EmojiCategory::factory()->create();
$emoji = Emoji::factory()->create(['emoji_category_id' => $category->id]);
EmojiAlias::factory()->create([
'emoji_id' => $emoji->id,
'alias' => ':happy:',
]);
EmojiAlias::factory()->create([
'emoji_id' => $emoji->id,
'alias' => ':sad:',
]);
$response = $this->getJson('/api/emoji/aliases?search=happy');
$response->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.alias', ':happy:');
});
test('can include emoji and category with aliases', function () {
$category = EmojiCategory::factory()->create(['title' => 'Test Category']);
$emoji = Emoji::factory()->create([
'emoji_category_id' => $category->id,
'title' => 'Test Emoji',
]);
EmojiAlias::factory()->create(['emoji_id' => $emoji->id]);
$response = $this->getJson('/api/emoji/aliases?with_emoji=1');
$response->assertOk()
->assertJsonPath('data.0.emoji.title', 'Test Emoji')
->assertJsonPath('data.0.emoji.category.title', 'Test Category');
});
test('can get specific alias', function () {
$category = EmojiCategory::factory()->create();
$emoji = Emoji::factory()->create(['emoji_category_id' => $category->id]);
$alias = EmojiAlias::factory()->create([
'emoji_id' => $emoji->id,
'alias' => ':test_alias:',
]);
$response = $this->getJson("/api/emoji/aliases/{$alias->id}");
$response->assertOk()
->assertJsonPath('data.alias', ':test_alias:')
->assertJsonStructure([
'data' => [
'id',
'alias',
'emoji',
'created_at',
'updated_at',
],
]);
});
test('returns 404 for non-existent alias', function () {
$response = $this->getJson('/api/emoji/aliases/999');
$response->assertNotFound();
});
test('can create alias', function () {
$category = EmojiCategory::factory()->create();
$emoji = Emoji::factory()->create(['emoji_category_id' => $category->id]);
$data = [
'emoji_id' => $emoji->id,
'alias' => ':new_alias:',
];
$response = $this->postJson('/api/emoji/aliases', $data);
$response->assertCreated()
->assertJsonPath('data.alias', ':new_alias:')
->assertJsonPath('data.emoji', null); // Not loaded by default
$this->assertDatabaseHas('emoji_aliases', [
'emoji_id' => $emoji->id,
'alias' => ':new_alias:',
]);
});
test('validates required fields when creating alias', function () {
$response = $this->postJson('/api/emoji/aliases', []);
$response->assertUnprocessable()
->assertJsonValidationErrors(['emoji_id', 'alias']);
});
test('validates alias format', function () {
$category = EmojiCategory::factory()->create();
$emoji = Emoji::factory()->create(['emoji_category_id' => $category->id]);
$response = $this->postJson('/api/emoji/aliases', [
'emoji_id' => $emoji->id,
'alias' => 'invalid-format',
]);
$response->assertUnprocessable()
->assertJsonValidationErrors(['alias']);
});
test('validates unique alias', function () {
$category = EmojiCategory::factory()->create();
$emoji = Emoji::factory()->create(['emoji_category_id' => $category->id]);
EmojiAlias::factory()->create([
'emoji_id' => $emoji->id,
'alias' => ':existing:',
]);
$response = $this->postJson('/api/emoji/aliases', [
'emoji_id' => $emoji->id,
'alias' => ':existing:',
]);
$response->assertUnprocessable()
->assertJsonValidationErrors(['alias']);
});
test('validates emoji exists', function () {
$response = $this->postJson('/api/emoji/aliases', [
'emoji_id' => 999,
'alias' => ':valid_alias:',
]);
$response->assertUnprocessable()
->assertJsonValidationErrors(['emoji_id']);
});
test('prevents alias from conflicting with emoji shortcode', function () {
$category = EmojiCategory::factory()->create();
$emoji1 = Emoji::factory()->create([
'emoji_category_id' => $category->id,
'emoji_shortcode' => ':existing_emoji:',
]);
$emoji2 = Emoji::factory()->create(['emoji_category_id' => $category->id]);
$response = $this->postJson('/api/emoji/aliases', [
'emoji_id' => $emoji2->id,
'alias' => ':existing_emoji:',
]);
$response->assertUnprocessable()
->assertJsonValidationErrors(['alias']);
});
test('can update alias', function () {
$category = EmojiCategory::factory()->create();
$emoji1 = Emoji::factory()->create(['emoji_category_id' => $category->id]);
$emoji2 = Emoji::factory()->create(['emoji_category_id' => $category->id]);
$alias = EmojiAlias::factory()->create(['emoji_id' => $emoji1->id]);
$data = [
'emoji_id' => $emoji2->id,
'alias' => ':updated_alias:',
];
$response = $this->patchJson("/api/emoji/aliases/{$alias->id}", $data);
$response->assertOk()
->assertJsonPath('data.alias', ':updated_alias:');
$this->assertDatabaseHas('emoji_aliases', [
'id' => $alias->id,
'emoji_id' => $emoji2->id,
'alias' => ':updated_alias:',
]);
});
test('can partially update alias', function () {
$category = EmojiCategory::factory()->create();
$emoji = Emoji::factory()->create(['emoji_category_id' => $category->id]);
$alias = EmojiAlias::factory()->create([
'emoji_id' => $emoji->id,
'alias' => ':original:',
]);
$response = $this->patchJson("/api/emoji/aliases/{$alias->id}", [
'alias' => ':updated:',
]);
$response->assertOk()
->assertJsonPath('data.alias', ':updated:');
// emoji_id should remain unchanged
$this->assertDatabaseHas('emoji_aliases', [
'id' => $alias->id,
'emoji_id' => $emoji->id,
'alias' => ':updated:',
]);
});
test('can delete alias', function () {
$category = EmojiCategory::factory()->create();
$emoji = Emoji::factory()->create(['emoji_category_id' => $category->id]);
$alias = EmojiAlias::factory()->create(['emoji_id' => $emoji->id]);
$response = $this->deleteJson("/api/emoji/aliases/{$alias->id}");
$response->assertNoContent();
$this->assertDatabaseMissing('emoji_aliases', ['id' => $alias->id]);
// Emoji should still exist
$this->assertDatabaseHas('emojis', ['id' => $emoji->id]);
});
test('supports pagination', function () {
$category = EmojiCategory::factory()->create();
$emoji = Emoji::factory()->create(['emoji_category_id' => $category->id]);
EmojiAlias::factory()->count(60)->create(['emoji_id' => $emoji->id]);
$response = $this->getJson('/api/emoji/aliases?per_page=20&page=2');
$response->assertOk()
->assertJsonPath('meta.current_page', 2)
->assertJsonPath('meta.per_page', 20)
->assertJsonCount(20, 'data');
});
test('aliases are ordered alphabetically', function () {
$category = EmojiCategory::factory()->create();
$emoji = Emoji::factory()->create(['emoji_category_id' => $category->id]);
EmojiAlias::factory()->create(['emoji_id' => $emoji->id, 'alias' => ':zebra:']);
EmojiAlias::factory()->create(['emoji_id' => $emoji->id, 'alias' => ':apple:']);
EmojiAlias::factory()->create(['emoji_id' => $emoji->id, 'alias' => ':banana:']);
$response = $this->getJson('/api/emoji/aliases');
$response->assertOk()
->assertJsonPath('data.0.alias', ':apple:')
->assertJsonPath('data.1.alias', ':banana:')
->assertJsonPath('data.2.alias', ':zebra:');
});
});
describe('Emoji Alias Search API', function () {
test('can search aliases using scout', function () {
$category = EmojiCategory::factory()->create();
$emoji = Emoji::factory()->create(['emoji_category_id' => $category->id]);
EmojiAlias::factory()->create([
'emoji_id' => $emoji->id,
'alias' => ':happy:',
]);
EmojiAlias::factory()->create([
'emoji_id' => $emoji->id,
'alias' => ':sad:',
]);
$response = $this->getJson('/api/emoji/aliases/search?q=happy');
$response->assertOk()
->assertJsonStructure([
'data' => [
'*' => [
'id',
'alias',
'emoji',
],
],
]);
});
test('validates search query parameter', function () {
$response = $this->getJson('/api/emoji/aliases/search');
$response->assertUnprocessable()
->assertJsonValidationErrors(['q']);
});
test('validates search limit parameter', function () {
$response = $this->getJson('/api/emoji/aliases/search?q=test&limit=150');
$response->assertUnprocessable()
->assertJsonValidationErrors(['limit']);
});
test('can include emoji data in search results', function () {
$category = EmojiCategory::factory()->create();
$emoji = Emoji::factory()->create(['emoji_category_id' => $category->id]);
$alias = EmojiAlias::factory()->create(['emoji_id' => $emoji->id]);
$response = $this->getJson('/api/emoji/aliases/search?q=test&with_emoji=1');
$response->assertOk()
->assertJsonStructure([
'data' => [
'*' => [
'emoji' => [
'id',
'title',
'emoji_shortcode',
],
],
],
]);
});
});

View file

@ -0,0 +1,232 @@
<?php
use App\Models\Emoji;
use App\Models\EmojiCategory;
describe('Emoji Category API Endpoints', function () {
test('can list categories', function () {
EmojiCategory::factory()->count(3)->create();
$response = $this->getJson('/api/emoji/categories');
$response->assertOk()
->assertJsonStructure([
'data' => [
'*' => [
'id',
'title',
'display_order',
'created_at',
'updated_at',
],
],
])
->assertJsonCount(3, 'data');
});
test('can list categories with emoji counts', function () {
// Ensure we start with clean slate
EmojiCategory::query()->delete();
Emoji::query()->delete();
$category1 = EmojiCategory::factory()->create([
'title' => 'Category 1',
'display_order' => 1,
]);
$category2 = EmojiCategory::factory()->create([
'title' => 'Category 2',
'display_order' => 2,
]);
Emoji::factory()->count(5)->create(['emoji_category_id' => $category1->id]);
Emoji::factory()->count(3)->create(['emoji_category_id' => $category2->id]);
$response = $this->getJson('/api/emoji/categories?with_emojis=1');
$response->assertOk()
->assertJsonPath('data.0.emojis_count', 5)
->assertJsonPath('data.1.emojis_count', 3);
});
test('can list categories with full emoji data', function () {
$category = EmojiCategory::factory()->create();
Emoji::factory()->count(2)->create(['emoji_category_id' => $category->id]);
$response = $this->getJson('/api/emoji/categories?include_emojis=1');
$response->assertOk()
->assertJsonCount(2, 'data.0.emojis')
->assertJsonStructure([
'data' => [
'*' => [
'emojis' => [
'*' => [
'id',
'title',
'emoji_shortcode',
'emoji_text',
],
],
],
],
]);
});
test('can get specific category', function () {
$category = EmojiCategory::factory()->create([
'title' => 'Test Category',
'display_order' => 5,
]);
$response = $this->getJson("/api/emoji/categories/{$category->id}");
$response->assertOk()
->assertJsonPath('data.title', 'Test Category')
->assertJsonPath('data.display_order', 5)
->assertJsonStructure([
'data' => [
'id',
'title',
'display_order',
'emojis',
'created_at',
'updated_at',
],
]);
});
test('can get category with emoji aliases', function () {
$category = EmojiCategory::factory()->create();
$emoji = Emoji::factory()->create(['emoji_category_id' => $category->id]);
$response = $this->getJson("/api/emoji/categories/{$category->id}?with_aliases=1");
$response->assertOk()
->assertJsonStructure([
'data' => [
'emojis' => [
'*' => [
'aliases',
],
],
],
]);
});
test('returns 404 for non-existent category', function () {
$response = $this->getJson('/api/emoji/categories/999');
$response->assertNotFound();
});
test('can create category', function () {
$data = [
'title' => 'New Category',
'display_order' => 10,
];
$response = $this->postJson('/api/emoji/categories', $data);
$response->assertCreated()
->assertJsonPath('data.title', 'New Category')
->assertJsonPath('data.display_order', 10);
$this->assertDatabaseHas('emoji_categories', [
'title' => 'New Category',
'display_order' => 10,
]);
});
test('validates required fields when creating category', function () {
$response = $this->postJson('/api/emoji/categories', []);
$response->assertUnprocessable()
->assertJsonValidationErrors(['title', 'display_order']);
});
test('validates display order is non-negative', function () {
$response = $this->postJson('/api/emoji/categories', [
'title' => 'Test Category',
'display_order' => -1,
]);
$response->assertUnprocessable()
->assertJsonValidationErrors(['display_order']);
});
test('validates title length', function () {
$response = $this->postJson('/api/emoji/categories', [
'title' => str_repeat('a', 256),
'display_order' => 1,
]);
$response->assertUnprocessable()
->assertJsonValidationErrors(['title']);
});
test('can update category', function () {
$category = EmojiCategory::factory()->create();
$data = [
'title' => 'Updated Title',
'display_order' => 99,
];
$response = $this->patchJson("/api/emoji/categories/{$category->id}", $data);
$response->assertOk()
->assertJsonPath('data.title', 'Updated Title')
->assertJsonPath('data.display_order', 99);
$this->assertDatabaseHas('emoji_categories', [
'id' => $category->id,
'title' => 'Updated Title',
'display_order' => 99,
]);
});
test('can partially update category', function () {
$category = EmojiCategory::factory()->create([
'title' => 'Original Title',
'display_order' => 5,
]);
$response = $this->patchJson("/api/emoji/categories/{$category->id}", [
'title' => 'New Title',
]);
$response->assertOk()
->assertJsonPath('data.title', 'New Title')
->assertJsonPath('data.display_order', 5); // Should remain unchanged
});
test('can delete category', function () {
$category = EmojiCategory::factory()->create();
$emoji = Emoji::factory()->create(['emoji_category_id' => $category->id]);
$response = $this->deleteJson("/api/emoji/categories/{$category->id}");
$response->assertNoContent();
$this->assertDatabaseMissing('emoji_categories', ['id' => $category->id]);
// Emoji should still exist but with null category_id
$this->assertDatabaseHas('emojis', [
'id' => $emoji->id,
'emoji_category_id' => null,
]);
});
test('categories are ordered by display_order', function () {
EmojiCategory::factory()->create(['title' => 'Third', 'display_order' => 3]);
EmojiCategory::factory()->create(['title' => 'First', 'display_order' => 1]);
EmojiCategory::factory()->create(['title' => 'Second', 'display_order' => 2]);
$response = $this->getJson('/api/emoji/categories');
$response->assertOk()
->assertJsonPath('data.0.title', 'First')
->assertJsonPath('data.1.title', 'Second')
->assertJsonPath('data.2.title', 'Third');
});
});

View file

@ -0,0 +1,333 @@
<?php
use App\Models\Emoji;
use App\Models\EmojiAlias;
use App\Models\EmojiCategory;
describe('Emoji API Endpoints', function () {
test('can list emojis', function () {
$category = EmojiCategory::factory()->create();
$emojis = Emoji::factory()->count(3)->create(['emoji_category_id' => $category->id]);
$response = $this->getJson('/api/emoji/emojis');
$response->assertOk()
->assertJsonStructure([
'data' => [
'*' => [
'id',
'title',
'emoji_text',
'emoji_shortcode',
'image_url',
'sprite_mode',
'sprite_params',
'display_order',
'created_at',
'updated_at',
],
],
'links',
'meta',
])
->assertJsonCount(3, 'data');
});
test('can filter emojis by category', function () {
$category1 = EmojiCategory::factory()->create();
$category2 = EmojiCategory::factory()->create();
Emoji::factory()->count(2)->create(['emoji_category_id' => $category1->id]);
Emoji::factory()->count(3)->create(['emoji_category_id' => $category2->id]);
$response = $this->getJson("/api/emoji/emojis?category_id={$category1->id}");
$response->assertOk()
->assertJsonCount(2, 'data');
});
test('can search emojis', function () {
$category = EmojiCategory::factory()->create();
Emoji::factory()->create([
'title' => 'Happy Face',
'emoji_shortcode' => ':happy:',
'emoji_category_id' => $category->id,
]);
Emoji::factory()->create([
'title' => 'Sad Face',
'emoji_shortcode' => ':sad:',
'emoji_category_id' => $category->id,
]);
$response = $this->getJson('/api/emoji/emojis?search=happy');
$response->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.title', 'Happy Face');
});
test('can include category and aliases with emojis', function () {
$category = EmojiCategory::factory()->create(['title' => 'Test Category']);
$emoji = Emoji::factory()->create(['emoji_category_id' => $category->id]);
EmojiAlias::factory()->count(2)->create(['emoji_id' => $emoji->id]);
$response = $this->getJson('/api/emoji/emojis?with_category=1&with_aliases=1');
$response->assertOk()
->assertJsonPath('data.0.category.title', 'Test Category')
->assertJsonCount(2, 'data.0.aliases');
});
test('can get specific emoji', function () {
$category = EmojiCategory::factory()->create();
$emoji = Emoji::factory()->create([
'title' => 'Test Emoji',
'emoji_shortcode' => ':test:',
'emoji_category_id' => $category->id,
]);
$response = $this->getJson("/api/emoji/emojis/{$emoji->id}");
$response->assertOk()
->assertJsonPath('data.title', 'Test Emoji')
->assertJsonPath('data.emoji_shortcode', ':test:')
->assertJsonStructure([
'data' => [
'id',
'title',
'emoji_shortcode',
'category',
'aliases',
],
]);
});
test('returns 404 for non-existent emoji', function () {
$response = $this->getJson('/api/emoji/emojis/999');
$response->assertNotFound();
});
test('can create unicode emoji', function () {
$category = EmojiCategory::factory()->create();
$data = [
'title' => 'Happy Face',
'emoji_text' => '😊',
'emoji_shortcode' => ':happy_face:',
'emoji_category_id' => $category->id,
'display_order' => 1,
];
$response = $this->postJson('/api/emoji/emojis', $data);
$response->assertCreated()
->assertJsonPath('data.title', 'Happy Face')
->assertJsonPath('data.emoji_text', '😊')
->assertJsonPath('data.emoji_shortcode', ':happy_face:');
$this->assertDatabaseHas('emojis', [
'title' => 'Happy Face',
'emoji_text' => '😊',
'emoji_shortcode' => ':happy_face:',
]);
});
test('can create custom image emoji', function () {
$category = EmojiCategory::factory()->create();
$data = [
'title' => 'Custom Emoji',
'emoji_shortcode' => ':custom:',
'image_url' => '/emojis/custom/custom.png',
'emoji_category_id' => $category->id,
'display_order' => 1,
];
$response = $this->postJson('/api/emoji/emojis', $data);
$response->assertCreated()
->assertJsonPath('data.title', 'Custom Emoji')
->assertJsonPath('data.image_url', '/emojis/custom/custom.png')
->assertJsonPath('data.emoji_text', null);
});
test('can create sprite emoji', function () {
$category = EmojiCategory::factory()->create();
$data = [
'title' => 'Sprite Emoji',
'emoji_shortcode' => ':sprite:',
'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' => 1,
];
$response = $this->postJson('/api/emoji/emojis', $data);
$response->assertCreated()
->assertJsonPath('data.sprite_mode', true)
->assertJsonPath('data.sprite_params.x', 32)
->assertJsonPath('data.sprite_params.sheet', 'emoji-sheet-1.png');
});
test('validates required fields when creating emoji', function () {
$response = $this->postJson('/api/emoji/emojis', []);
$response->assertUnprocessable()
->assertJsonValidationErrors(['title', 'emoji_shortcode', 'display_order']);
});
test('validates emoji shortcode format', function () {
$category = EmojiCategory::factory()->create();
$response = $this->postJson('/api/emoji/emojis', [
'title' => 'Test',
'emoji_shortcode' => 'invalid-format',
'emoji_category_id' => $category->id,
'display_order' => 1,
]);
$response->assertUnprocessable()
->assertJsonValidationErrors(['emoji_shortcode']);
});
test('validates unique emoji shortcode', function () {
$category = EmojiCategory::factory()->create();
Emoji::factory()->create(['emoji_shortcode' => ':existing:']);
$response = $this->postJson('/api/emoji/emojis', [
'title' => 'Test',
'emoji_shortcode' => ':existing:',
'emoji_category_id' => $category->id,
'display_order' => 1,
]);
$response->assertUnprocessable()
->assertJsonValidationErrors(['emoji_shortcode']);
});
test('validates sprite params when sprite mode is enabled', function () {
$category = EmojiCategory::factory()->create();
$response = $this->postJson('/api/emoji/emojis', [
'title' => 'Test',
'emoji_shortcode' => ':test:',
'sprite_mode' => true,
'emoji_category_id' => $category->id,
'display_order' => 1,
]);
$response->assertUnprocessable()
->assertJsonValidationErrors([
'sprite_params.x',
'sprite_params.y',
'sprite_params.width',
'sprite_params.height',
'sprite_params.sheet',
]);
});
test('can update emoji', function () {
$category = EmojiCategory::factory()->create();
$emoji = Emoji::factory()->create(['emoji_category_id' => $category->id]);
$data = [
'title' => 'Updated Title',
'display_order' => 99,
];
$response = $this->patchJson("/api/emoji/emojis/{$emoji->id}", $data);
$response->assertOk()
->assertJsonPath('data.title', 'Updated Title')
->assertJsonPath('data.display_order', 99);
$this->assertDatabaseHas('emojis', [
'id' => $emoji->id,
'title' => 'Updated Title',
'display_order' => 99,
]);
});
test('can delete emoji', function () {
$category = EmojiCategory::factory()->create();
$emoji = Emoji::factory()->create(['emoji_category_id' => $category->id]);
$alias = EmojiAlias::factory()->create(['emoji_id' => $emoji->id]);
$response = $this->deleteJson("/api/emoji/emojis/{$emoji->id}");
$response->assertNoContent();
$this->assertDatabaseMissing('emojis', ['id' => $emoji->id]);
$this->assertDatabaseMissing('emoji_aliases', ['id' => $alias->id]);
});
test('supports pagination', function () {
$category = EmojiCategory::factory()->create();
Emoji::factory()->count(60)->create(['emoji_category_id' => $category->id]);
$response = $this->getJson('/api/emoji/emojis?per_page=20&page=2');
$response->assertOk()
->assertJsonPath('meta.current_page', 2)
->assertJsonPath('meta.per_page', 20)
->assertJsonCount(20, 'data');
});
});
describe('Emoji Search API', function () {
test('can search emojis using scout', function () {
$category = EmojiCategory::factory()->create();
// Create emojis with different content
Emoji::factory()->create([
'title' => 'Happy Face',
'emoji_shortcode' => ':happy:',
'emoji_text' => '😊',
'emoji_category_id' => $category->id,
]);
Emoji::factory()->create([
'title' => 'Sad Face',
'emoji_shortcode' => ':sad:',
'emoji_text' => '😢',
'emoji_category_id' => $category->id,
]);
$response = $this->getJson('/api/emoji/emojis/search?q=happy');
$response->assertOk()
->assertJsonStructure([
'data' => [
'*' => [
'id',
'title',
'emoji_shortcode',
'emoji_text',
],
],
]);
});
test('validates search query parameter', function () {
$response = $this->getJson('/api/emoji/emojis/search');
$response->assertUnprocessable()
->assertJsonValidationErrors(['q']);
});
test('validates search limit parameter', function () {
$response = $this->getJson('/api/emoji/emojis/search?q=test&limit=150');
$response->assertUnprocessable()
->assertJsonValidationErrors(['limit']);
});
});