Compare commits

...

57 Commits

Author SHA1 Message Date
parsa aghaei
35686d82c3 chore: remove Iranian mirror from CI workflow - use official Alpine repos
Some checks failed
Deploy Backend / deploy (push) Failing after 13s
2026-07-12 13:35:32 +03:30
parsa aghaei
ab0e9517ea chore: remove committed vendor directory from git
- Remove vendor/ from git tracking
- Uncomment /vendor in .gitignore to prevent future commits
- Dependencies will be installed via CI/CD with VPN + Nexus cache
2026-07-12 13:28:08 +03:30
DevOps
6366843e47 test: trigger backend CI/CD pipeline 2026-06-28 18:09:20 +00:00
DevOps
1a75055dd9 fix: add missing hasTranslation method to AboutMe model 2026-06-28 17:41:38 +00:00
DevOps
223a4f5640 fix: remove HasTranslations trait, use translations JSON in models + seeders 2026-06-28 17:37:08 +00:00
parsa aghaei
ebd4bd19ed content: update about and service seeders with professional senior engineer copy 2026-06-28 20:22:12 +03:30
DevOps
4c5f6cfe10 fix: update Service model for translations JSON column 2026-06-28 15:59:20 +00:00
DevOps
1b2e73bc0e cleanup: remove trigger, fix .dockerignore 2026-06-28 15:45:15 +00:00
DevOps
b659d267cb test: trigger CI/CD with offline vendor build 2026-06-28 13:46:04 +00:00
parsa aghaei
d86aef2bec fix: remove ENV HTTP_PROXY, offline install when vendor exists 2026-06-28 16:56:28 +03:30
parsa aghaei
6b81bf0afe fix: only set composer proxy when vendor absent, use offline install otherwise 2026-06-28 16:52:12 +03:30
parsa aghaei
59a8d1f82b chore: commit vendor for offline build 2026-06-28 16:47:17 +03:30
DevOps
7f44e5d33a chore: add vendor for offline builds 2026-06-28 12:47:12 +00:00
DevOps
0e1ac38cf1 final test - nexus mirrors 2026-06-28 11:37:54 +00:00
DevOps
4584645518 restore Nexus mirror, test CI/CD 2026-06-28 10:51:19 +00:00
DevOps
ff1fc2afd0 fix: remove Nexus mirror, use packagist.org directly via proxy 2026-06-28 10:30:23 +00:00
DevOps
e841c89444 retry ci - symfony/routing 400 transient 2026-06-28 10:27:41 +00:00
DevOps
11ebd658e4 trigger ci - test proxy 2026-06-28 10:25:12 +00:00
parsa aghaei
110d4e0ed3 trigger ci - test optimized build 2026-06-28 13:40:57 +03:30
parsa aghaei
f951e19ffb trigger ci backend - test proxy 2026-06-28 12:25:14 +03:30
parsa aghaei
b31b4d73ac upgrade: Laravel 13 + swagger-php v6 attributes 2026-06-28 12:01:57 +03:30
parsa aghaei
f7ad5f6844 trigger ci 2026-06-28 11:11:43 +03:30
parsa aghaei
7935eb7f62 trigger ci 2026-06-27 14:07:38 +03:30
parsa aghaei
91d5398702 fix: use arvancloud Alpine mirror (Liara returns 500), remove unused proxy 2026-06-27 11:38:22 +03:30
parsa aghaei
7d8c125a39 fix: revert Alpine mirror to Liara (CI containers cant reach Nexus) 2026-06-27 11:32:21 +03:30
parsa aghaei
fd5c10572d chore: trigger CI test 2026-06-27 11:22:56 +03:30
parsa aghaei
bfffd46f4e Switch Alpine mirror to Nexus proxy registry.parsaaghayi.ir 2026-06-27 11:00:46 +03:30
parsa aghaei
4d10cbf13d fix deploy: revert to Liara mirror + add http proxy 172.17.0.1:3001 2026-06-27 09:51:45 +03:30
parsa aghaei
8025dd9197 fix deploy: use default package manager repos instead of broken mirror 2026-06-27 09:41:17 +03:30
parsa aghaei
86de286e46 replace default Laravel welcome page with professional API landing page 2026-06-27 09:39:38 +03:30
parsa aghaei
e9601a91af test file added for testing cicd 2026-06-23 20:10:46 +03:30
parsa aghaei
e3e5033fec fix: add json translation columns migration, re-add /vendor to .gitignore 2026-06-23 20:00:35 +03:30
parsa aghaei
db0cf119ab fix: remove /vendor from .gitignore (vendor committed to git) 2026-06-23 19:40:24 +03:30
parsa aghaei
78a14646eb 6.1-6.2: fix seeders for spatie translatable, uncomment ProjectSeeder, order correctly 2026-06-23 19:23:44 +03:30
parsa aghaei
9ff5b93157 4.1-4.3: add PHPUnit feature tests for all endpoints + factories 2026-06-23 18:37:14 +03:30
parsa aghaei
afb202f333 3.1-3.6: install l5-swagger, add @OA annotations to all controllers 2026-06-23 18:34:20 +03:30
parsa aghaei
db7ce880a0 2.1-2.4: install spatie/laravel-translatable, migrate to JSON, update models & controllers 2026-06-23 18:31:51 +03:30
parsa aghaei
1dc72d8563 1.3: fix N+1 in getTranslation by using eager-loaded collection 2026-06-23 18:10:22 +03:30
parsa aghaei
28c7a317e5 1.2: restructure controllers to Api/ prefix and update routes 2026-06-23 18:09:15 +03:30
parsa aghaei
cbcccfcbf8 1.1: remove dead code - old models and controllers 2026-06-23 18:07:38 +03:30
parsa aghaei
4cfbb05baf 0.4: apply throttle:60,1 to public API routes 2026-06-23 18:06:52 +03:30
parsa aghaei
faea1759cf 0.2: restrict CORS allowed_origins to FRONTEND_URL 2026-06-23 18:05:27 +03:30
parsa aghaei
f3568ab277 0.1: replace Request with FormRequest in all controllers 2026-06-23 18:04:57 +03:30
DevOps
ffafcf447b test: full pipeline deploy 2026-06-23 12:28:00 +00:00
DevOps
8de63a61f3 ci: vendor injection for offline composer install 2026-06-23 12:10:52 +00:00
DevOps
728716c589 ci: trigger backend deploy with Liara composer mirror 2026-06-23 12:01:36 +00:00
devops
ea03954a84 fix deploy: use Liara Alpine mirror for apk 2026-06-23 11:26:52 +00:00
devops
2e769bfece fix deploy: use sh shell (alpine has no bash) 2026-06-23 11:20:40 +00:00
devops
109b580d1b fix deploy: add apk SSH install, use docker gateway IP 2026-06-23 11:18:54 +00:00
devops
2402bccc69 fix deploy: use docker gateway IP, remove apk (SSH pre-installed on Ubuntu) 2026-06-23 11:13:52 +00:00
parsa aghaei
5ed3e905c6 Add Gitea Actions deploy workflow 2026-06-23 12:16:52 +03:30
root
a65b7859be version e server e kharej 2025-04-16 10:06:15 +00:00
parsa aghaei
3450c22b20 service seeder updated 2024-11-11 12:02:13 +03:30
parsa aghaei
67fc35e6f4 service seeder updated 2024-11-11 11:48:04 +03:30
parsa aghaei
ac4bcbb5bf Merge branch 'develop' 2024-11-11 11:41:08 +03:30
d4e8a9441e merge with develop 2024-10-24 11:46:26 +03:30
6cc70158f4 Initial commit 2024-09-25 13:04:04 +03:30
113 changed files with 5441 additions and 2901 deletions

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
.git/
node_modules/
.env

View File

@ -0,0 +1,28 @@
name: Deploy Backend
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
defaults:
run:
shell: sh
steps:
- name: Install SSH client
run: |
V=$(cut -d'.' -f1-2 /etc/alpine-release)
echo "https://dl-cdn.alpinelinux.org/alpine/v$V/main" > /etc/apk/repositories
echo "https://dl-cdn.alpinelinux.org/alpine/v$V/community" >> /etc/apk/repositories
apk add --no-cache openssh-client
- name: Setup SSH key
env:
SSH_KEY: ${{ secrets.DEPLOY_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -p 2234 -H 172.17.0.1 >> ~/.ssh/known_hosts
- name: Deploy
run: ssh -p 2234 devops@172.17.0.1 'bash /opt/services/parsa/backend/deploy.sh'

21
Dockerfile Normal file
View File

@ -0,0 +1,21 @@
FROM serversideup/php:8.3-fpm-nginx
ARG HTTP_PROXY
ARG HTTPS_PROXY
COPY --chown=www-data:www-data . /var/www/html/
ENV COMPOSER_HOME=/tmp/composer
USER www-data
RUN if [ -d /var/www/html/vendor ]; then \
echo "vendor/ found — installing offline" && \
HTTP_PROXY="" HTTPS_PROXY="" http_proxy="" https_proxy="" \
COMPOSER_DISABLE_NETWORK=1 \
composer install --no-dev --optimize-autoloader --no-interaction; \
else \
echo "vendor/ not found — installing from proxy" && \
composer config -g repos.packagist composer https://registry.parsaaghayi.ir/repository/composer-proxy/ && \
composer install --no-dev --optimize-autoloader --no-interaction; \
fi
EXPOSE 8080

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 parsa aghayi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,39 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\AboutMe;
class AboutMeController extends Controller
{
public function index()
{
$aboutMe = AboutMe::with('skills')->first(); // بارگذاری مهارت‌ها به همراه اطلاعات AboutMe
return response()->json($aboutMe);
}
public function show($id)
{
$aboutMe = AboutMe::with('skills')->findOrFail($id);
return response()->json($aboutMe);
}
public function store(Request $request)
{
$aboutMe = AboutMe::create($request->all());
return response()->json($aboutMe, 201);
}
public function update(Request $request, $id)
{
$aboutMe = AboutMe::findOrFail($id);
$aboutMe->update($request->all());
return response()->json($aboutMe);
}
public function destroy($id)
{
AboutMe::destroy($id);
return response()->json(null, 204);
}
}

View File

@ -0,0 +1,170 @@
<?php
namespace App\Http\Controllers\Api\Blog;
use App\Http\Controllers\Controller;
use App\Http\Requests\Blog\StoreCategoryRequest;
use App\Http\Requests\Blog\UpdateCategoryRequest;
use App\Http\Resources\CategoryResource;
use App\Http\Resources\PostResource;
use App\Models\Blog\Category;
use Illuminate\Http\Request;
/**
* @OA\Tag(name="Categories", description="Blog categories")
*/
class CategoryController extends Controller
{
/**
* @OA\Get(
* path="/blog/categories",
* tags={"Categories"},
* summary="Paginated list of categories",
* @OA\Parameter(name="per_page", in="query", @OA\Schema(type="integer")),
* @OA\Parameter(name="filter", in="query", @OA\Schema(type="string")),
* @OA\Response(response=200, description="OK")
* )
*/
public function index(Request $request)
{
$filter = $request->input('filter');
$sortBy = $request->input('sort_by', 'created_at');
$sortDirection = $request->input('sort_direction', 'desc');
$perPage = $request->input('per_page', 10);
$query = Category::with('posts');
if (!empty($filter)) {
$query->where('name', 'like', '%' . $filter . '%');
}
$query->orderBy($sortBy, $sortDirection);
$categories = $query->paginate($perPage);
return CategoryResource::collection($categories);
}
/**
* @OA\Get(
* path="/blog/categories/{id}",
* tags={"Categories"},
* summary="Get a category by ID",
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\Response(response=200, description="OK")
* )
*/
public function show($id)
{
$category = Category::with('posts')->findOrFail($id);
return new CategoryResource($category);
}
/**
* @OA\Get(
* path="/blog/categories/{slug}/posts",
* tags={"Categories"},
* summary="Get posts by category slug",
* @OA\Parameter(name="slug", in="path", required=true, @OA\Schema(type="string")),
* @OA\Parameter(name="per_page", in="query", @OA\Schema(type="integer")),
* @OA\Response(response=200, description="OK"),
* @OA\Response(response=404, description="Category not found")
* )
*/
public function getCategoryWithPosts(Request $request, $slug)
{
$category = Category::where('slug', $slug)->first();
if (!$category) {
return response()->json([
'message' => 'Category not found.'
], 404);
}
$perPage = $request->input('per_page', 10);
$filter = $request->input('filter');
$sortBy = $request->input('sort_by', 'published_at');
$sortDirection = $request->input('sort_direction', 'desc');
$postsQuery = $category->posts()->with(['category', 'tags', 'author']);
if (!empty($filter)) {
$postsQuery->where('title', 'like', '%' . $filter . '%');
}
$postsQuery->orderBy($sortBy, $sortDirection);
$posts = $postsQuery->paginate($perPage);
$meta = [
'total' => $posts->total(),
'per_page' => $posts->perPage(),
'current_page' => $posts->currentPage(),
'last_page' => $posts->lastPage(),
'from' => $posts->firstItem(),
'to' => $posts->lastItem(),
];
$links = [
'first' => $posts->url(1),
'last' => $posts->url($posts->lastPage()),
'prev' => $posts->previousPageUrl(),
'next' => $posts->nextPageUrl(),
];
return response()->json([
'category' => new CategoryResource($category),
'posts' => PostResource::collection($posts),
'meta' => $meta,
'links' => $links,
], 200);
}
/**
* @OA\Post(
* path="/blog/categories",
* tags={"Categories"},
* summary="Create a category",
* @OA\RequestBody(@OA\MediaType(mediaType="application/json")),
* @OA\Response(response=201, description="Created")
* )
*/
public function store(StoreCategoryRequest $request)
{
$category = Category::create($request->validated());
return new CategoryResource($category);
}
/**
* @OA\Put(
* path="/blog/categories/{id}",
* tags={"Categories"},
* summary="Update a category",
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\RequestBody(@OA\MediaType(mediaType="application/json")),
* @OA\Response(response=200, description="OK")
* )
*/
public function update(UpdateCategoryRequest $request, $id)
{
$category = Category::findOrFail($id);
$category->update($request->validated());
return new CategoryResource($category);
}
/**
* @OA\Delete(
* path="/blog/categories/{id}",
* tags={"Categories"},
* summary="Delete a category",
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\Response(response=200, description="OK")
* )
*/
public function destroy($id)
{
$category = Category::findOrFail($id);
$category->delete();
return response()->json(['message' => 'Category deleted successfully'], 200);
}
}

View File

@ -1,47 +1,47 @@
<?php
namespace App\Http\Controllers\Blog;
namespace App\Http\Controllers\Api\Blog;
use App\Http\Controllers\Controller;
use App\Http\Resources\CategoryResource;
use App\Http\Resources\PostResource;
use App\Models\Blog\Category;
use App\Models\Blog\Post;
use Illuminate\Http\Request;
/**
* @OA\Tag(name="Blog Home", description="Blog homepage aggregated data")
*/
class HomeController extends Controller
{
/**
* برگرداندن داده‌های مورد نیاز برای صفحه‌ی اصلی
*
* @return \Illuminate\Http\JsonResponse
* @OA\Get(
* path="/blog/homepage",
* tags={"Blog Home"},
* summary="Get homepage data: latest posts, categories, most viewed, random",
* @OA\Response(response=200, description="OK")
* )
*/
public function index()
{
// 1. 5 پست آخر
$latestPosts = Post::with(['category', 'tags', 'author'])
->orderBy('published_at', 'desc')
->take(5)
->get();
// 2. 5 دسته‌بندی آخر
$latestCategories = Category::orderBy('created_at', 'desc')
->take(5)
->get();
// 3. 5 پست با بیشترین بازدید
$mostViewedPosts = Post::with(['category', 'tags', 'author'])
->orderBy('views', 'desc')
->take(5)
->get();
// 4. 4 پست تصادفی
$randomPosts = Post::with(['category', 'tags', 'author'])
->inRandomOrder()
->take(4)
->get();
// بازگشت به صورت JSON با استفاده از منابع (Resources)
return response()->json([
'latest_posts' => PostResource::collection($latestPosts),
'latest_categories' => CategoryResource::collection($latestCategories),

View File

@ -0,0 +1,130 @@
<?php
namespace App\Http\Controllers\Api\Blog;
use App\Http\Controllers\Controller;
use App\Http\Requests\Blog\StorePostRequest;
use App\Http\Requests\Blog\UpdatePostRequest;
use App\Http\Resources\PostResource;
use App\Models\Blog\Post;
use Illuminate\Http\Request;
/**
* @OA\Tag(name="Posts", description="Blog post management")
*/
class PostController extends Controller
{
/**
* @OA\Get(
* path="/blog/posts",
* tags={"Posts"},
* summary="Paginated list of posts",
* @OA\Parameter(name="per_page", in="query", @OA\Schema(type="integer")),
* @OA\Parameter(name="category", in="query", @OA\Schema(type="string")),
* @OA\Parameter(name="tag", in="query", @OA\Schema(type="string")),
* @OA\Parameter(name="search", in="query", @OA\Schema(type="string")),
* @OA\Parameter(name="sort_field", in="query", @OA\Schema(type="string")),
* @OA\Parameter(name="sort_direction", in="query", @OA\Schema(type="string", enum={"asc","desc"})),
* @OA\Response(response=200, description="OK")
* )
*/
public function index(Request $request)
{
$filters = $request->only(['category', 'tag', 'search', 'author', 'published']);
$sortField = $request->get('sort_field', 'published_at');
$sortDirection = $request->get('sort_direction', 'desc');
$perPage = $request->get('per_page', 10);
$query = Post::with(['category', 'tags', 'author'])
->filter($filters)
->sort($sortField, $sortDirection);
$posts = $query->paginate($perPage);
return PostResource::collection($posts);
}
/**
* @OA\Get(
* path="/blog/posts/{slug}",
* tags={"Posts"},
* summary="Get a single post by slug with related posts",
* @OA\Parameter(name="slug", in="path", required=true, @OA\Schema(type="string")),
* @OA\Response(response=200, description="OK")
* )
*/
public function show($slug)
{
$post = Post::with(['category', 'tags', 'author', 'relatedPosts.category', 'relatedPosts.tags', 'relatedPosts.author'])
->where('slug', $slug)
->firstOrFail();
return response()->json([
'post' => new PostResource($post),
'related_posts' => PostResource::collection($post->relatedPosts->sortByDesc('published_at')->take(5)),
], 200);
}
/**
* @OA\Post(
* path="/blog/posts",
* tags={"Posts"},
* summary="Create a post",
* @OA\RequestBody(@OA\MediaType(mediaType="application/json")),
* @OA\Response(response=201, description="Created")
* )
*/
public function store(StorePostRequest $request)
{
$post = Post::create($request->validated());
if ($request->has('tags')) {
$post->tags()->attach($request->tags);
}
return new PostResource($post->load(['category', 'tags', 'author']));
}
/**
* @OA\Put(
* path="/blog/posts/{id}",
* tags={"Posts"},
* summary="Update a post",
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\RequestBody(@OA\MediaType(mediaType="application/json")),
* @OA\Response(response=200, description="OK")
* )
*/
public function update(UpdatePostRequest $request, $id)
{
$post = Post::findOrFail($id);
$post->update($request->validated());
if ($request->has('tags')) {
$post->tags()->sync($request->tags);
}
return new PostResource($post->load(['category', 'tags', 'author']));
}
/**
* @OA\Delete(
* path="/blog/posts/{id}",
* tags={"Posts"},
* summary="Delete a post",
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\Response(response=200, description="OK")
* )
*/
public function destroy($id)
{
$post = Post::findOrFail($id);
$post->tags()->detach();
$post->relatedPosts()->detach();
$post->delete();
return response()->json(['message' => 'Post deleted successfully'], 200);
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace App\Http\Controllers\Api\Blog;
use App\Http\Controllers\Controller;
use App\Http\Requests\Blog\StoreTagRequest;
use App\Http\Requests\Blog\UpdateTagRequest;
use App\Http\Resources\TagResource;
use App\Models\Blog\Tag;
use Illuminate\Http\Request;
/**
* @OA\Tag(name="Tags", description="Blog tag management")
*/
class TagController extends Controller
{
/**
* @OA\Get(
* path="/blog/tags",
* tags={"Tags"},
* summary="Paginated list of tags",
* @OA\Response(response=200, description="OK")
* )
*/
public function index(Request $request)
{
$tags = Tag::with('posts')->paginate(10);
return TagResource::collection($tags);
}
/**
* @OA\Get(
* path="/blog/tags/{id}",
* tags={"Tags"},
* summary="Get a tag by ID",
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\Response(response=200, description="OK")
* )
*/
public function show($id)
{
$tag = Tag::with('posts')->findOrFail($id);
return new TagResource($tag);
}
/**
* @OA\Post(
* path="/blog/tags",
* tags={"Tags"},
* summary="Create a tag",
* @OA\RequestBody(@OA\MediaType(mediaType="application/json")),
* @OA\Response(response=201, description="Created")
* )
*/
public function store(StoreTagRequest $request)
{
$tag = Tag::create($request->validated());
return new TagResource($tag);
}
/**
* @OA\Put(
* path="/blog/tags/{id}",
* tags={"Tags"},
* summary="Update a tag",
* @OA\Parameter(name="id", in="path", required=True, @OA\Schema(type="integer")),
* @OA\RequestBody(@OA\MediaType(mediaType="application/json")),
* @OA\Response(response=200, description="OK")
* )
*/
public function update(UpdateTagRequest $request, $id)
{
$tag = Tag::findOrFail($id);
$tag->update($request->validated());
return new TagResource($tag);
}
/**
* @OA\Delete(
* path="/blog/tags/{id}",
* tags={"Tags"},
* summary="Delete a tag",
* @OA\Parameter(name="id", in="path", required=True, @OA\Schema(type="integer")),
* @OA\Response(response=200, description="OK")
* )
*/
public function destroy($id)
{
$tag = Tag::findOrFail($id);
$tag->posts()->detach();
$tag->delete();
return response()->json(['message' => 'Tag deleted successfully'], 200);
}
}

View File

@ -0,0 +1,131 @@
<?php
namespace App\Http\Controllers\Api\Website;
use App\Http\Controllers\Controller;
use App\Http\Requests\Website\StoreAboutMeRequest;
use App\Http\Requests\Website\UpdateAboutMeRequest;
use App\Models\Website\AboutMe;
/**
* @OA\Tag(name="About Me", description="About Me section endpoints")
*/
class AboutMeController extends Controller
{
/**
* @OA\Get(
* path="/website/about-me",
* tags={"About Me"},
* summary="Get the first About Me entry with skills",
* @OA\Response(response=200, description="OK", @OA\JsonContent(ref="#/components/schemas/AboutMe"))
* )
*/
public function index()
{
$aboutMe = AboutMe::with('skills')->first();
return response()->json($aboutMe);
}
/**
* @OA\Get(
* path="/website/about-me/{id}",
* tags={"About Me"},
* summary="Get a specific About Me entry",
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\Response(response=200, description="OK")
* )
*/
public function show($id)
{
$aboutMe = AboutMe::with('skills')->findOrFail($id);
return response()->json($aboutMe);
}
/**
* @OA\Post(
* path="/website/about-me",
* tags={"About Me"},
* summary="Create an About Me entry",
* @OA\RequestBody(@OA\MediaType(mediaType="application/json")),
* @OA\Response(response=201, description="Created")
* )
*/
public function store(StoreAboutMeRequest $request)
{
$aboutMe = AboutMe::create($request->validated());
return response()->json($aboutMe, 201);
}
/**
* @OA\Put(
* path="/website/about-me/{id}",
* tags={"About Me"},
* summary="Update an About Me entry",
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\RequestBody(@OA\MediaType(mediaType="application/json")),
* @OA\Response(response=200, description="OK")
* )
*/
public function update(UpdateAboutMeRequest $request, $id)
{
$aboutMe = AboutMe::findOrFail($id);
$aboutMe->update($request->validated());
return response()->json($aboutMe);
}
/**
* @OA\Delete(
* path="/website/about-me/{id}",
* tags={"About Me"},
* summary="Delete an About Me entry",
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\Response(response=204, description="No Content")
* )
*/
public function destroy($id)
{
AboutMe::destroy($id);
return response()->json(null, 204);
}
/**
* @OA\Get(
* path="/website/about-me/{id}/translation/{locale}",
* tags={"About Me"},
* summary="Get localized About Me with skills",
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\Parameter(name="locale", in="path", required=true, @OA\Schema(type="string", enum={"fa","en"})),
* @OA\Response(response=200, description="OK", @OA\JsonContent(ref="#/components/schemas/AboutMe")),
* @OA\Response(response=404, description="Translation not found")
* )
*/
public function getTranslation($id, $locale)
{
$aboutMe = AboutMe::with('skills')->findOrFail($id);
if (!$aboutMe->hasTranslation('title', $locale)) {
return response()->json(['message' => 'Translation not found', 'locale' => $locale], 404);
}
$skills = $aboutMe->skills->map(function ($skill) use ($locale) {
return [
'id' => $skill->id,
'title' => $skill->getTranslation('title', $locale),
'description' => $skill->getTranslation('description', $locale),
'image' => $skill->image,
'link' => $skill->link,
'percentage' => $skill->percentage,
];
});
$data = [
'id' => $aboutMe->id,
'title' => $aboutMe->getTranslation('title', $locale),
'description' => $aboutMe->getTranslation('description', $locale),
'image' => $aboutMe->image,
'skills' => $skills,
];
return response()->json($data);
}
}

View File

@ -0,0 +1,158 @@
<?php
namespace App\Http\Controllers\Api\Website;
use App\Http\Controllers\Controller;
use App\Http\Requests\Website\StoreProjectRequest;
use App\Http\Requests\Website\UpdateProjectRequest;
use App\Models\Website\Project;
use App\Models\Website\ProjectCategory;
/**
* @OA\Tag(name="Projects", description="Projects section endpoints")
*/
class ProjectController extends Controller
{
/**
* @OA\Get(
* path="/website/projects/translation/{locale}",
* tags={"Projects"},
* summary="List projects and categories in a locale",
* @OA\Parameter(name="locale", in="path", required=true, @OA\Schema(type="string", enum={"fa","en"})),
* @OA\Parameter(name="per_page", in="query", @OA\Schema(type="integer")),
* @OA\Parameter(name="sort", in="query", @OA\Schema(type="string", enum={"asc","desc"})),
* @OA\Response(response=200, description="OK")
* )
*/
public function index($locale): \Illuminate\Http\JsonResponse
{
$perPage = request()->query('per_page', 10);
$projects = Project::with('category')->paginate($perPage);
$formattedProjects = $projects->items();
$formattedProjects = array_map(function ($project) use ($locale) {
return [
'id' => $project->id,
'title' => $project->getTranslation('title', $locale),
'category' => $project->category?->getTranslation('title', $locale),
'images' => [
['src' => $project->image1, 'position' => '1', 'zIndex' => 1],
['src' => $project->image2, 'position' => '2', 'zIndex' => 2],
],
'created_at' => $project->created_at,
'updated_at' => $project->updated_at,
];
}, $formattedProjects);
return response()->json([
'projects' => [
'current_page' => $projects->currentPage(),
'data' => $formattedProjects,
'total' => $projects->total(),
'last_page' => $projects->lastPage(),
],
'categories' => ProjectCategory::all()->map(function ($category) use ($locale) {
return [
'id' => $category->id,
'title' => $category->getTranslation('title', $locale),
'image' => $category->image,
];
}),
]);
}
/**
* @OA\Post(
* path="/website/projects",
* tags={"Projects"},
* summary="Create a project",
* @OA\RequestBody(@OA\MediaType(mediaType="application/json")),
* @OA\Response(response=201, description="Created")
* )
*/
public function store(StoreProjectRequest $request)
{
$project = Project::create($request->only(['category_id', 'image1', 'image2']));
foreach (['fa', 'en'] as $locale) {
$project->setTranslation('title', $locale, $request->input("title_$locale"));
$project->setTranslation('description', $locale, $request->input("description_$locale"));
}
$project->save();
return response()->json($project, 201);
}
/**
* @OA\Get(
* path="/website/projects/{id}/translation/{locale}",
* tags={"Projects"},
* summary="Get a single project translation",
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\Parameter(name="locale", in="path", required=true, @OA\Schema(type="string", enum={"fa","en"})),
* @OA\Response(response=200, description="OK")
* )
*/
public function show($id, $locale)
{
$project = Project::with('category')->findOrFail($id);
$formattedProject = [
'id' => $project->id,
'title' => $project->getTranslation('title', $locale),
'category' => $project->category?->getTranslation('title', $locale),
'images' => [
['src' => $project->image1, 'position' => '1', 'zIndex' => 1],
['src' => $project->image2, 'position' => '2', 'zIndex' => 2],
],
'created_at' => $project->created_at,
'updated_at' => $project->updated_at,
];
return response()->json(['project' => $formattedProject]);
}
/**
* @OA\Put(
* path="/website/projects/{id}",
* tags={"Projects"},
* summary="Update a project",
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\RequestBody(@OA\MediaType(mediaType="application/json")),
* @OA\Response(response=200, description="OK")
* )
*/
public function update(UpdateProjectRequest $request, $id)
{
$project = Project::findOrFail($id);
$project->update($request->only(['category_id', 'image1', 'image2']));
foreach (['fa', 'en'] as $locale) {
if ($request->has("title_$locale")) {
$project->setTranslation('title', $locale, $request->input("title_$locale"));
}
if ($request->has("description_$locale")) {
$project->setTranslation('description', $locale, $request->input("description_$locale"));
}
}
$project->save();
return response()->json($project);
}
/**
* @OA\Delete(
* path="/website/projects/{id}",
* tags={"Projects"},
* summary="Delete a project",
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\Response(response=204, description="No Content")
* )
*/
public function destroy($id)
{
Project::destroy($id);
return response()->json(null, 204);
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Api\Website;
use App\Http\Controllers\Controller;
use App\Models\Website\Service;
/**
* @OA\Tag(name="Services", description="Services section endpoints")
*/
class ServiceController extends Controller
{
/**
* @OA\Get(
* path="/website/services/{locale}",
* tags={"Services"},
* summary="List all services in a given locale",
* @OA\Parameter(name="locale", in="path", required=true, @OA\Schema(type="string", enum={"fa","en"})),
* @OA\Response(response=200, description="OK", @OA\JsonContent(type="array", @OA\Items(ref="#/components/schemas/Service")))
* )
*/
public function index($locale)
{
$services = Service::all();
$response = $services->map(function ($service) use ($locale) {
return [
'id' => $service->id,
'imageSrc' => $service->imageSrc,
'altText' => '',
'created_at' => $service->created_at,
'updated_at' => $service->updated_at,
'title' => $service->getTranslation('title', $locale),
'description' => $service->getTranslation('description', $locale),
];
});
return response()->json($response);
}
/**
* @OA\Get(
* path="/website/services/{id}/translation/{locale}",
* tags={"Services"},
* summary="Get a single service translation",
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\Parameter(name="locale", in="path", required=true, @OA\Schema(type="string", enum={"fa","en"})),
* @OA\Response(response=200, description="OK")
* )
*/
public function showTranslation($id, $locale)
{
$service = Service::findOrFail($id);
return response()->json([
'id' => $service->id,
'imageSrc' => $service->imageSrc,
'title' => $service->getTranslation('title', $locale),
'description' => $service->getTranslation('description', $locale),
]);
}
}

View File

@ -1,168 +0,0 @@
<?php
namespace App\Http\Controllers\Blog;
use App\Http\Controllers\Controller;
use App\Http\Resources\CategoryResource;
use App\Http\Resources\PostResource;
use App\Models\Blog\Category;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class CategoryController extends Controller
{
// نمایش لیست دسته‌بندی‌ها با صفحه‌بندی
public function index(Request $request)
{
// گرفتن فیلتر و سورت از درخواست کاربر
$filter = $request->input('filter'); // فیلتر (به عنوان مثال، نام دسته‌بندی)
$sortBy = $request->input('sort_by', 'created_at'); // مرتب‌سازی (پیش‌فرض بر اساس زمان ایجاد)
$sortDirection = $request->input('sort_direction', 'desc'); // جهت مرتب‌سازی (پیش‌فرض نزولی)
$perPage = $request->input('per_page', 10); // تعداد آیتم‌ها در هر صفحه (پیش‌فرض ۱۰)
// شروع کوئری با مدل دسته‌بندی و روابط
$query = Category::with('posts');
// اعمال فیلتر در صورت موجود بودن
if (!empty($filter)) {
$query->where('name', 'like', '%' . $filter . '%');
}
// اعمال مرتب‌سازی
$query->orderBy($sortBy, $sortDirection);
// گرفتن نتایج با پجینیشن
$categories = $query->paginate($perPage);
// بازگشت داده‌ها با استفاده از ریسورس
return CategoryResource::collection($categories);
}
// نمایش یک دسته‌بندی خاص
public function show($id)
{
$category = Category::with('posts')->findOrFail($id);
return new CategoryResource($category);
}
/**
* نمایش دسته‌بندی و پست‌های مرتبط با آن بر اساس slug
*
* @param \Illuminate\Http\Request $request
* @param string $slug
* @return \Illuminate\Http\JsonResponse
*/
public function getCategoryWithPosts(Request $request, $slug)
{
// یافتن دسته‌بندی بر اساس slug
$category = Category::where('slug', $slug)->first();
if (!$category) {
return response()->json([
'message' => 'دسته‌بندی مورد نظر یافت نشد.'
], 404);
}
// دریافت تعداد پست‌ها در هر صفحه (پیش‌فرض: ۱۰)
$perPage = $request->input('per_page', 10);
// دریافت فیلتر و سورت از درخواست
$filter = $request->input('filter'); // فیلتر (به عنوان مثال، عنوان پست)
$sortBy = $request->input('sort_by', 'published_at'); // مرتب‌سازی (پیش‌فرض بر اساس تاریخ انتشار)
$sortDirection = $request->input('sort_direction', 'desc'); // جهت مرتب‌سازی (پیش‌فرض نزولی)
// شروع کوئری با پست‌های مربوط به دسته‌بندی
$postsQuery = $category->posts()->with(['category', 'tags', 'author']);
// اعمال فیلتر در صورت موجود بودن
if (!empty($filter)) {
$postsQuery->where('title', 'like', '%' . $filter . '%');
}
// اعمال مرتب‌سازی
$postsQuery->orderBy($sortBy, $sortDirection);
// گرفتن پست‌ها با پجینیشن
$posts = $postsQuery->paginate($perPage);
// ساخت meta و links برای pagination
$meta = [
'total' => $posts->total(),
'per_page' => $posts->perPage(),
'current_page' => $posts->currentPage(),
'last_page' => $posts->lastPage(),
'from' => $posts->firstItem(),
'to' => $posts->lastItem(),
];
$links = [
'first' => $posts->url(1),
'last' => $posts->url($posts->lastPage()),
'prev' => $posts->previousPageUrl(),
'next' => $posts->nextPageUrl(),
];
// بازگشت اطلاعات دسته‌بندی و پست‌ها
return response()->json([
'category' => new CategoryResource($category),
'posts' => PostResource::collection($posts),
'meta' => $meta,
'links' => $links,
], 200);
}
// ذخیره یک دسته‌بندی جدید
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255|unique:categories,name',
'slug' => 'required|string|unique:categories,slug',
'description' => 'required|string',
'short_description' => 'nullable|string|max:500',
'image' => 'nullable|url',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$category = Category::create($validator->validated());
return new CategoryResource($category);
}
// به‌روزرسانی یک دسته‌بندی خاص
public function update(Request $request, $id)
{
$category = Category::findOrFail($id);
$validator = Validator::make($request->all(), [
'name' => 'sometimes|required|string|max:255|unique:categories,name,' . $category->id,
'slug' => 'sometimes|required|string|unique:categories,slug,' . $category->id,
'description' => 'sometimes|required|string',
'short_description' => 'nullable|string|max:500',
'image' => 'nullable|url',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$category->update($validator->validated());
return new CategoryResource($category);
}
// حذف یک دسته‌بندی خاص
public function destroy($id)
{
$category = Category::findOrFail($id);
// قبل از حذف، مطمئن شوید که پست‌ها به دسته‌بندی دیگری منتقل شده‌اند یا حذف شوند
// برای سادگی، اینجا فقط دسته‌بندی را حذف می‌کنیم
$category->delete();
return response()->json(['message' => 'Category deleted successfully'], 200);
}
}

View File

@ -1,127 +0,0 @@
<?php
namespace App\Http\Controllers\Blog;
use App\Http\Controllers\Controller;
use App\Http\Resources\PostResource;
use App\Models\Blog\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class PostController extends Controller
{
// نمایش لیست پست‌ها با فیلتر و صفحه‌بندی
public function index(Request $request)
{
// دریافت پارامترهای فیلتر از درخواست
$filters = $request->only(['category', 'tag', 'search', 'author', 'published']);
// دریافت پارامترهای مرتب‌سازی از درخواست
$sortField = $request->get('sort_field', 'published_at'); // فیلد پیش‌فرض: published_at
$sortDirection = $request->get('sort_direction', 'desc'); // جهت پیش‌فرض: نزولی
// دریافت پارامترهای صفحه‌بندی از درخواست
$perPage = $request->get('per_page', 10); // تعداد آیتم‌ها در هر صفحه (پیش‌فرض: ۱۰)
// ساخت Query با اعمال فیلترها و مرتب‌سازی
$query = Post::with(['category', 'tags', 'author'])
->filter($filters)
->sort($sortField, $sortDirection);
// اعمال صفحه‌بندی
$posts = $query->paginate($perPage);
// بازگشت نتایج به صورت JSON با استفاده از Resource
return PostResource::collection($posts);
}
// نمایش یک پست خاص
public function show($slug)
{
// یافتن پست بر اساس slug و بارگذاری روابط مرتبط
$post = Post::with(['category', 'tags', 'author', 'relatedPosts.category', 'relatedPosts.tags', 'relatedPosts.author'])
->where('slug', $slug)
->firstOrFail(); // در صورت عدم وجود پست، خطای 404 برمی‌گرداند
// بازگشت پست و پست‌های مرتبط به صورت JSON با استفاده از منابع
return response()->json([
'post' => new PostResource($post),
'related_posts' => PostResource::collection($post->relatedPosts->sortByDesc('published_at')->take(5)), // بازگشت ۵ پست مرتبط جدیدترین
], 200);
}
// ذخیره یک پست جدید
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'title' => 'required|string|max:255',
'body' => 'required|string',
'excerpt' => 'nullable|string|max:500',
'slug' => 'required|string|unique:posts,slug',
'author_id' => 'required|exists:users,id',
'category_id' => 'required|exists:categories,id',
'featured_image' => 'nullable|url',
'views' => 'nullable|integer',
'likes' => 'nullable|integer',
'published_at' => 'nullable|date',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$post = Post::create($validator->validated());
if ($request->has('tags')) {
$post->tags()->attach($request->tags);
}
return new PostResource($post->load(['category', 'tags', 'author']));
}
// به‌روزرسانی یک پست خاص
public function update(Request $request, $id)
{
$post = Post::findOrFail($id);
$validator = Validator::make($request->all(), [
'title' => 'sometimes|required|string|max:255',
'body' => 'sometimes|required|string',
'excerpt' => 'nullable|string|max:500',
'slug' => 'sometimes|required|string|unique:posts,slug,' . $post->id,
'author_id' => 'sometimes|required|exists:users,id',
'category_id' => 'sometimes|required|exists:categories,id',
'featured_image' => 'nullable|url',
'views' => 'nullable|integer',
'likes' => 'nullable|integer',
'published_at' => 'nullable|date',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$post->update($validator->validated());
if ($request->has('tags')) {
$post->tags()->sync($request->tags);
}
return new PostResource($post->load(['category', 'tags', 'author']));
}
// حذف یک پست خاص
public function destroy($id)
{
$post = Post::findOrFail($id);
$post->tags()->detach();
$post->relatedPosts()->detach();
$post->delete();
return response()->json(['message' => 'Post deleted successfully'], 200);
}
}

View File

@ -1,72 +0,0 @@
<?php
namespace App\Http\Controllers\Blog;
use App\Http\Controllers\Controller;
use App\Http\Resources\TagResource;
use App\Models\Blog\Tag;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class TagController extends Controller
{
// نمایش لیست تگ‌ها با صفحه‌بندی
public function index(Request $request)
{
$tags = Tag::with('posts')->paginate(10);
return TagResource::collection($tags);
}
// نمایش یک تگ خاص
public function show($id)
{
$tag = Tag::with('posts')->findOrFail($id);
return new TagResource($tag);
}
// ذخیره یک تگ جدید
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255|unique:tags,name',
'slug' => 'required|string|unique:tags,slug',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$tag = Tag::create($validator->validated());
return new TagResource($tag);
}
// به‌روزرسانی یک تگ خاص
public function update(Request $request, $id)
{
$tag = Tag::findOrFail($id);
$validator = Validator::make($request->all(), [
'name' => 'sometimes|required|string|max:255|unique:tags,name,' . $tag->id,
'slug' => 'sometimes|required|string|unique:tags,slug,' . $tag->id,
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$tag->update($validator->validated());
return new TagResource($tag);
}
// حذف یک تگ خاص
public function destroy($id)
{
$tag = Tag::findOrFail($id);
$tag->posts()->detach();
$tag->delete();
return response()->json(['message' => 'Tag deleted successfully'], 200);
}
}

View File

@ -1,73 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Project;
class ProjectController extends Controller
{
public function index(Request $request)
{
$projects = Project::query();
// 1. اگر category مشخص شده باشد
if ($request->has('category')) {
$category = $request->input('category');
// فیلتر کردن پروژه‌ها بر اساس category با استفاده از LIKE
$projects = $projects->where('category', 'like', '%' . $category . '%');
}
// 2. انتخاب نوع مرتب‌سازی (جدیدترین یا قدیمی‌ترین)
$sortOrder = $request->input('sort', 'desc'); // 'desc' برای جدیدترین و 'asc' برای قدیمی‌ترین
$projects = $projects->orderBy('created_at', $sortOrder);
// 3. پیاده‌سازی pagination
$perPage = $request->input('per_page', 10); // تعداد پروژه‌ها در هر صفحه
$paginatedProjects = $projects->paginate($perPage);
// 4. دریافت تمام categories موجود از ستون category
$categories = Project::distinct()->pluck('category'); // گرفتن دسته‌بندی‌های یکتا
return response()->json([
'projects' => $paginatedProjects,
'categories' => $categories,
]);
}
public function create()
{
// برگرداندن ویو فرم ایجاد پروژه (اگر نیاز است)
}
public function store(Request $request)
{
$project = Project::create($request->all());
return response()->json($project, 201);
}
public function show($id)
{
$project = Project::findOrFail($id);
return response()->json($project);
}
public function edit($id)
{
// برگرداندن ویو فرم ویرایش پروژه (اگر نیاز است)
}
public function update(Request $request, $id)
{
$project = Project::findOrFail($id);
$project->update($request->all());
return response()->json($project);
}
public function destroy($id)
{
Project::destroy($id);
return response()->json(null, 204);
}
}

View File

@ -1,79 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\Service;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Validation\ValidationException;
class ServiceController extends Controller
{
// نمایش لیست تمامی سرویس‌ها
public function index()
{
return response()->json(Service::all(), Response::HTTP_OK);
}
// نمایش یک سرویس خاص بر اساس ID
public function show($id)
{
$service = Service::find($id);
if (!$service) {
return response()->json(['message' => 'Service not found'], Response::HTTP_NOT_FOUND);
}
return response()->json($service, Response::HTTP_OK);
}
// ایجاد یک سرویس جدید
public function store(Request $request)
{
$validatedData = $request->validate([
'title' => 'required|string|max:255',
'description' => 'required|string',
'imageSrc' => 'required|string|max:255',
'altText' => 'required|string|max:255',
]);
$service = Service::create($validatedData);
return response()->json($service, Response::HTTP_CREATED);
}
// بروزرسانی یک سرویس موجود
public function update(Request $request, $id)
{
$service = Service::find($id);
if (!$service) {
return response()->json(['message' => 'Service not found'], Response::HTTP_NOT_FOUND);
}
$validatedData = $request->validate([
'title' => 'string|max:255',
'description' => 'string',
'imageSrc' => 'string|max:255',
'altText' => 'string|max:255',
]);
$service->update(array_filter($validatedData));
return response()->json($service, Response::HTTP_OK);
}
// حذف یک سرویس
public function destroy($id)
{
$service = Service::find($id);
if (!$service) {
return response()->json(['message' => 'Service not found'], Response::HTTP_NOT_FOUND);
}
$service->delete();
return response()->json(['message' => 'Service deleted successfully'], Response::HTTP_NO_CONTENT);
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Skill;
class SkillController extends Controller
{
public function index()
{
return response()->json(Skill::all());
}
public function show($id)
{
return response()->json(Skill::findOrFail($id));
}
public function store(Request $request)
{
$skill = Skill::create($request->all());
return response()->json($skill, 201);
}
public function update(Request $request, $id)
{
$skill = Skill::findOrFail($id);
$skill->update($request->all());
return response()->json($skill);
}
public function destroy($id)
{
Skill::destroy($id);
return response()->json(null, 204);
}
}

View File

@ -1,82 +0,0 @@
<?php
namespace App\Http\Controllers\Website;
use Illuminate\Http\Request;
use App\Models\Website\AboutMe;
use App\Http\Controllers\Controller;
class AboutMeController extends Controller
{
public function index()
{
$aboutMe = AboutMe::with('skills.translations')->first(); // بارگذاری مهارت‌ها و ترجمه‌ها
return response()->json($aboutMe);
}
public function show($id)
{
$aboutMe = AboutMe::with('skills.translations')->findOrFail($id);
return response()->json($aboutMe);
}
public function store(Request $request)
{
$aboutMe = AboutMe::create($request->all());
return response()->json($aboutMe, 201);
}
public function update(Request $request, $id)
{
$aboutMe = AboutMe::findOrFail($id);
$aboutMe->update($request->all());
return response()->json($aboutMe);
}
public function destroy($id)
{
AboutMe::destroy($id);
return response()->json(null, 204);
}
// متد جدید برای دریافت ترجمه بر اساس locale از کوکی
public function getTranslation($id, $locale)
{
// بارگذاری AboutMe با ترجمه‌های مربوط به locale و مهارت‌ها
$aboutMe = AboutMe::with(['translations' => function ($query) use ($locale) {
$query->where('locale', $locale);
}, 'skills.translations' => function ($query) use ($locale) {
$query->where('locale', $locale);
}])->findOrFail($id);
// اگر ترجمه موجود نیست، می‌توانید ترجمه پیش‌فرض یا خطا را برگردانید
if ($aboutMe->translations->isEmpty()) {
return response()->json(['message' => 'Translation not found', 'locale' => $locale], 404);
}
// ترکیب اطلاعات AboutMe و ترجمه
$translation = $aboutMe->translations->first(); // فقط یک ترجمه دریافت می‌کنیم
$skills = $aboutMe->skills->map(function ($skill) use ($locale) {
$translation = $skill->getTranslation($locale); // گرفتن ترجمه برای هر مهارت
return [
'id' => $skill->id,
'title' => $translation ? $translation->title : null, // عنوان از ترجمه
'description' => $translation ? $translation->description : null, // توضیحات از ترجمه
'image' => $skill->image, // تصویر
'link' => $skill->link, // لینک
'percentage' => $skill->percentage, // درصد
];
});
$data = [
'id' => $aboutMe->id,
'title' => $translation->title,
'description' => $translation->description,
'image' => $aboutMe->image,
'skills' => $skills, // مهارت‌ها اکنون بارگذاری شده‌اند
];
return response()->json($data);
}
}

View File

@ -1,119 +0,0 @@
<?php
namespace App\Http\Controllers\website;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Website\Project;
use App\Models\Website\ProjectCategory;
class ProjectController extends Controller
{
public function index($locale): \Illuminate\Http\JsonResponse
{
// Read per_page from query string or set a default value
$perPage = request()->query('per_page', 10); // مقدار پیش‌فرض ۱۰ در نظر گرفته می‌شود
// Retrieve projects with translations and categories
$projects = Project::with('translations', 'category.translations')->paginate($perPage);
$formattedProjects = $projects->items();
$formattedProjects = array_map(function ($project) use ($locale) {
$translation = $project->translations->firstWhere('locale', $locale);
$categoryTranslation = $project->category->translations->firstWhere('locale', $locale);
return [
'id' => $project->id,
'title' => $translation ? $translation->title : null,
'category' => $categoryTranslation ? $categoryTranslation->title : null,
'images' => [
['src' => $project->image1, 'position' => '1', 'zIndex' => 1],
['src' => $project->image2, 'position' => '2', 'zIndex' => 2],
],
'created_at' => $project->created_at,
'updated_at' => $project->updated_at,
];
}, $formattedProjects);
return response()->json([
'projects' => [
'current_page' => $projects->currentPage(),
'data' => $formattedProjects,
'total' => $projects->total(),
'last_page' => $projects->lastPage(),
],
'categories' => ProjectCategory::all()->map(function ($category) use ($locale) {
return [
'id' => $category->id,
'title' => $category->translations->firstWhere('locale', $locale)->title ?? null,
'image' => $category->image,
];
}),
]);
}
public function store(Request $request)
{
$project = Project::create($request->only(['category_id', 'image1', 'image2']));
// ایجاد ترجمه‌ها
foreach (['fa', 'en'] as $locale) {
$project->translations()->create([
'locale' => $locale,
'title' => $request->input("title_$locale"),
'description' => $request->input("description_$locale"),
]);
}
return response()->json($project->load('translations'), 201);
}
public function show($id)
{
$project = Project::with('translations', 'category.translations')->findOrFail($id);
$translation = $project->translations->firstWhere('locale', 'fa');
$categoryTranslation = $project->category->translations->firstWhere('locale', 'fa');
$formattedProject = [
'id' => $project->id,
'title' => $translation ? $translation->title : null,
'category' => $categoryTranslation ? $categoryTranslation->title : null,
'images' => [
['src' => $project->image1, 'position' => '1', 'zIndex' => 1],
['src' => $project->image2, 'position' => '2', 'zIndex' => 2],
],
'created_at' => $project->created_at,
'updated_at' => $project->updated_at,
];
return response()->json(['project' => $formattedProject]);
}
public function update(Request $request, $id)
{
$project = Project::findOrFail($id);
$project->update($request->only(['category_id', 'image1', 'image2']));
// به‌روزرسانی ترجمه‌ها
foreach (['fa', 'en'] as $locale) {
$project->translations()->updateOrCreate(
['locale' => $locale],
[
'title' => $request->input("title_$locale"),
'description' => $request->input("description_$locale"),
]
);
}
return response()->json($project->load('translations'));
}
public function destroy($id)
{
Project::destroy($id);
return response()->json(null, 204);
}
}

View File

@ -1,50 +0,0 @@
<?php
namespace App\Http\Controllers\Website;
use App\Http\Controllers\Controller;
use App\Models\Website\Service;
class ServiceController extends Controller
{
/**
* دریافت لیست خدمات با ترجمه‌ها بر اساس زبان
*/
public function index($locale)
{
// گرفتن همه خدمات به همراه ترجمه‌ها
$services = Service::with(['translations' => function ($query) use ($locale) {
$query->where('locale', $locale);
}])->get();
// فرمتی که می‌خواهیم به کاربر ارسال کنیم
$response = $services->map(function ($service) use ($locale) {
return [
'id' => $service->id,
'imageSrc' => $service->imageSrc,
'altText' => $service->translations->first()->altText ?? '',
'created_at' => $service->created_at,
'updated_at' => $service->updated_at,
'title' => optional($service->translations->first())->title,
'description' => optional($service->translations->first())->description,
];
});
return response()->json($response);
}
/**
* نمایش یک خدمت خاص با ترجمه
*/
public function showTranslation($id, $locale)
{
$service = Service::with('translations')->findOrFail($id);
return response()->json([
'id' => $service->id,
'imageSrc' => $service->imageSrc,
'title' => $service->getTitle($locale),
'description' => $service->getDescription($locale),
]);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests\Blog;
use Illuminate\Foundation\Http\FormRequest;
class StoreCategoryRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => 'required|string|max:255|unique:categories,name',
'slug' => 'required|string|unique:categories,slug',
'description' => 'required|string',
'short_description' => 'nullable|string|max:500',
'image' => 'nullable|url',
];
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\Blog;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'body' => 'required|string',
'excerpt' => 'nullable|string|max:500',
'slug' => 'required|string|unique:posts,slug',
'author_id' => 'required|exists:users,id',
'category_id' => 'required|exists:categories,id',
'featured_image' => 'nullable|url',
'views' => 'nullable|integer',
'likes' => 'nullable|integer',
'published_at' => 'nullable|date',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
];
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests\Blog;
use Illuminate\Foundation\Http\FormRequest;
class StoreTagRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => 'required|string|max:255|unique:tags,name',
'slug' => 'required|string|unique:tags,slug',
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests\Blog;
use Illuminate\Foundation\Http\FormRequest;
class UpdateCategoryRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$categoryId = $this->route('category');
return [
'name' => 'sometimes|required|string|max:255|unique:categories,name,' . $categoryId,
'slug' => 'sometimes|required|string|unique:categories,slug,' . $categoryId,
'description' => 'sometimes|required|string',
'short_description' => 'nullable|string|max:500',
'image' => 'nullable|url',
];
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\Blog;
use Illuminate\Foundation\Http\FormRequest;
class UpdatePostRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'sometimes|required|string|max:255',
'body' => 'sometimes|required|string',
'excerpt' => 'nullable|string|max:500',
'slug' => 'sometimes|required|string|unique:posts,slug,' . $this->route('post'),
'author_id' => 'sometimes|required|exists:users,id',
'category_id' => 'sometimes|required|exists:categories,id',
'featured_image' => 'nullable|url',
'views' => 'nullable|integer',
'likes' => 'nullable|integer',
'published_at' => 'nullable|date',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests\Blog;
use Illuminate\Foundation\Http\FormRequest;
class UpdateTagRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$tagId = $this->route('tag');
return [
'name' => 'sometimes|required|string|max:255|unique:tags,name,' . $tagId,
'slug' => 'sometimes|required|string|unique:tags,slug,' . $tagId,
];
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests\Website;
use Illuminate\Foundation\Http\FormRequest;
class StoreAboutMeRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'image' => 'required|string|max:255',
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests\Website;
use Illuminate\Foundation\Http\FormRequest;
class StoreProjectRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'category_id' => 'required|exists:website_project_categories,id',
'image1' => 'required|string|max:255',
'image2' => 'required|string|max:255',
'title_fa' => 'required|string|max:255',
'title_en' => 'required|string|max:255',
'description_fa' => 'required|string',
'description_en' => 'required|string',
];
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Http\Requests\Website;
use Illuminate\Foundation\Http\FormRequest;
class UpdateAboutMeRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'image' => 'sometimes|required|string|max:255',
];
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests\Website;
use Illuminate\Foundation\Http\FormRequest;
class UpdateProjectRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'category_id' => 'sometimes|required|exists:website_project_categories,id',
'image1' => 'sometimes|required|string|max:255',
'image2' => 'sometimes|required|string|max:255',
'title_fa' => 'sometimes|required|string|max:255',
'title_en' => 'sometimes|required|string|max:255',
'description_fa' => 'sometimes|required|string',
'description_en' => 'sometimes|required|string',
];
}
}

View File

@ -1,25 +0,0 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class User extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
// اضافه کردن سایر فیلدهای مورد نیاز
'email' => $this->email,
'created_at' => $this->created_at,
];
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AboutMe extends Model
{
use HasFactory;
protected $table = 'about_me';
protected $fillable = ['title', 'description', 'image'];
public function skills()
{
return $this->hasMany(Skill::class);
}
}

View File

@ -1,17 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Skill extends Model
{
use HasFactory;
protected $fillable = ['title', 'image', 'description', 'link', 'about_me_id'];
public function aboutMe()
{
return $this->belongsTo(AboutMe::class);
}
}

View File

@ -5,49 +5,52 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AboutMe extends Model
{
use HasFactory;
protected $table = 'website_about_me';
protected $fillable = ['image'];
protected $fillable = ['image',
'translations',];
/**
* ارتباط با ترجمه‌ها
*/
public function translations()
{
return $this->hasMany(AboutMeTranslation::class, 'about_me_id');
}
/**
* گرفتن ترجمه بر اساس زبان فعلی
*/
public function getTranslation($locale)
{
return $this->translations()->where('locale', $locale)->first();
}
/**
* گرفتن عنوان بر اساس زبان فعلی
*/
public function getTitle($locale)
{
$translation = $this->getTranslation($locale);
return $translation ? $translation->title : null;
}
/**
* گرفتن توضیحات بر اساس زبان فعلی
*/
public function getDescription($locale)
{
$translation = $this->getTranslation($locale);
return $translation ? $translation->description : null;
}
public function skills()
{
return $this->hasMany(Skill::class);
}
protected $casts = [
'translations' => 'array',
];
public function getTranslation(string $key, string $locale, bool $useFallbackLocale = true): mixed
{
$translations = $this->translations;
if (isset($translations[$key][$locale])) {
return $translations[$key][$locale];
}
if ($useFallbackLocale && isset($translations[$key]['en'])) {
return $translations[$key]['en'];
}
return null;
}
public function hasTranslation(string $key, string $locale): bool
{
$translations = $this->translations;
return isset($translations[$key][$locale]);
}
public function getTitleAttribute()
{
return $this->translations['title'] ?? null;
}
public function getDescriptionAttribute()
{
return $this->translations['description'] ?? null;
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Models\Website;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AboutMeTranslation extends Model
{
use HasFactory;
protected $table = 'website_about_me_translations';
protected $fillable = ['about_me_id', 'locale', 'title', 'description'];
/**
* ارتباط با AboutMe
*/
public function aboutMe()
{
return $this->belongsTo(AboutMe::class, 'about_me_id');
}
}

View File

@ -5,20 +5,46 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Project extends Model
{
use HasFactory;
protected $table = 'website_projects'; // نام صحیح جدول
protected $fillable = ['category_id', 'image1', 'image2'];
protected $table = 'website_projects';
protected $fillable = ['category_id', 'image1', 'image2',
'translations',];
public function translations()
{
return $this->hasMany(ProjectTranslation::class, 'project_id');
}
public function category()
{
return $this->belongsTo(ProjectCategory::class, 'category_id');
}
protected $casts = [
'translations' => 'array',
];
public function getTranslation(string $key, string $locale, bool $useFallbackLocale = true): mixed
{
$translations = $this->translations;
if (isset($translations[$key][$locale])) {
return $translations[$key][$locale];
}
if ($useFallbackLocale && isset($translations[$key]['en'])) {
return $translations[$key]['en'];
}
return null;
}
public function getTitleAttribute()
{
return $this->translations['title'] ?? null;
}
public function getDescriptionAttribute()
{
return $this->translations['description'] ?? null;
}
}

View File

@ -5,15 +5,41 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ProjectCategory extends Model
{
use HasFactory;
protected $table = 'website_project_categories'; // نام صحیح جدول
protected $fillable = ['image'];
protected $table = 'website_project_categories';
protected $fillable = ['image',
'translations',];
public function translations()
protected $casts = [
'translations' => 'array',
];
public function getTranslation(string $key, string $locale, bool $useFallbackLocale = true): mixed
{
return $this->hasMany(ProjectCategoryTranslation::class, 'category_id');
$translations = $this->translations;
if (isset($translations[$key][$locale])) {
return $translations[$key][$locale];
}
if ($useFallbackLocale && isset($translations[$key]['en'])) {
return $translations[$key]['en'];
}
return null;
}
public function getTitleAttribute()
{
return $this->translations['title'] ?? null;
}
public function getDescriptionAttribute()
{
return $this->translations['description'] ?? null;
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace App\Models\Website;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ProjectCategoryTranslation extends Model
{
use HasFactory;
protected $table = 'website_project_category_translations'; // نام صحیح جدول
protected $fillable = ['category_id', 'locale', 'title', 'description'];
}

View File

@ -1,19 +0,0 @@
<?php
namespace App\Models\Website;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ProjectTranslation extends Model
{
use HasFactory;
protected $table = 'website_project_translations'; // نام صحیح جدول
protected $fillable = ['project_id', 'locale', 'title', 'description'];
public function project()
{
return $this->belongsTo(Project::class, 'project_id');
}
}

View File

@ -9,40 +9,33 @@ class Service extends Model
{
use HasFactory;
protected $table = 'website_services'; // نام جدول
protected $fillable = ['imageSrc']; // فیلدهایی که قابل پر کردن هستند
protected $table = 'website_services';
protected $fillable = ['imageSrc',
'translations'];
/**
* ارتباط با جدول ترجمه‌ها
*/
public function translations()
protected $casts = [
'translations' => 'array',
];
public function getTranslation(string $key, string $locale, bool $useFallbackLocale = true): mixed
{
return $this->hasMany(ServiceTranslation::class, 'service_id');
$translations = $this->translations;
if (isset($translations[$key][$locale])) {
return $translations[$key][$locale];
}
if ($useFallbackLocale && isset($translations[$key]['en'])) {
return $translations[$key]['en'];
}
return null;
}
/**
* گرفتن ترجمه بر اساس زبان فعلی
*/
public function getTranslation($locale)
public function getTitleAttribute()
{
return $this->translations()->where('locale', $locale)->first();
return $this->translations['title'] ?? null;
}
/**
* گرفتن عنوان بر اساس زبان فعلی
*/
public function getTitle($locale)
public function getDescriptionAttribute()
{
$translation = $this->getTranslation($locale);
return $translation ? $translation->title : null;
}
/**
* گرفتن توضیحات بر اساس زبان فعلی
*/
public function getDescription($locale)
{
$translation = $this->getTranslation($locale);
return $translation ? $translation->description : null;
return $this->translations['description'] ?? null;
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Models\Website;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ServiceTranslation extends Model
{
use HasFactory;
protected $table = 'website_service_translations'; // نام جدول
protected $fillable = ['service_id', 'locale', 'title', 'description']; // فیلدهایی که قابل پر کردن هستند
/**
* ارتباط با جدول خدمات
*/
public function service()
{
return $this->belongsTo(Service::class, 'service_id');
}
}

View File

@ -5,52 +5,46 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Skill extends Model
{
use HasFactory;
protected $table = 'website_skills'; // نام جدول
protected $fillable = ['image', 'link', 'about_me_id', 'percentage']; // فیلدهایی که قابل پر کردن هستند
protected $table = 'website_skills';
protected $fillable = ['image', 'link', 'about_me_id', 'percentage',
'translations',];
/**
* ارتباط با جدول AboutMe
*/
public function aboutMe()
{
return $this->belongsTo(AboutMe::class, 'about_me_id');
}
/**
* ارتباط با جدول ترجمه‌ها
*/
public function translations()
protected $casts = [
'translations' => 'array',
];
public function getTranslation(string $key, string $locale, bool $useFallbackLocale = true): mixed
{
return $this->hasMany(SkillTranslation::class, 'skill_id');
$translations = $this->translations;
if (isset($translations[$key][$locale])) {
return $translations[$key][$locale];
}
if ($useFallbackLocale && isset($translations[$key]['en'])) {
return $translations[$key]['en'];
}
return null;
}
/**
* گرفتن ترجمه بر اساس زبان فعلی
*/
public function getTranslation($locale)
public function getTitleAttribute()
{
return $this->translations()->where('locale', $locale)->first();
return $this->translations['title'] ?? null;
}
/**
* گرفتن عنوان بر اساس زبان فعلی
*/
public function getTitle($locale)
public function getDescriptionAttribute()
{
$translation = $this->getTranslation($locale);
return $translation ? $translation->title : null;
return $this->translations['description'] ?? null;
}
/**
* گرفتن توضیحات بر اساس زبان فعلی
*/
public function getDescription($locale)
{
$translation = $this->getTranslation($locale);
return $translation ? $translation->description : null;
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Models\Website;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class SkillTranslation extends Model
{
use HasFactory;
protected $table = 'website_skill_translations'; // نام جدول
protected $fillable = ['skill_id', 'locale', 'title', 'description']; // فیلدهایی که قابل پر کردن هستند
/**
* ارتباط با جدول Skills
*/
public function skill()
{
return $this->belongsTo(Skill::class, 'skill_id');
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace App;
use OpenApi\Attributes as OA;
#[OA\Info(
title: 'Parsa Aghayi Portfolio API',
version: '1.0.0',
description: 'REST API for Parsa Aghayi\'s personal portfolio and blog'
)]
#[OA\Server(
url: 'http://localhost:8000/api',
description: 'Development server'
)]
#[OA\Server(
url: 'https://backend.parsaaghayi.ir/api',
description: 'Production server'
)]
#[OA\Schema(
schema: 'Skill',
type: 'object',
properties: [
new OA\Property(property: 'id', type: 'integer'),
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'description', type: 'string'),
new OA\Property(property: 'image', type: 'string'),
new OA\Property(property: 'link', type: 'string', nullable: true),
new OA\Property(property: 'percentage', type: 'integer'),
]
)]
#[OA\Schema(
schema: 'AboutMe',
type: 'object',
properties: [
new OA\Property(property: 'id', type: 'integer'),
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'description', type: 'string'),
new OA\Property(property: 'image', type: 'string'),
new OA\Property(property: 'skills', type: 'array', items: new OA\Items(ref: '#/components/schemas/Skill')),
]
)]
#[OA\Schema(
schema: 'Service',
type: 'object',
properties: [
new OA\Property(property: 'id', type: 'integer'),
new OA\Property(property: 'imageSrc', type: 'string'),
new OA\Property(property: 'altText', type: 'string'),
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'description', type: 'string'),
new OA\Property(property: 'created_at', type: 'string', format: 'datetime'),
new OA\Property(property: 'updated_at', type: 'string', format: 'datetime'),
]
)]
#[OA\Schema(
schema: 'Project',
type: 'object',
properties: [
new OA\Property(property: 'id', type: 'integer'),
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'category', type: 'string', nullable: true),
new OA\Property(property: 'images', type: 'array', items: new OA\Items(type: 'object')),
new OA\Property(property: 'created_at', type: 'string', format: 'datetime'),
new OA\Property(property: 'updated_at', type: 'string', format: 'datetime'),
]
)]
#[OA\Schema(
schema: 'Category',
type: 'object',
properties: [
new OA\Property(property: 'id', type: 'integer'),
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'image', type: 'string'),
]
)]
#[OA\Schema(
schema: 'Post',
type: 'object',
properties: [
new OA\Property(property: 'id', type: 'integer'),
new OA\Property(property: 'title', type: 'string'),
new OA\Property(property: 'body', type: 'string'),
new OA\Property(property: 'excerpt', type: 'string'),
new OA\Property(property: 'slug', type: 'string'),
new OA\Property(property: 'featured_image', type: 'string', nullable: true),
new OA\Property(property: 'author', type: 'object'),
new OA\Property(property: 'category', type: 'object'),
new OA\Property(property: 'tags', type: 'array', items: new OA\Items(type: 'object')),
new OA\Property(property: 'published_at', type: 'string', format: 'datetime', nullable: true),
]
)]
class SwaggerDefinitions
{
}

View File

@ -0,0 +1,15 @@
<?php
namespace App;
use L5Swagger\CustomGeneratorInterface;
use OpenApi\Generator as OpenApiGenerator;
use Psr\Log\NullLogger;
class SwaggerGeneratorFactory implements CustomGeneratorInterface
{
public function create(): OpenApiGenerator
{
return new OpenApiGenerator(new NullLogger());
}
}

0
bootstrap/cache/.gitignore vendored Normal file → Executable file
View File

1
ci_trigger.txt Normal file
View File

@ -0,0 +1 @@
1782670160

View File

@ -6,9 +6,11 @@
"license": "MIT",
"require": {
"php": "^8.2",
"laravel/framework": "^11.9",
"darkaonline/l5-swagger": "^11.0",
"laravel/framework": "^13.0",
"laravel/sanctum": "^4.0",
"laravel/tinker": "^2.9"
"laravel/tinker": "^3.0",
"spatie/laravel-translatable": "^6.14"
},
"require-dev": {
"fakerphp/faker": "^1.23",
@ -16,7 +18,7 @@
"laravel/sail": "^1.26",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.0",
"phpunit/phpunit": "^11.0.1"
"phpunit/phpunit": "^12.0"
},
"autoload": {
"psr-4": {

3572
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,8 @@
'default' => env('CACHE_STORE', 'database'),
'serializable_classes' => false,
/*
|--------------------------------------------------------------------------
| Cache Stores

View File

@ -19,7 +19,7 @@
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins' => [env('FRONTEND_URL', 'https://parsaaghayi.ir')],
'allowed_origins_patterns' => [],

326
config/l5-swagger.php Normal file
View File

@ -0,0 +1,326 @@
<?php
return [
'default' => 'default',
'documentations' => [
'default' => [
'api' => [
'title' => 'L5 Swagger UI',
],
'routes' => [
/*
* Route for accessing api documentation interface
*/
'api' => 'api/documentation',
],
'paths' => [
/*
* Edit to include full URL in ui for assets
*/
'use_absolute_path' => env('L5_SWAGGER_USE_ABSOLUTE_PATH', true),
/*
* Edit to set path where swagger ui assets should be stored
*/
'swagger_ui_assets_path' => env('L5_SWAGGER_UI_ASSETS_PATH', 'vendor/swagger-api/swagger-ui/dist/'),
/*
* File name of the generated json documentation file
*/
'docs_json' => 'api-docs.json',
/*
* File name of the generated YAML documentation file
*/
'docs_yaml' => 'api-docs.yaml',
/*
* Set this to `json` or `yaml` to determine which documentation file to use in UI
*/
'format_to_use_for_docs' => env('L5_FORMAT_TO_USE_FOR_DOCS', 'json'),
/*
* Absolute paths to directory containing the swagger annotations are stored.
*/
'annotations' => [
base_path('app'),
],
],
],
],
'defaults' => [
'routes' => [
/*
* Route for accessing parsed swagger annotations.
*/
'docs' => 'docs',
/*
* Route for Oauth2 authentication callback.
*/
'oauth2_callback' => 'api/oauth2-callback',
/*
* Middleware allows to prevent unexpected access to API documentation
*/
'middleware' => [
'api' => [],
'asset' => [],
'docs' => [],
'oauth2_callback' => [],
],
/*
* Route Group options
*/
'group_options' => [],
],
'paths' => [
/*
* Absolute path to location where parsed annotations will be stored
*/
'docs' => storage_path('api-docs'),
/*
* Absolute path to directory where to export views
*/
'views' => base_path('resources/views/vendor/l5-swagger'),
/*
* Edit to set the api's base path
*/
'base' => env('L5_SWAGGER_BASE_PATH', null),
/*
* Absolute path to directories that should be excluded from scanning
* @deprecated Please use `scanOptions.exclude`
* `scanOptions.exclude` overwrites this
*/
'excludes' => [],
],
'scanOptions' => [
/**
* Use a custom generator factory that suppresses swagger-php warnings
* (which get converted to ErrorException by Laravel).
*
* @see \OpenApi\Generator
*/
'generator_factory' => \App\SwaggerGeneratorFactory::class,
/**
* Configuration for default processors. Allows to pass processors configuration to swagger-php.
*
* @link https://zircote.github.io/swagger-php/reference/processors.html
*/
'default_processors_configuration' => [
/** Example */
/**
* 'operationId.hash' => true,
* 'pathFilter' => [
* 'tags' => [
* '/pets/',
* '/store/',
* ],
* ],.
*/
],
/**
* analyser: defaults to \OpenApi\StaticAnalyser .
*
* @see \OpenApi\scan
*/
'analyser' => null,
/**
* analysis: defaults to a new \OpenApi\Analysis .
*
* @see \OpenApi\scan
*/
'analysis' => null,
/**
* Custom query path processors classes.
*
* @link https://github.com/zircote/swagger-php/tree/master/Examples/processors/schema-query-parameter
* @see \OpenApi\scan
*/
'processors' => [
// new \App\SwaggerProcessors\SchemaQueryParameter(),
],
/**
* pattern: string $pattern File pattern(s) to scan (default: *.php) .
*
* @see \OpenApi\scan
*/
'pattern' => null,
/*
* Absolute path to directories that should be excluded from scanning
* @note This option overwrites `paths.excludes`
* @see \OpenApi\scan
*/
'exclude' => [],
/*
* Allows to generate specs either for OpenAPI 3.0.0 or OpenAPI 3.1.0.
* By default the spec will be in version 3.0.0
*/
'open_api_spec_version' => env('L5_SWAGGER_OPEN_API_SPEC_VERSION', \L5Swagger\Generator::OPEN_API_DEFAULT_SPEC_VERSION),
],
/*
* API security definitions. Will be generated into documentation file.
*/
'securityDefinitions' => [
'securitySchemes' => [
/*
* Examples of Security schemes
*/
/*
'api_key_security_example' => [ // Unique name of security
'type' => 'apiKey', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2".
'description' => 'A short description for security scheme',
'name' => 'api_key', // The name of the header or query parameter to be used.
'in' => 'header', // The location of the API key. Valid values are "query" or "header".
],
'oauth2_security_example' => [ // Unique name of security
'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2".
'description' => 'A short description for oauth2 security scheme.',
'flow' => 'implicit', // The flow used by the OAuth2 security scheme. Valid values are "implicit", "password", "application" or "accessCode".
'authorizationUrl' => 'http://example.com/auth', // The authorization URL to be used for (implicit/accessCode)
//'tokenUrl' => 'http://example.com/auth' // The authorization URL to be used for (password/application/accessCode)
'scopes' => [
'read:projects' => 'read your projects',
'write:projects' => 'modify projects in your account',
]
],
*/
/* Open API 3.0 support
'passport' => [ // Unique name of security
'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2".
'description' => 'Laravel passport oauth2 security.',
'in' => 'header',
'scheme' => 'https',
'flows' => [
"password" => [
"authorizationUrl" => config('app.url') . '/oauth/authorize',
"tokenUrl" => config('app.url') . '/oauth/token',
"refreshUrl" => config('app.url') . '/token/refresh',
"scopes" => []
],
],
],
'sanctum' => [ // Unique name of security
'type' => 'apiKey', // Valid values are "basic", "apiKey" or "oauth2".
'description' => 'Enter token in format (Bearer <token>)',
'name' => 'Authorization', // The name of the header or query parameter to be used.
'in' => 'header', // The location of the API key. Valid values are "query" or "header".
],
*/
],
'security' => [
/*
* Examples of Securities
*/
[
/*
'oauth2_security_example' => [
'read',
'write'
],
'passport' => []
*/
],
],
],
/*
* Set this to `true` in development mode so that docs would be regenerated on each request
* Set this to `false` to disable swagger generation on production
*/
'generate_always' => env('L5_SWAGGER_GENERATE_ALWAYS', false),
/*
* Set this to `true` to generate a copy of documentation in yaml format
*/
'generate_yaml_copy' => env('L5_SWAGGER_GENERATE_YAML_COPY', false),
/*
* Edit to trust the proxy's ip address - needed for AWS Load Balancer
* string[]
*/
'proxy' => false,
/*
* Configs plugin allows to fetch external configs instead of passing them to SwaggerUIBundle.
* See more at: https://github.com/swagger-api/swagger-ui#configs-plugin
*/
'additional_config_url' => null,
/*
* Apply a sort to the operation list of each API. It can be 'alpha' (sort by paths alphanumerically),
* 'method' (sort by HTTP method).
* Default is the order returned by the server unchanged.
*/
'operations_sort' => env('L5_SWAGGER_OPERATIONS_SORT', null),
/*
* Pass the validatorUrl parameter to SwaggerUi init on the JS side.
* A null value here disables validation.
*/
'validator_url' => null,
/*
* Swagger UI configuration parameters
*/
'ui' => [
'display' => [
'dark_mode' => env('L5_SWAGGER_UI_DARK_MODE', false),
/*
* Controls the default expansion setting for the operations and tags. It can be :
* 'list' (expands only the tags),
* 'full' (expands the tags and operations),
* 'none' (expands nothing).
*/
'doc_expansion' => env('L5_SWAGGER_UI_DOC_EXPANSION', 'none'),
/**
* If set, enables filtering. The top bar will show an edit box that
* you can use to filter the tagged operations that are shown. Can be
* Boolean to enable or disable, or a string, in which case filtering
* will be enabled using that string as the filter expression. Filtering
* is case-sensitive matching the filter expression anywhere inside
* the tag.
*/
'filter' => env('L5_SWAGGER_UI_FILTERS', true), // true | false
],
'authorization' => [
/*
* If set to true, it persists authorization data, and it would not be lost on browser close/refresh
*/
'persist_authorization' => env('L5_SWAGGER_UI_PERSIST_AUTHORIZATION', false),
'oauth2' => [
/*
* If set to true, adds PKCE to AuthorizationCodeGrant flow
*/
'use_pkce_with_authorization_code_grant' => false,
],
],
],
/*
* Constants which can be used in annotations
*/
'constants' => [
'L5_SWAGGER_CONST_HOST' => env('L5_SWAGGER_CONST_HOST', 'http://my-default-host.com'),
],
],
];

View File

@ -77,7 +77,7 @@
'middleware' => [
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\PreventRequestForgery::class,
],
];

View File

@ -0,0 +1,17 @@
<?php
namespace Database\Factories\Website;
use Illuminate\Database\Eloquent\Factories\Factory;
class AboutMeFactory extends Factory
{
protected $model = \App\Models\Website\AboutMe::class;
public function definition(): array
{
return [
'image' => $this->faker->imageUrl(),
];
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Database\Factories\Website;
use Illuminate\Database\Eloquent\Factories\Factory;
class ProjectCategoryFactory extends Factory
{
protected $model = \App\Models\Website\ProjectCategory::class;
public function definition(): array
{
return [
'image' => $this->faker->imageUrl(),
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Database\Factories\Website;
use Illuminate\Database\Eloquent\Factories\Factory;
class ProjectFactory extends Factory
{
protected $model = \App\Models\Website\Project::class;
public function definition(): array
{
return [
'category_id' => \App\Models\Website\ProjectCategory::factory(),
'image1' => $this->faker->imageUrl(),
'image2' => $this->faker->imageUrl(),
];
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Database\Factories\Website;
use Illuminate\Database\Eloquent\Factories\Factory;
class ServiceFactory extends Factory
{
protected $model = \App\Models\Website\Service::class;
public function definition(): array
{
return [
'imageSrc' => $this->faker->imageUrl(),
];
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Database\Factories\Website;
use Illuminate\Database\Eloquent\Factories\Factory;
class SkillFactory extends Factory
{
protected $model = \App\Models\Website\Skill::class;
public function definition(): array
{
return [
'image' => $this->faker->imageUrl(),
'link' => $this->faker->url(),
'percentage' => $this->faker->numberBetween(10, 100),
'about_me_id' => \App\Models\Website\AboutMe::factory(),
];
}
}

View File

@ -0,0 +1,73 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
// Add JSON columns for spatie/laravel-translatable on website_about_me
Schema::table('website_about_me', function (Blueprint $table) {
if (!Schema::hasColumn('website_about_me', 'title')) {
$table->json('title')->nullable()->after('image');
}
if (!Schema::hasColumn('website_about_me', 'description')) {
$table->json('description')->nullable()->after('title');
}
});
// Migrate data from translations table to JSON columns
DB::statement('
UPDATE website_about_me a
SET a.title = (
SELECT JSON_OBJECTAGG(locale, title)
FROM website_about_me_translations t
WHERE t.about_me_id = a.id
),
a.description = (
SELECT JSON_OBJECTAGG(locale, description)
FROM website_about_me_translations t
WHERE t.about_me_id = a.id
)
');
// Add JSON columns for spatie/laravel-translatable on website_skills
Schema::table('website_skills', function (Blueprint $table) {
if (!Schema::hasColumn('website_skills', 'title')) {
$table->json('title')->nullable()->after('image');
}
if (!Schema::hasColumn('website_skills', 'description')) {
$table->json('description')->nullable()->after('title');
}
});
// Migrate data from skill translations table to JSON columns
DB::statement('
UPDATE website_skills s
SET s.title = (
SELECT JSON_OBJECTAGG(locale, title)
FROM website_skill_translations t
WHERE t.skill_id = s.id
),
s.description = (
SELECT JSON_OBJECTAGG(locale, description)
FROM website_skill_translations t
WHERE t.skill_id = s.id
)
');
}
public function down(): void
{
Schema::table('website_about_me', function (Blueprint $table) {
$table->dropColumn(['title', 'description']);
});
Schema::table('website_skills', function (Blueprint $table) {
$table->dropColumn(['title', 'description']);
});
}
};

View File

@ -0,0 +1,248 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// === WEBSITE TABLES ===
// website_about_me
Schema::table('website_about_me', function (Blueprint $table) {
$table->json('translations')->nullable()->after('image');
});
$rows = DB::table('website_about_me_translations')->get();
foreach ($rows->groupBy('about_me_id') as $aboutMeId => $translations) {
$data = [];
foreach ($translations as $t) {
$data['title'][$t->locale] = $t->title;
$data['description'][$t->locale] = $t->description;
}
DB::table('website_about_me')->where('id', $aboutMeId)->update(['translations' => json_encode($data)]);
}
Schema::dropIfExists('website_about_me_translations');
// website_skills
Schema::table('website_skills', function (Blueprint $table) {
$table->json('translations')->nullable()->after('percentage');
});
$rows = DB::table('website_skill_translations')->get();
foreach ($rows->groupBy('skill_id') as $skillId => $translations) {
$data = [];
foreach ($translations as $t) {
$data['title'][$t->locale] = $t->title;
$data['description'][$t->locale] = $t->description;
}
DB::table('website_skills')->where('id', $skillId)->update(['translations' => json_encode($data)]);
}
Schema::dropIfExists('website_skill_translations');
// website_services
Schema::table('website_services', function (Blueprint $table) {
$table->json('translations')->nullable()->after('imageSrc');
});
$rows = DB::table('website_service_translations')->get();
foreach ($rows->groupBy('service_id') as $serviceId => $translations) {
$data = [];
foreach ($translations as $t) {
$data['title'][$t->locale] = $t->title;
$data['description'][$t->locale] = $t->description;
}
DB::table('website_services')->where('id', $serviceId)->update(['translations' => json_encode($data)]);
}
Schema::dropIfExists('website_service_translations');
// website_project_categories
Schema::table('website_project_categories', function (Blueprint $table) {
$table->json('translations')->nullable()->after('image');
});
$rows = DB::table('website_project_category_translations')->get();
foreach ($rows->groupBy('category_id') as $categoryId => $translations) {
$data = [];
foreach ($translations as $t) {
$data['title'][$t->locale] = $t->title;
$data['description'][$t->locale] = $t->description;
}
DB::table('website_project_categories')->where('id', $categoryId)->update(['translations' => json_encode($data)]);
}
Schema::dropIfExists('website_project_category_translations');
// website_projects
Schema::table('website_projects', function (Blueprint $table) {
$table->json('translations')->nullable()->after('image2');
});
$rows = DB::table('website_project_translations')->get();
foreach ($rows->groupBy('project_id') as $projectId => $translations) {
$data = [];
foreach ($translations as $t) {
$data['title'][$t->locale] = $t->title;
$data['description'][$t->locale] = $t->description;
}
DB::table('website_projects')->where('id', $projectId)->update(['translations' => json_encode($data)]);
}
Schema::dropIfExists('website_project_translations');
// === BLOG TABLES ===
Schema::table('posts', function (Blueprint $table) {
$table->json('translations')->nullable()->after('featured_image');
});
Schema::table('categories', function (Blueprint $table) {
$table->json('translations')->nullable()->after('image');
});
Schema::table('tags', function (Blueprint $table) {
$table->json('translations')->nullable()->after('slug');
});
}
public function down(): void
{
// Restore website_about_me_translations
Schema::create('website_about_me_translations', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('about_me_id');
$table->string('locale')->index();
$table->string('title');
$table->text('description');
$table->timestamps();
$table->foreign('about_me_id')->references('id')->on('website_about_me')->onDelete('cascade');
$table->unique(['about_me_id', 'locale']);
});
DB::table('website_about_me')->whereNotNull('translations')->each(function ($row) {
$translations = json_decode($row->translations, true);
if (!$translations) return;
foreach (($translations['title'] ?? []) as $locale => $title) {
DB::table('website_about_me_translations')->insert([
'about_me_id' => $row->id,
'locale' => $locale,
'title' => $title,
'description' => $translations['description'][$locale] ?? '',
]);
}
});
Schema::table('website_about_me', function (Blueprint $table) {
$table->dropColumn('translations');
});
// Restore website_skill_translations
Schema::create('website_skill_translations', function (Blueprint $table) {
$table->id();
$table->foreignId('skill_id')->constrained('website_skills')->onDelete('cascade');
$table->string('locale')->index();
$table->string('title');
$table->text('description');
$table->unique(['skill_id', 'locale']);
$table->timestamps();
});
DB::table('website_skills')->whereNotNull('translations')->each(function ($row) {
$translations = json_decode($row->translations, true);
if (!$translations) return;
foreach (($translations['title'] ?? []) as $locale => $title) {
DB::table('website_skill_translations')->insert([
'skill_id' => $row->id,
'locale' => $locale,
'title' => $title,
'description' => $translations['description'][$locale] ?? '',
]);
}
});
Schema::table('website_skills', function (Blueprint $table) {
$table->dropColumn('translations');
});
// Restore website_service_translations
Schema::create('website_service_translations', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('service_id');
$table->string('locale')->index();
$table->string('title');
$table->text('description');
$table->timestamps();
$table->foreign('service_id')->references('id')->on('website_services')->onDelete('cascade');
$table->unique(['service_id', 'locale']);
});
DB::table('website_services')->whereNotNull('translations')->each(function ($row) {
$translations = json_decode($row->translations, true);
if (!$translations) return;
foreach (($translations['title'] ?? []) as $locale => $title) {
DB::table('website_service_translations')->insert([
'service_id' => $row->id,
'locale' => $locale,
'title' => $title,
'description' => $translations['description'][$locale] ?? '',
]);
}
});
Schema::table('website_services', function (Blueprint $table) {
$table->dropColumn('translations');
});
// Restore website_project_category_translations
Schema::create('website_project_category_translations', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('category_id');
$table->string('locale')->index();
$table->string('title');
$table->text('description');
$table->timestamps();
$table->foreign('category_id')->references('id')->on('website_project_categories')->onDelete('cascade');
$table->unique(['category_id', 'locale']);
});
DB::table('website_project_categories')->whereNotNull('translations')->each(function ($row) {
$translations = json_decode($row->translations, true);
if (!$translations) return;
foreach (($translations['title'] ?? []) as $locale => $title) {
DB::table('website_project_category_translations')->insert([
'category_id' => $row->id,
'locale' => $locale,
'title' => $title,
'description' => $translations['description'][$locale] ?? '',
]);
}
});
Schema::table('website_project_categories', function (Blueprint $table) {
$table->dropColumn('translations');
});
// Restore website_project_translations
Schema::create('website_project_translations', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('project_id');
$table->string('locale')->index();
$table->string('title');
$table->text('description');
$table->timestamps();
$table->foreign('project_id')->references('id')->on('website_projects')->onDelete('cascade');
$table->unique(['project_id', 'locale']);
});
DB::table('website_projects')->whereNotNull('translations')->each(function ($row) {
$translations = json_decode($row->translations, true);
if (!$translations) return;
foreach (($translations['title'] ?? []) as $locale => $title) {
DB::table('website_project_translations')->insert([
'project_id' => $row->id,
'locale' => $locale,
'title' => $title,
'description' => $translations['description'][$locale] ?? '',
]);
}
});
Schema::table('website_projects', function (Blueprint $table) {
$table->dropColumn('translations');
});
// Blog tables
Schema::table('posts', function (Blueprint $table) {
$table->dropColumn('translations');
});
Schema::table('categories', function (Blueprint $table) {
$table->dropColumn('translations');
});
Schema::table('tags', function (Blueprint $table) {
$table->dropColumn('translations');
});
}
};

View File

@ -7,6 +7,7 @@
use Database\Seeders\Website\AboutMeSeeder;
use Database\Seeders\Website\ProjectCategorySeeder;
use Database\Seeders\Website\ProjectSeeder;
use Database\Seeders\Website\ServiceSeeder;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
@ -23,13 +24,11 @@ public function run(): void
'email' => 'ceo@parsaaghayi.ir',
]);
$this->call([
AboutMeSeeder::class,
// ProjectSeeder::class,
ProjectCategorySeeder::class,
AboutMeSeeder::class,
ProjectSeeder::class,
ServiceSeeder::class,
BlogSeeder::class
// دیگر seeders
BlogSeeder::class,
]);
}
}

View File

@ -1,47 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Service;
class ServiceSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run()
{
$services = [
[
'title' => 'Custom Web Development',
'description' => 'We create tailored web applications that meet your unique business requirements, utilizing the latest technologies for optimal performance.',
'imageSrc' => '/images/ui-ux-vector.svg',
'altText' => 'Custom Web Development Vector',
],
[
'title' => 'Frontend Development',
'description' => 'Our frontend development services focus on crafting engaging, responsive user interfaces that enhance user experience across all devices.',
'imageSrc' => '/images/web-design-vector.svg',
'altText' => 'Frontend Development Vector',
],
[
'title' => 'Backend Development',
'description' => 'We provide reliable backend development solutions, building robust server-side architectures and APIs to ensure smooth and secure application functionality.',
'imageSrc' => '/images/app-design-vector.svg',
'altText' => 'Backend Development Vector',
],
[
'title' => 'Website Maintenance and Support',
'description' => 'Our ongoing maintenance and support services keep your website updated, secure, and performing at its best, allowing you to focus on your core business.',
'imageSrc' => '/images/graphic-design-vector.svg',
'altText' => 'Website Maintenance and Support Vector',
],
];
foreach ($services as $service) {
Service::create($service);
}
}
}

View File

@ -3,149 +3,59 @@
namespace Database\Seeders\Website;
use App\Models\Website\AboutMe;
use App\Models\Website\AboutMeTranslation;
use App\Models\Website\Skill;
use App\Models\Website\SkillTranslation;
use Illuminate\Database\Seeder;
class AboutMeSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run()
{
// ایجاد رکورد AboutMe بدون ترجمه
$aboutMe = AboutMe::create([
'image' => '/images/profile.png',
'translations' => [
'title' => [
'en' => 'About Me',
'fa' => 'درباره من',
],
'description' => [
'en' => 'With over 10 years of experience in web development, I bridge the gap between pixel-perfect frontend engineering and robust backend architecture. Specialized in refactoring legacy codebases, optimizing core web vitals, and building scalable enterprise-grade Next.js applications.',
'fa' => 'با بیش از ۱۰ سال تجربه در دنیای توسعه وب، مسیر کاری من ترکیبی از مهندسی دقیق فرانت‌اند و درک عمیق از معماری بک‌اند است. تخصص اصلی من در بازطراحی و Refactor کدهای پیچیده، بهینه‌سازی Performance پروژه‌های بزرگ Next.js و پیاده‌سازی زیرساخت‌های پایدار وب است.',
],
],
]);
// ایجاد ترجمه انگلیسی
AboutMeTranslation::create([
'about_me_id' => $aboutMe->id,
'locale' => 'en',
'title' => 'About Me',
'description' => 'I am a web developer with over 10 years of experience in creating dynamic and user-centric web applications. Specializing in React and Next.js, I build scalable solutions that enhance performance and user experience. I enjoy tackling complex challenges, from improving legacy systems to developing new applications. My focus on frontend development and seamless backend integration allows me to deliver innovative results while staying current with industry trends.',
]);
// ایجاد ترجمه فارسی
AboutMeTranslation::create([
'about_me_id' => $aboutMe->id,
'locale' => 'fa',
'title' => 'درباره من',
'description' => 'من یک توسعه‌دهنده وب با بیش از 10 سال تجربه در ایجاد برنامه‌های وب پویا و کاربرمحور هستم. با تخصص در React و Next.js، راه‌حل‌های مقیاس‌پذیری ایجاد می‌کنم که عملکرد و تجربه کاربری را بهبود می‌بخشند. من از حل چالش‌های پیچیده لذت می‌برم، از بهبود سیستم‌های قدیمی گرفته تا توسعه برنامه‌های جدید. تمرکز من بر توسعه فرانت‌اند و یکپارچه‌سازی بی‌دردسر با بک‌اند به من این امکان را می‌دهد که نتایج نوآورانه‌ای ارائه دهم و هم‌زمان با روندهای صنعت به‌روز باشم.',
]);
// ایجاد رکوردهای Skill و ترجمه‌های انگلیسی و فارسی
// HTML & CSS (SASS, Responsive Design)
$htmlSkill = Skill::create([
'about_me_id' => $aboutMe->id,
$skills = [
[
'image' => '/images/frontend.png',
'percentage' => 95,
'link' => 'http://example.com/frontend',
]);
SkillTranslation::create([
'skill_id' => $htmlSkill->id,
'locale' => 'en',
'title' => 'HTML & CSS (SASS, Responsive Design)',
'description' => 'Skilled in HTML5, CSS3, including SASS for modular and maintainable stylesheets. Experienced in building responsive web designs.',
]);
SkillTranslation::create([
'skill_id' => $htmlSkill->id,
'locale' => 'fa',
'title' => 'HTML و CSS (SASS، طراحی واکنش‌گرا)',
'description' => 'مهارت در HTML5، CSS3، شامل SASS برای ساخت استایل‌های مدولار و قابل نگهداری. تجربه در طراحی وب‌سایت‌های واکنش‌گرا.',
]);
// React.js and Next.js Development
$reactSkill = Skill::create([
'about_me_id' => $aboutMe->id,
'percentage' => 100,
'link' => '#',
'translations' => [
'title' => ['en' => 'React / Next.js / TypeScript', 'fa' => 'React / Next.js / TypeScript'],
'description' => ['en' => 'Core Tech', 'fa' => 'تکنولوژی‌های اصلی'],
],
],
[
'image' => '/images/backend.png',
'percentage' => 90,
'link' => 'http://example.com/backend',
]);
SkillTranslation::create([
'skill_id' => $reactSkill->id,
'locale' => 'en',
'title' => 'React.js and Next.js Development',
'description' => 'Extensive experience in React.js and Next.js for building scalable and performant web applications.',
]);
SkillTranslation::create([
'skill_id' => $reactSkill->id,
'locale' => 'fa',
'title' => 'توسعه React.js و Next.js',
'description' => 'تجربه گسترده در توسعه React.js و Next.js برای ساخت برنامه‌های وب مقیاس‌پذیر و پرکاربرد.',
]);
// Backend Development (RESTful APIs, Laravel)
$backendSkill = Skill::create([
'about_me_id' => $aboutMe->id,
'percentage' => 100,
'link' => '#',
'translations' => [
'title' => ['en' => 'Laravel / PHP / MySQL', 'fa' => 'Laravel / PHP / MySQL'],
'description' => ['en' => 'Backend & Tools', 'fa' => 'بک‌اند و ابزارها'],
],
],
[
'image' => '/images/ui.png',
'percentage' => 85,
'link' => 'http://example.com/ui',
]);
'percentage' => 100,
'link' => '#',
'translations' => [
'title' => ['en' => 'TDD / Clean Architecture', 'fa' => 'TDD / Clean Architecture'],
'description' => ['en' => 'Architecture & Others', 'fa' => 'معماری و سایر موارد'],
],
],
];
SkillTranslation::create([
'skill_id' => $backendSkill->id,
'locale' => 'en',
'title' => 'Backend Development (RESTful APIs, Laravel)',
'description' => 'Proficient in backend development, building robust RESTful APIs using Laravel and other technologies.',
]);
SkillTranslation::create([
'skill_id' => $backendSkill->id,
'locale' => 'fa',
'title' => 'توسعه بک‌اند (APIهای RESTful، لاراول)',
'description' => 'تسلط به توسعه بک‌اند و ساخت APIهای RESTful با استفاده از لاراول و دیگر تکنولوژی‌ها.',
]);
// WordPress Development and Customization
$wordpressSkill = Skill::create([
'about_me_id' => $aboutMe->id,
'image' => '/images/ux.png',
'percentage' => 90,
'link' => 'http://example.com/ux',
]);
SkillTranslation::create([
'skill_id' => $wordpressSkill->id,
'locale' => 'en',
'title' => 'WordPress Development and Customization',
'description' => 'Skilled in WordPress development, including theme customization and plugin development.',
]);
SkillTranslation::create([
'skill_id' => $wordpressSkill->id,
'locale' => 'fa',
'title' => 'توسعه و شخصی‌سازی وردپرس',
'description' => 'مهارت در توسعه وردپرس، شامل شخصی‌سازی قالب‌ها و توسعه افزونه‌ها.',
]);
// Version Control (Git, GitHub)
$gitSkill = Skill::create([
'about_me_id' => $aboutMe->id,
'image' => '/images/ux.png',
'percentage' => 80,
'link' => 'http://example.com/ux',
]);
SkillTranslation::create([
'skill_id' => $gitSkill->id,
'locale' => 'en',
'title' => 'Version Control (Git, GitHub)',
'description' => 'Experienced in version control using Git and GitHub for managing codebases and collaboration.',
]);
SkillTranslation::create([
'skill_id' => $gitSkill->id,
'locale' => 'fa',
'title' => 'کنترل نسخه (Git, GitHub)',
'description' => 'تجربه در کنترل نسخه با استفاده از Git و GitHub برای مدیریت کدها و همکاری تیمی.',
]);
foreach ($skills as $skillData) {
$aboutMe->skills()->create($skillData);
}
}
}

View File

@ -4,38 +4,51 @@
use Illuminate\Database\Seeder;
use App\Models\Website\ProjectCategory;
use App\Models\Website\ProjectCategoryTranslation;
class ProjectCategorySeeder extends Seeder
{
public function run()
{
$categories = [
['fa' => 'طراحی اپلیکیشن', 'en' => 'App Design'],
['fa' => 'فرانت اند', 'en' => 'Front End'],
['fa' => 'بک اند', 'en' => 'Backend'],
['fa' => 'طراحی وب', 'en' => 'Web Design'],
['fa' => 'طراحی گرافیک', 'en' => 'Graphic Design'],
[
'image' => 'category1.jpg',
'translations' => [
'title' => ['en' => 'App Design', 'fa' => 'طراحی اپلیکیشن'],
'description' => ['en' => 'Description for App Design', 'fa' => 'توضیحات طراحی اپلیکیشن'],
],
],
[
'image' => 'category2.jpg',
'translations' => [
'title' => ['en' => 'Front End', 'fa' => 'فرانت اند'],
'description' => ['en' => 'Description for Front End', 'fa' => 'توضیحات فرانت اند'],
],
],
[
'image' => 'category3.jpg',
'translations' => [
'title' => ['en' => 'Backend', 'fa' => 'بک اند'],
'description' => ['en' => 'Description for Backend', 'fa' => 'توضیحات بک اند'],
],
],
[
'image' => 'category4.jpg',
'translations' => [
'title' => ['en' => 'Web Design', 'fa' => 'طراحی وب'],
'description' => ['en' => 'Description for Web Design', 'fa' => 'توضیحات طراحی وب'],
],
],
[
'image' => 'category5.jpg',
'translations' => [
'title' => ['en' => 'Graphic Design', 'fa' => 'طراحی گرافیک'],
'description' => ['en' => 'Description for Graphic Design', 'fa' => 'توضیحات طراحی گرافیک'],
],
],
];
foreach ($categories as $key => $category) {
// ایجاد دسته‌بندی اصلی
$createdCategory = ProjectCategory::create(['image' => 'category' . ($key + 1) . '.jpg']);
// ایجاد ترجمه‌ها
ProjectCategoryTranslation::create([
'category_id' => $createdCategory->id,
'locale' => 'fa',
'title' => $category['fa'],
'description' => 'توضیحات ' . $category['fa'],
]);
ProjectCategoryTranslation::create([
'category_id' => $createdCategory->id,
'locale' => 'en',
'title' => $category['en'],
'description' => 'Description for ' . $category['en'],
]);
foreach ($categories as $category) {
ProjectCategory::create($category);
}
}
}

View File

@ -4,41 +4,32 @@
use Illuminate\Database\Seeder;
use App\Models\Website\Project;
use App\Models\Website\ProjectTranslation;
use App\Models\Website\ProjectCategory;
class ProjectSeeder extends Seeder
{
public function run()
{
// دریافت تمام دسته‌بندی‌ها
$categories = ProjectCategory::all();
foreach ($categories as $category) {
// ایجاد تعداد تصادفی پروژه برای هر دسته‌بندی
$projectCount = rand(5, 10);
for ($i = 1; $i <= $projectCount; $i++) {
// ایجاد پروژه جدید
$project = Project::create([
Project::create([
'category_id' => $category->id,
'image1' => 'project' . $i . '_image1.jpg',
'image2' => 'project' . $i . '_image2.jpg'
]);
// ایجاد ترجمه‌ها
ProjectTranslation::create([
'project_id' => $project->id,
'locale' => 'fa',
'title' => 'پروژه ' . $i . ' - ' . $category->translations()->where('locale', 'fa')->first()->title,
'description' => 'توضیحات پروژه ' . $i . ' برای دسته‌بندی ' . $category->translations()->where('locale', 'fa')->first()->title,
]);
ProjectTranslation::create([
'project_id' => $project->id,
'locale' => 'en',
'title' => 'Project ' . $i . ' - ' . $category->translations()->where('locale', 'en')->first()->title,
'description' => 'Description for project ' . $i . ' in category ' . $category->translations()->where('locale', 'en')->first()->title,
'image2' => 'project' . $i . '_image2.jpg',
'translations' => [
'title' => [
'en' => 'Project ' . $i . ' - ' . $category->getTranslation('title', 'en'),
'fa' => 'پروژه ' . $i . ' - ' . $category->getTranslation('title', 'fa'),
],
'description' => [
'en' => 'Description for project ' . $i . ' in category ' . $category->getTranslation('title', 'en'),
'fa' => 'توضیحات پروژه ' . $i . ' برای دسته‌بندی ' . $category->getTranslation('title', 'fa'),
],
],
]);
}
}

View File

@ -1,96 +1,58 @@
<?php
namespace Database\Seeders\website;
namespace Database\Seeders\Website;
use Illuminate\Database\Seeder;
use App\Models\Website\Service;
use App\Models\Website\ServiceTranslation;
class ServiceSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run()
{
// خدمات با ترجمه‌های فارسی
$services = [
[
'imageSrc' => '/images/ui-ux-vector.svg',
'translations' => [
[
'locale' => 'fa',
'title' => 'توسعه وب سفارشی',
'description' => 'ما برنامه‌های وب سفارشی را ایجاد می‌کنیم که نیازهای خاص کسب‌وکار شما را برآورده می‌سازد و از آخرین تکنولوژی‌ها برای عملکرد بهینه استفاده می‌کند.',
'title' => [
'en' => 'Senior Frontend Engineering',
'fa' => 'توسعه فرانت‌اند ارشد',
],
[
'locale' => 'en',
'title' => 'Custom Web Development',
'description' => 'We create tailored web applications that meet your unique business requirements, utilizing the latest technologies for optimal performance.',
'description' => [
'en' => 'Building ultra-fast SSR/Static apps, complex state management, and modern UI architectures focusing on core web vitals.',
'fa' => 'خلق SPAها و SSRهای بسیار سریع با Next.js، مدیریت استیت‌های پیچیده و بهینه‌سازی Core Web Vitals',
],
],
],
[
'imageSrc' => '/images/web-design-vector.svg',
'translations' => [
[
'locale' => 'fa',
'title' => 'توسعه فرانت‌اند',
'description' => 'خدمات توسعه فرانت‌اند ما بر روی ایجاد رابط‌های کاربری جذاب و پاسخگو تمرکز دارد که تجربه کاربری را در تمامی دستگاه‌ها بهبود می‌بخشد.',
'title' => [
'en' => 'Backend & API Design',
'fa' => 'معماری و توسعه بک‌اند',
],
[
'locale' => 'en',
'title' => 'Frontend Development',
'description' => 'Our frontend development services focus on crafting engaging, responsive user interfaces that enhance user experience across all devices.',
'description' => [
'en' => 'Designing secure, modular, and optimized RESTful APIs using Laravel and database optimization for scalable systems.',
'fa' => 'طراحی APIهای ماژولار، امن و بهینه با Laravel و مدیریت دیتابیس‌های مقیاس‌پذیر',
],
],
],
[
'imageSrc' => '/images/app-design-vector.svg',
'translations' => [
[
'locale' => 'fa',
'title' => 'توسعه بک‌اند',
'description' => 'ما راه‌حل‌های قابل اعتماد توسعه بک‌اند را ارائه می‌دهیم و معماری‌های سروری و APIهای قوی را برای اطمینان از عملکرد روان و ایمن برنامه ایجاد می‌کنیم.',
'title' => [
'en' => 'Code Refactoring & Modernization',
'fa' => 'بازطراحی و مدرن‌سازی کد',
],
[
'locale' => 'en',
'title' => 'Backend Development',
'description' => 'We provide reliable backend development solutions, building robust server-side architectures and APIs to ensure smooth and secure application functionality.',
],
],
],
[
'imageSrc' => '/images/graphic-design-vector.svg',
'translations' => [
[
'locale' => 'fa',
'title' => 'نگهداری و پشتیبانی وب‌سایت',
'description' => 'خدمات نگهداری و پشتیبانی مداوم ما وب‌سایت شما را به‌روز، امن و با بهترین عملکرد نگه می‌دارد تا شما بتوانید بر روی کسب‌وکار اصلی خود تمرکز کنید.',
],
[
'locale' => 'en',
'title' => 'Website Maintenance and Support',
'description' => 'Our ongoing maintenance and support services keep your website updated, secure, and performing at its best, allowing you to focus on your core business.',
'description' => [
'en' => 'Migrating legacy systems into clean, component-driven, and fully testable architectures to ensure long-term maintainability.',
'fa' => 'تبدیل کدهای قدیمی و مگالیتی به معماری‌های کامپوننت‌محور، تمیز و تست‌پذیر',
],
],
],
];
// ذخیره خدمات و ترجمه‌ها
foreach ($services as $serviceData) {
$service = Service::create([
'imageSrc' => $serviceData['imageSrc'],
]);
foreach ($serviceData['translations'] as $translation) {
ServiceTranslation::create([
'service_id' => $service->id,
'locale' => $translation['locale'],
'title' => $translation['title'],
'description' => $translation['description'],
]);
}
Service::create($serviceData);
}
}
}

View File

@ -22,8 +22,8 @@
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_STORE" value="array"/>
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="MAIL_MAILER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="QUEUE_CONNECTION" value="sync"/>

BIN
public/images/parsa2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
public/images/parsa21.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
public/images/parsa22.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

View File

@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ $documentationTitle }}</title>
<link rel="stylesheet" type="text/css" href="{{ l5_swagger_asset($documentation, 'swagger-ui.css') }}">
<link rel="icon" type="image/png" href="{{ l5_swagger_asset($documentation, 'favicon-32x32.png') }}" sizes="32x32"/>
<link rel="icon" type="image/png" href="{{ l5_swagger_asset($documentation, 'favicon-16x16.png') }}" sizes="16x16"/>
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body {
margin:0;
background: #fafafa;
}
</style>
@if(config('l5-swagger.defaults.ui.display.dark_mode'))
<style>
body#dark-mode,
#dark-mode .scheme-container {
background: #1b1b1b;
}
#dark-mode .scheme-container,
#dark-mode .opblock .opblock-section-header{
box-shadow: 0 1px 2px 0 rgba(255, 255, 255, 0.15);
}
#dark-mode .operation-filter-input,
#dark-mode .dialog-ux .modal-ux,
#dark-mode input[type=email],
#dark-mode input[type=file],
#dark-mode input[type=password],
#dark-mode input[type=search],
#dark-mode input[type=text],
#dark-mode textarea{
background: #343434;
color: #e7e7e7;
}
#dark-mode .title,
#dark-mode li,
#dark-mode p,
#dark-mode table,
#dark-mode label,
#dark-mode .opblock-tag,
#dark-mode .opblock .opblock-summary-operation-id,
#dark-mode .opblock .opblock-summary-path,
#dark-mode .opblock .opblock-summary-path__deprecated,
#dark-mode h1,
#dark-mode h2,
#dark-mode h3,
#dark-mode h4,
#dark-mode h5,
#dark-mode .btn,
#dark-mode .tab li,
#dark-mode .parameter__name,
#dark-mode .parameter__type,
#dark-mode .prop-format,
#dark-mode .loading-container .loading:after{
color: #e7e7e7;
}
#dark-mode .opblock-description-wrapper p,
#dark-mode .opblock-external-docs-wrapper p,
#dark-mode .opblock-title_normal p,
#dark-mode .response-col_status,
#dark-mode table thead tr td,
#dark-mode table thead tr th,
#dark-mode .response-col_links,
#dark-mode .swagger-ui{
color: wheat;
}
#dark-mode .parameter__extension,
#dark-mode .parameter__in,
#dark-mode .model-title{
color: #949494;
}
#dark-mode table thead tr td,
#dark-mode table thead tr th{
border-color: rgba(120,120,120,.2);
}
#dark-mode .opblock .opblock-section-header{
background: transparent;
}
#dark-mode .opblock.opblock-post{
background: rgba(73,204,144,.25);
}
#dark-mode .opblock.opblock-get{
background: rgba(97,175,254,.25);
}
#dark-mode .opblock.opblock-put{
background: rgba(252,161,48,.25);
}
#dark-mode .opblock.opblock-delete{
background: rgba(249,62,62,.25);
}
#dark-mode .loading-container .loading:before{
border-color: rgba(255,255,255,10%);
border-top-color: rgba(255,255,255,.6);
}
#dark-mode svg:not(:root){
fill: #e7e7e7;
}
#dark-mode .opblock-summary-description {
color: #fafafa;
}
</style>
@endif
</head>
<body @if(config('l5-swagger.defaults.ui.display.dark_mode')) id="dark-mode" @endif>
<div id="swagger-ui"></div>
<script src="{{ l5_swagger_asset($documentation, 'swagger-ui-bundle.js') }}"></script>
<script src="{{ l5_swagger_asset($documentation, 'swagger-ui-standalone-preset.js') }}"></script>
<script>
window.onload = function() {
const urls = [];
@foreach($urlsToDocs as $title => $url)
urls.push({name: "{{ $title }}", url: "{{ $url }}"});
@endforeach
// Build a system
const ui = SwaggerUIBundle({
dom_id: '#swagger-ui',
urls: urls,
"urls.primaryName": "{{ $documentationTitle }}",
operationsSorter: {!! isset($operationsSorter) ? '"' . $operationsSorter . '"' : 'null' !!},
configUrl: {!! isset($configUrl) ? '"' . $configUrl . '"' : 'null' !!},
validatorUrl: {!! isset($validatorUrl) ? '"' . $validatorUrl . '"' : 'null' !!},
oauth2RedirectUrl: "{{ route('l5-swagger.'.$documentation.'.oauth2_callback', [], $useAbsolutePath) }}",
requestInterceptor: function(request) {
request.headers['X-CSRF-TOKEN'] = '{{ csrf_token() }}';
return request;
},
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
docExpansion : "{!! config('l5-swagger.defaults.ui.display.doc_expansion', 'none') !!}",
deepLinking: true,
filter: {!! config('l5-swagger.defaults.ui.display.filter') ? 'true' : 'false' !!},
persistAuthorization: "{!! config('l5-swagger.defaults.ui.authorization.persist_authorization') ? 'true' : 'false' !!}",
})
window.ui = ui
@if(in_array('oauth2', array_column(config('l5-swagger.defaults.securityDefinitions.securitySchemes'), 'type')))
ui.initOAuth({
usePkceWithAuthorizationCodeGrant: "{!! (bool)config('l5-swagger.defaults.ui.authorization.oauth2.use_pkce_with_authorization_code_grant') !!}"
})
@endif
}
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -1,26 +1,18 @@
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Blog\CategoryController;
use App\Http\Controllers\Blog\HomeController;
use App\Http\Controllers\Blog\PostController;
use App\Http\Controllers\Blog\TagController;
// use App\Http\Controllers\SkillController;
use App\Http\Controllers\Website\ServiceController;
use App\Http\Controllers\Website\AboutMeController;
use App\Http\Controllers\Website\ProjectController;
use App\Http\Controllers\Api\Blog\CategoryController;
use App\Http\Controllers\Api\Blog\HomeController;
use App\Http\Controllers\Api\Blog\PostController;
use App\Http\Controllers\Api\Blog\TagController;
use App\Http\Controllers\Api\Website\AboutMeController;
use App\Http\Controllers\Api\Website\ProjectController;
use App\Http\Controllers\Api\Website\ServiceController;
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');
// Route::resource('projects', ProjectController::class);
Route::apiResource('services', ServiceController::class);
Route::prefix('website')->group(function () {
Route::middleware('throttle:60,1')->group(function () {
Route::prefix('website')->group(function () {
Route::get('about-me/{id}/translation/{locale}', [AboutMeController::class, 'getTranslation']);
Route::get('services/{locale}', [ServiceController::class, 'index']); // مسیر جدید برای لیست خدمات با ترجمه
Route::get('services/{locale}', [ServiceController::class, 'index']);
Route::get('services/{id}/translation/{locale}', [ServiceController::class, 'showTranslation']);
Route::group(['prefix' => 'projects'], function () {
Route::get('/translation/{locale}', [ProjectController::class, 'index']);
@ -29,17 +21,13 @@
Route::put('/{id}', [ProjectController::class, 'update']);
Route::delete('/{id}', [ProjectController::class, 'destroy']);
});
});
});
// مسیرهای بلاگ
Route::prefix('blog')->group(function () {
// پست‌ها
Route::prefix('blog')->group(function () {
Route::apiResource('posts', PostController::class);
// دسته‌بندی‌ها
Route::apiResource('categories', CategoryController::class);
// تگ‌ها
Route::apiResource('tags', TagController::class);
// مسیر Homepage
Route::get('homepage', [HomeController::class, 'index']);
Route::get('categories/{slug}/posts', [CategoryController::class, 'getCategoryWithPosts']);
});
});

20
seed-about.php Normal file
View File

@ -0,0 +1,20 @@
<?php
require __DIR__ . '/vendor/autoload.php';
$app = require_once __DIR__ . '/bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
use App\Models\Website\AboutMe;
use App\Models\Website\AboutMeTranslation;
echo "Creating AboutMe record...\n";
$about = AboutMe::create(["id" => 1, "image" => "", "resume" => ""]);
echo "Created AboutMe id={$about->id}\n";
AboutMeTranslation::create(["about_me_id" => 1, "locale" => "en", "title" => "About Me", "description" => "Welcome to my portfolio"]);
echo "Created EN translation\n";
AboutMeTranslation::create(["about_me_id" => 1, "locale" => "fa", "title" => "درباره من", "description" => "به پورتفولیوی من خوش آمدید"]);
echo "Created FA translation\n";
echo "Done!\n";

View File

@ -0,0 +1,181 @@
{
"openapi": "3.0.0",
"info": {
"title": "Parsa Aghayi Portfolio API",
"description": "REST API for Parsa Aghayi's personal portfolio and blog",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:8000/api",
"description": "Development server"
},
{
"url": "https://backend.parsaaghayi.ir/api",
"description": "Production server"
}
],
"components": {
"schemas": {
"Skill": {
"properties": {
"id": {
"type": "integer"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"image": {
"type": "string"
},
"link": {
"type": "string",
"nullable": true
},
"percentage": {
"type": "integer"
}
},
"type": "object"
},
"AboutMe": {
"properties": {
"id": {
"type": "integer"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"image": {
"type": "string"
},
"skills": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Skill"
}
}
},
"type": "object"
},
"Service": {
"properties": {
"id": {
"type": "integer"
},
"imageSrc": {
"type": "string"
},
"altText": {
"type": "string"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"created_at": {
"type": "string",
"format": "datetime"
},
"updated_at": {
"type": "string",
"format": "datetime"
}
},
"type": "object"
},
"Project": {
"properties": {
"id": {
"type": "integer"
},
"title": {
"type": "string"
},
"category": {
"type": "string",
"nullable": true
},
"images": {
"type": "array",
"items": {
"type": "object"
}
},
"created_at": {
"type": "string",
"format": "datetime"
},
"updated_at": {
"type": "string",
"format": "datetime"
}
},
"type": "object"
},
"Category": {
"properties": {
"id": {
"type": "integer"
},
"title": {
"type": "string"
},
"image": {
"type": "string"
}
},
"type": "object"
},
"Post": {
"properties": {
"id": {
"type": "integer"
},
"title": {
"type": "string"
},
"body": {
"type": "string"
},
"excerpt": {
"type": "string"
},
"slug": {
"type": "string"
},
"featured_image": {
"type": "string",
"nullable": true
},
"author": {
"type": "object"
},
"category": {
"type": "object"
},
"tags": {
"type": "array",
"items": {
"type": "object"
}
},
"published_at": {
"type": "string",
"format": "datetime",
"nullable": true
}
},
"type": "object"
}
}
}
}

0
storage/app/.gitignore vendored Normal file → Executable file
View File

0
storage/app/private/.gitignore vendored Normal file → Executable file
View File

0
storage/app/public/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/cache/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/cache/data/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/sessions/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/testing/.gitignore vendored Normal file → Executable file
View File

0
storage/framework/views/.gitignore vendored Normal file → Executable file
View File

0
storage/logs/.gitignore vendored Normal file → Executable file
View File

Some files were not shown because too many files have changed in this diff Show More