Laravel CRUD with Datatables and Dropzone.js File Upload

Laravel Framework এ Dropzone.js File Upload-এর মতো লাইব্রেরি ব্যবহার করা যেমন কিছু সুবিধা আছে, আবার Dropzone.js File Upload-এর মতো লাইব্রেরি এর ব্যবহার করতে গিয়ে কিছু জটিলতারও মুখোমুখী হতে হয়। । এখানে লারাভেলে file upload system এ Dropzone.js সুবিধা এবং জটিলতার একটি ওভারভিউ রয়েছে:

সুবিধা সমূহঃ

১. User-Friendly Interface: Dropzone.js File Upload লাইব্রেরি ফাইল আপলোড করার জন্য একটি user-friendly ইন্টারফেস প্রদান করে। যাতে ইউজাররা সহজভাবে ফাইলগুলিকে drag and drop করতে পারেন অথবা চাইলে ম্যানুয়ালি আপলোডের জন্য প্রয়োজনীয় একাধিক ফাইল সিলেক্ট করতে পারেন৷

২. Asynchronous Uploads: Dropzone.js File Upload system, Asynchronous ফাইল আপলোডের সুযোগ দেয়। এর মানে হল, যখন কোনো ব্যবহারকারী ফাইল আপলোড করে, তখন ব্যবহারকারী আপলোড শেষ হওয়ার জন্য অপেক্ষা না করে চাইলে বাকি পৃষ্ঠার সাথে ইন্টারঅ্যাক্ট চালিয়ে যেতে পারে৷

৩. Client-Side File Validation: Dropzone.js ফাইল আপলোড করার আগে file types এবং sizes ক্লায়েন্ট-সাইড ভ্যালিডেশন করতে পারে, ইউজাররা যদি কোনো invalid ফাইল আপলোড করার চেষ্টা করে তবে তাদের ইমমেডিয়েট ফিডব্যাক প্রদান করে।

৪. Integration with Laravel: Dropzone.js File Upload লাইব্রেরি ইন্টিগ্রেশন এর ফলে লারাভেল file uploads এবং storage হ্যান্ডেল করা তুলনামূলকভাবে সহজ করে তোলে। আপলোড করা ফাইলগুলিকে নিরাপদে প্রসেস এবং সংরক্ষণ করতে আপনি লারাভেলের বিল্টইন ফীচার গুলি ব্যবহার করতে পারেন৷

৫. Scalability: Laravel Framework এ একটি well-implemented file upload system আপনাকে প্রচুর ফাইল এবং ইউজারদের হ্যান্ডেল করার সুযোগ দেয়, এটিকে বিস্তৃত (wide range) অ্যাপ্লিকেশনের জন্য উপযুক্ত করে তোলে।

জটিলতা সমূহ:

১. Setup and Integration: Dropzone.js সেট আপ করতে এবং এটিকে লারাভেলের সাথে ব্যবহার করতে কিছু প্রাথমিক কনফিগারেশন এবং জাভাস্ক্রিপ্ট কোডিং প্রয়োজন। আপনাকে নিশ্চিত করতে হবে যে JavaScript লাইব্রেরি সঠিকভাবে লোড হয়েছে এবং এটি আপনার Laravel ব্যাকএন্ডের সাথে নির্বিঘ্নে ইন্টারঅ্যাক্ট করে।

২. Security: File uploads যদি সঠিকভাবে হ্যান্ডেল না করা হয় তবে security vulnerabilities হতে পারে। আপনাকে অবশ্যই file types যাচাই করতে হবে, ফাইলের নামগুলো স্যানিটাইজ করতে হবে এবং সম্ভাব্য exploits এর বিরুদ্ধে রক্ষা করতে হবে, যেমন file inclusion attacks.

৩. Storage: আপলোড করা ফাইলগুলি কোথায় এবং কীভাবে সংরক্ষণ করতে হবে তা নির্ধারণ করা জটিল হতে পারে। Laravel locally ফাইল সংরক্ষণের পাশাপাশি আরো অনেক বিকল্প প্রদান করে, যেমন Amazon S3 এর মতো ক্লাউড স্টোরেজ পরিষেবাগুলিতে, বা লোকাল এবং ক্লাউড উভয়ের সংমিশ্রণ ব্যবহার করেও ফাইল সংরক্ষণ করা যায় ।

৪. Validation: যদিও Dropzone.js client-side validation করতে পারে, তারপরও আপলোড করা ফাইলগুলি আপনার অ্যাপ্লিকেশনের প্রয়োজনীয়তাগুলি পূরণ করে তা নিশ্চিত করতে আপনার server-side validation সঞ্চালন করা উচিত। এর মধ্যে file types, sizes, এবং ফাইলগুলি malicious নয় তা নিশ্চিত করা অন্তর্ভুক্ত।

Mastering Laravel with ReactJS Course

Create a Laravel CRUD with Datatables and Dropzone.js File Upload

তো চলুন দেখা যাক , কিভাবে Laravel Framework এ একটি CRUD Operation এ Dropzone.js File Upload System ব্যবহার করা যায় এবং একই সাথে আমরা এখানে datatable লাইব্রেরি টি ব্যবহার করব , যেন আমাদের Search এবং pagination এর কাজ গুলো একই সাথে হয়ে যায়।

Mastering Laravel with ReactJS Course

Create Routes

প্রথমে আপনার Routes/web.php ফাইলে নিম্নোক্ত route গুলো ডিফাইন করে দিন। যা আমাদের CRUD অপারেশন এ বিভিন্ন জায়গায় ব্যবহার করব।

use App\Http\Controllers\ItemController;

Route::resource('item', ItemController::class); 
Route::post('uploads', [ItemController::class,'uploads'])->name('uploads');
Route::post('image/delete',[ItemController::class,'fileDestroy']);
Route::delete('/items/{image}', [ItemController::class,'delete'])->name('image.delete');

    ব্যাখ্যা:

  • লাইন নং ৩ এ আমরা সাধারণ একটি CRUD Operation পরিচালনা করার জন্য আমাদের ItemController নামের Resource Controller এর জন্য resource route টি ডিফাইন করি।
  • লাইন নম্বর ৪ এ uploads route দিয়ে Dropzone.js এর আপলোডকৃত ফাইলকে আমাদের পছন্দ মতো একটি ইউনিক নাম দিয়ে ফোল্ডারে মুভ করতে পারি সেই ব্যবস্থা করি।
  • লাইন নম্বর ৫ এ image/delete route দিয়ে Dropzone.js এর আপলোডকৃত ফাইলকে যেন ফাইনাল save এর পূর্বে আমরা রিমুভ করতে পারি তার জন্য ডিফাইন করি।
  • এবং লাইন নম্বর ৬ এ /items/{image} route দিয়ে আমরা যখন কোনো আইটেম এডিট করব , তখন যেন যেকোনো ফাইলকে রিমুভ করতে পারি তারজন্য ডিফাইন করি।

Create a Custom Request Class

Laravel Framework এ custom request class হল এমন একটি class যা লারাভেলের বেস Illuminate\Http\Request class কে extend করে এবং আপনাকে নির্দিষ্ট HTTP Request এর জন্য custom validation rules, authorization logic এবং data processing method গুলো ডিফাইন করতে দেয়। আপনার অ্যাপ্লিকেশনে incoming HTTP requests গুলির সাথে সম্পর্কিত validation এবং processing logic কে encapsulate এবং centralize করতে আপনার উচিত custom request classes গুলি ব্যবহার করা । আমাদের কেন Laravel Custom Request class ব্যবহার করা উচিত, তার কিছু কারণ বর্ণনা করা হলো:

১.Separation of Concerns:লারাভেল আপনার application এর প্রত্যেকটি কাজ আলাদা (Separation) ভাবে করার ব্যাপারে প্রমোট করে। custom request class ব্যবহার করে আপনি আপনার validation logic কে আপনার controller method গুলো থেকে আলাদা রাখতে পারবেন, এতে আপনার কোড আরও organized ও রক্ষণাবেক্ষণযোগ্য হবে।

২.Reusability: custom request classes তৈরি করে, আপনি একাধিক controller method গুলিকে বা এমনকি আপনার অ্যাপ্লিকেশনের বিভিন্ন অংশে একই validation এবং processing logic পুনরায় ব্যবহার করতে পারেন। এটি code duplication হ্রাস করে এবং একটি DRY (Don’t Repeat Yourself) পদ্ধতির প্রচার করে।

3. Cleaner Controller Code: লারাভেলের Controllers গুলোতে আমাদেরকে request validation নিয়ে কাজ করার পরিবর্তে অ্যাপ্লিকেশনের business logic হ্যান্ডলিং এর দিকে বেশি মনোনিবেশ করা উচিত। custom request class গুলো ব্যবহার করে validation এবং request processing code কে controller এর বাইরে নিয়ে যায়, যার ফলে cleaner এবং আরও readable controller methods হয়।

৪. Easy Testing: Custom request class গুলো আপনার validation এবং processing logic এর জন্য unit test গুলো লিখতে সহজ করে তোলে। validation rules এবং processing logic গুলো যেন প্রত্যাশিতভাবে কাজ করে তা নিশ্চিত করতে আপনি বিশেষভাবে আপনার custom request class এর জন্য test case গুলো তৈরি করতে পারেন।

এবার নিম্নোক্ত আর্টিসান কমান্ড দুটির মাধ্যমে দুটি Custom Request Class তৈরি করে ফেলুন :

php artisan make:request ItemStoreRequest
php artisan make:request ItemUpdateRequest

Update ItemStoreRequest.php File

এবার আপনার সদ্য তৈরি হওয়া app/Http/Requests/ItemStoreRequest.php ফাইলকে নিচের মতো করে প্রয়োজনীয় Validation Rules লিখে আপডেট করে নিন :

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ItemStoreRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
     */
    public function rules(): array
    {
        return [
            'name' => 'required|string|max:100',
            'sku' => 'required|string|max:5|unique:items,sku',
            'price' => 'required|numeric|between:0,9999999999.99'
        ];
    }
}

Update ItemUpdateRequest.php File

একইভাবে আপনার সদ্য তৈরি হওয়া app/Http/Requests/ItemUpdateRequest.php ফাইলকে নিচের মতো করে প্রয়োজনীয় Validation Rules লিখে আপডেট করে নিন :

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ItemUpdateRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
     */
    public function rules(): array
    {
        return [
            'name' => 'required|string|max:100',
            'sku' => 'required|string|max:5|unique:items,sku,'.request()->segment(2),
            'price' => 'required|numeric|between:0,9999999999.99'
        ];
    }
}

ব্যাখ্যা:

১. এখানে লাইন নং ১৩ তে সব ধরণের Request এর উপর authorization rules গুলো সক্রিয় করার জন্য আমরা ItemStoreRequest.php এবং ItemUpdateRequest.php ফাইল দুটির authorize() function এর return কে true রিটার্ন করি।

২. লাইন নম্বর ২৪ – ২৮ এ আমরা প্রয়োজনীয় rules গুলো প্রয়োগ করি।

Create Resource Controller

মোটামুটি আমরা আমাদের ফাইল আপলোড এবং আপলোডেড ফাইলকে আপডেট করা কালীন প্রয়োজনীয় ভ্যালিডেশন এর কাজ সম্পর্ন করেছি। এখন আমরা নিম্নোক্ত আর্টিসান কমান্ড এর মাধ্যমে আমাদের ItemController.php Resource Controller ফাইলটি তৈরি করব :

php artisan make:controller ItemController --resource

এবার আপনার সদ্য তৈরি হওয়া app/Http/Controllers/ItemController.php ফাইলকে অতিরিক্ত delete, uploads এবং fileDestroy মেথড যুক্ত করে এবং আমাদের তৈরি Models এবং Custom Request Class Use করে নিচের মতো করে আপডেট করে নিন :

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Item;
use App\Models\ItemImage;
use App\Http\Requests\ItemStoreRequest;
use App\Http\Requests\ItemUpdateRequest;

class ItemController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        return response()->view('itemindex', [
            'items' => Item::orderBy('updated_at', 'desc')->get(),
        ]);
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        return view('itemcreate');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(ItemStoreRequest $request)
    {
        $created = Item::create(['name' => $request->name, 'sku' => $request->sku, 'price' => $request->price]);
        foreach ($request->input('document', []) as $file) {
            //your file to be uploaded insert to database
            ItemImage::create(['item_id' => $created->id, 'image' => $file]);
        }
        if ($created) { // inserted success
            return redirect()->route('item.index')
                ->withSuccess('Created successfully...!');
        }
        return redirect()
            ->back()
            ->withInput()
            ->with('error', 'fails not created..!');
    }

    /**
     * Display the specified resource.
     */
    public function show(Item $item)
    {
        $item->with('getImagesHasMany')->where('id',$item->id)->first();
        return view('itemshow',compact('item'));        
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit($id = "")
    {
        $item = Item::with('getImagesHasMany')->where('id', $id)->first();
        return view('itemedit', compact('item'));
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(ItemUpdateRequest $request, Item $item)
    {
        $item->update($request->all());
        if ($request->has('document') && sizeof($request->get('document')) > 0) {

            $media = ItemImage::where('item_id', $item->id)->pluck('image')->toArray();

            foreach ($request->input('document', []) as $file) {
                if (count($media) === 0 || !in_array($file, $media)) {
                    ItemImage::create(['item_id' => $item->id, 'image' => $file]);
                }
            }
        }
        return redirect()->route('item.index')
            ->withSuccess('Updated Successfully...!');
    }

    /**
     * Remove the specified resource from storage and table.
     */
    public function destroy(Item $item)
    {
        // delete item images from storage folder and item image table
        $itemImages = ItemImage::where('item_id', $item->id)->get();
        if (isset($itemImages) && !empty($itemImages)) {
            foreach ($itemImages as $imgVal) {                
                //$path = storage_path('app/public/images/') . $imgVal['image'];
                //$path = public_path() . '/images/' . $request->filename;
                $path = public_path() . '/images/' . $imgVal['image'];

                if (file_exists($path)) {
                    ItemImage::where('image', $imgVal['image'])->delete();
                    unlink($path);
                }
            } // Loops Ends
        }
        $item->delete();
        return redirect()->route('item.index')
            ->withSuccess('Deleted Successfully.');
    }

    public function delete($image_id)
    {
        // Find the image by its ID
        $image = ItemImage::find($image_id);

        // Check if the image exists
        if (!$image) {
            return back()->with('error', 'Image not found.');
        }

        // Delete the image from the database
        $image->delete();

        // Delete the image file from the storage
        // You should adjust the path and storage driver according to your configuration

        $path = public_path() . '/images/' . $image['image'];

                if (file_exists($path)) {
                    ItemImage::where('image', $image['image'])->delete();
                    unlink($path);
                }

        return redirect()->back()->with('success', 'Image deleted successfully.');
    }


    /**
     * Upload the specified resource to storage.
     */
    public function uploads(Request $request)
    {
        // $path = storage_path('tmp/uploads');
        //$path = storage_path('app/public/images');
        $path = public_path('images');
        !file_exists($path) && mkdir($path, 0777, true);
        $file = $request->file('file');
        //$name = uniqid() . '_' . trim($file->getClientOriginalName());
        $name = $file->getClientOriginalName();
        $file->move($path, $name);
        return response()->json([
            'name' => $name,
            'original_name' => $file->getClientOriginalName(),
        ]);
    }

    /**
     * Remove the specified resource from storage.
     */
    public function fileDestroy(Request $request)
    {
        //$path = storage_path('app/public/images/') . $request->filename;
        $path = public_path() . '/images/' . $request->filename;
        if (file_exists($path)) {
            ItemImage::where('image', $request->filename)->delete();
            unlink($path);
        }
        return $request->filename;
    }
}

ব্যাখ্যা:

১. প্রথমে আমরা লাইন নং ৬-৯ এ আমরা আমাদের তৈরি Model এবং Custom Request Class গুলো ইম্পোর্ট করি।

২. লাইন নম্বর ১৬ – ২১ এ আমরা index Method এর মাধ্যমে আমাদের Item গুলোকে datatables ব্যবহার করে প্রদর্শনের জন্য itemindex.blade.php এ পাঠাই।

৩. লাইন নম্বর ২৬ – ২৯ এ আমরা নতুন Item এবং তার ছবি গুলো dropzone.js ব্যবহার করে ডাটাবেসে Insert করার জন্য itemcreate.blade.php ফাইল কে কল করি।

৪. ৪. লাইন নম্বর ৩৪ – ৪৯ এ store Method এর মাধ্যমে আমরা আমাদের itemcreate.blade.php ফাইলের মাধ্যমে সাবমিট কৃত Item কে ডেটাবেসে সংরক্ষণের কাজ করেছি।

৫. লাইন নম্বর ৫৪-৫৮ এ show Method এ itemshow.blade.php ফাইলের মাধ্যমে আমরা একটি নির্দিষ্ট Item এর বিস্তারিত দেখানোর ব্যবস্থা করেছি।

৬. লাইন নম্বর ৬৩-৬৭ এ edit Method র মাধ্যমে আমরা একটি নির্দিষ্ট Item কে edit করার সুযোগ দানের জন্য itemedit.blade.php ফাইলে উক্ত Item এর বিস্তারিত পাঠিয়েছি।

৭. লাইন নম্বর ৭২-৮৭ তে update Method এ blade ফাইল itemedit.blade.php ফাইলের মাধ্যমে আপডেটকৃত ডেটা গুলোই ডেটাবেসে আপডেটের কাজ করেছি।

৮. লাইন নম্বর ৯২-১১১ তে destroy Method এর মাধ্যমে একটি নির্দিষ্ট Item এবং Image গুলোকে Delete এর কাজ করেছি।

৯. লাইন নম্বর ১১৩-১৩৭ এ delete Method এর মাধ্যমে কোনো একটি নির্দিষ্ট Item কে edit করা কালীন উক্ত Item এর অনেকগুলো ছবির মধ্যে এক বা একাধিক ছবিকে যেন রিমুভ করা যায় , সেই ব্যবস্থা করেছি।

১০. লাইন নম্বর ১৪৩-১৫৭ এ uploads Method দ্বারা Item Insert এবং Item Edit করা কালীন dropzone.js কর্তৃক uploaded ফাইল গুলোকে আমাদের পছন্দের ফোল্ডারে মুভ করানোর কাজ করেছি।

১১. লাইন নম্বর ১৬২-১৭১ এ fileDestroy Method দ্বারা Item Insert এবং Item Edit করা কালীন dropzone.js দ্বারা অটো আপ্লোডেড কৃত ফাইলকে চূড়ান্ত সংরক্ষণ বা আপডেট এর পূর্বে চাইলে যেন রিমুভ করা যায় , তার ব্যবস্থা করেছি।

Mastering Laravel with ReactJS Course

Add Assets To Your Public Folder

এবার আপনার প্রজেক্টের public ফোল্ডারের এর css ফোল্ডারের মধ্যে boostrap.min.css, datatables.min.css, dropzone.min.css এই ফাইল গুলোকে এবং js ফোল্ডারের মধ্যে boostrap.min.js,datatables.min.js, dropzone.min.js,jquery-3.6.4.min.js এই ফাইল গুলোকে সংশ্লিষ্ট সাইট থেকে ডাউনলোড করে রাখুন।

Create Blade Files

এবার আমরা আমাদের প্রয়োজনীয় সবগুলো blade File গুলোকে একে একে তৈরি করব। চলুন শুরু করা যাক :

Create itemindex.blade.php File

এই blade file টি দিয়ে আমরা আমাদের Item গুলোকে Datatable এর মাধ্যমে Tabulize আকারে প্রদর্শন করব। এবং এখন থেকে যেন প্রতিটি অ্যাকশন যেমন। নতুন Item তৈরি , আপডেট , এবং ডিলিট এর কাজ করা যায় , সেই সুবিধা গুলো রাখব।

নিচের কোড গুলো দিয়ে itemindex.blade.php ফাইল টি তৈরি করুন :

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="_token" content="{{csrf_token()}}" />
    <title>Item : List</title>

    <!-- Fonts -->
    <link rel="preconnect" href="https://fonts.bunny.net">
    <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet" />

    <!-- Styles -->
    <link href="{{ asset('css/bootstrap.min.css') }}" rel="stylesheet">
    <link href="{{ asset('css/datatables.min.css') }}" rel="stylesheet">
    <!-- Styles -->
   
</head>

<body class="antialiased">
    <div class="container-lg mx-auto py-4 px-lg-5">
    <div class="mx-auto p-4 p-lg-5">
        <div class="d-flex justify-content-center fw-bold h3 text-dark">
            Item List
        </div>
    </div>
    <div class="container-lg mx-auto">
        <div class="bg-white shadow-sm rounded-lg">
            <div class="px-4 py-4 bg-white border-bottom border-secondary">
                <a href="{{ route('item.create') }}"
                    class="btn btn-secondary btn-sm fw-bold text-uppercase mb-2">Add New Product</a>

                @if ($message = Session::get('success'))
                    <div class="bg-success border-top border-4 border-success text-white px-4 py-3 mb-3 shadow"
                        role="alert">
                        <div class="flex">
                            <div>
                                <p class="text-sm" style="color:cornsilk">{{ $message }}</p>
                            </div>
                        </div>
                    </div>
                @endif
                <table id="datatable_tbl" class="display" style="width:100%">
                    <thead>
                        <tr>
                            <th>Name</th>
                            <th>SKU</th>
                            <th>Price</th>
                            <th>Action</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach ($items as $item)
                            <tr>
                                <td>{{ $item->name }}</td>
                                <td>{{ $item->sku }}</td>
                                <td>{{ $item->price }}</td>
                                <td>
                                    <form action="{{ route('item.destroy', $item->id) }}" method="POST">
                                        <a class="btn btn-info btn-sm"
                                            href="{{ route('item.show', $item->id) }}">Show</a>
                                        <a class="btn btn-primary btn-sm"
                                            href="{{ route('item.edit', $item->id) }}">Edit</a>
                                        @csrf
                                        @method('DELETE')
                                        <button type="submit" class="btn btn-danger btn-sm"
                                            onclick="return confirm('Are you sure you want to delete this ?');">Delete</button>
                                    </form>
                                </td>
                            </tr>
                        @endforeach
                    </tbody>
                </table>
            </div>
        </div>
    </div>
    </div>
</div>
    <script type='text/javascript' src="{{ asset('js/jquery-3.6.4.min.js') }}"></script>
    <script type='text/javascript' src="{{ asset('js/datatables.min.js') }}"></script>
    <script type='text/javascript' src="{{ asset('js/bootstrap.min.js') }}"></script>
    <script type="text/javascript">
        $(document).ready(function() {
            $('#datatable_tbl').DataTable();
        });
    </script>
</body>

</html>

Mastering Laravel with ReactJS Course

Create itemcreate.blade.php File

এই blade file টি দিয়ে আমরা নতুন Item তৈরির কাজ করব। এবং একইসাথে dropzone.js ব্যবহার করব , যেন একাধিক ফাইলকে খুব সহজে drag and drop করে আপলোড করা যায়

নিচের কোড গুলো দিয়ে itemcreate.blade.php ফাইল টি তৈরি করুন :

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="_token" content="{{csrf_token()}}" />
    <title>Item : Create</title>

    <!-- Fonts -->
    <link rel="preconnect" href="https://fonts.bunny.net">
    <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet" />

    <!-- KEY : DROPZONE starts -->
    <!-- Styles -->
    <link href="{{ asset('css/bootstrap.min.css') }}" rel="stylesheet">
    <link href="{{ asset('css/dropzone.min.css') }}" rel="stylesheet">
    <!-- KEY : DROPZONE ends -->

</head>

<body class="antialiased">
    <div class="container-lg mx-auto py-4 px-lg-5">
        <div>
        <div class="d-flex justify-content-center font-weight-bold h3 text-dark">
            Item Create
        </div>
    </div>
    <div class="py-12">
        <div class="container-lg mx-auto px-sm-5 px-lg-7">
            <div class="bg-white overflow-hidden shadow-lg rounded px-4 py-4">
                <a href="{{ route('item.index') }}" class="btn btn-light">
                    Back
                </a>

                @if ($message = Session::get('error'))
                    <div class="alert alert-success rounded-0 border-0 border-top border-success"
                        role="alert">
                        <div class="d-flex">
                            <div>
                                <p class="text-sm text-danger">{{ $message }}</p>
                            </div>
                        </div>
                    </div>
                @endif
                <form action="{{ route('item.store') }}" name="form-create" id="form-create" method="POST"
                    enctype="multipart/form-data">
                    @csrf
                    <div class="mb-4">
                        <label for="name" class="mb-2 font-weight-bold text-muted small">Name<span
                                class="text-danger"> *
                            </span></label>
                        <input type="text" name="name" class="form-control" placeholder="Enter Name"
                            maxlength="50" value="{{ old('name') }}">
                        @error('name')
                            <span class="text-danger">{{ $message }}
                            </span>
                        @enderror
                    </div>

                    <div class="mb-4">
                        <label for="sku" class="mb-2 font-weight-bold text-muted small">Sku<span
                                class="text-danger"> *
                            </span></label>
                        <input type="text" name="sku" class="form-control" placeholder="Enter SKU" maxlength="5"
                            value="{{ old('sku') }}">
                        @error('sku')
                            <span class="text-danger">{{ $message }}
                            </span>
                        @enderror
                    </div>

                    <div class="mb-4">
                        <label for="price" class="mb-2 font-weight-bold text-muted small">Price<span
                                class="text-danger"> *
                            </span></label>
                        <input type="text" name="price" id="price" class="form-control"
                            placeholder="Enter Price" maxlength="50" value="{{ old('price') }}">
                        @error('price')
                            <span class="text-danger">{{ $message }}
                            </span>
                        @enderror
                    </div>
                    <!-- KEY : DROPZONE starts -->
                    <div class="form-group">
                        <label for="document">Documents</label>
                        <div class="needsclick dropzone" id="document-dropzone">
                        </div>
                        <div>
                            <button type="submit" id="submit-all" class="btn btn-primary btn-sm fw-bold text-uppercase mt-2">
                                Save
                            </button>
                        </div>
                    </div>
                    <!-- KEY : DROPZONE ends -->
                </form>
            </div>
        </div>
    </div>
    </div>
    <!-- KEY : DROPZONE starts -->
    <script type='text/javascript' src="{{ asset('js/jquery-3.6.4.min.js') }}"></script>
    <script type='text/javascript' src="{{ asset('js/bootstrap.min.js') }}"></script>
    <script type='text/javascript' src="{{ asset('js/dropzone.min.js') }}"></script>

    <script type="text/javascript">
        var uploadedDocumentMap = {}        
        var minFiles = 1; // minimum file must be to upload
        var maxFiles = 5; // maximum file allows to upload
        var myDropzone = Dropzone.options.documentDropzone = {
            url: "{{ route('uploads') }}",
            minFiles: minFiles,
            maxFiles: maxFiles,
            autoProcessQueue: true,
            maxFilesize: 5, // MB
            addRemoveLinks: true,
            acceptedFiles: ".jpeg,.jpg,.png",
            timeout: 5000,
            headers: {
                'X-CSRF-TOKEN': "{{ csrf_token() }}"
            },
            renameFile: function(file) {
                var dt = new Date();
                var time = dt.getTime();
               return time+file.name;
            },
            success: function(file, response) {
                console.log('success file');
                console.log(file);
                console.log(response);
                $('form').append('<input type="hidden" name="document[]" value="' + response.name + '">')
                uploadedDocumentMap[file.name] = response.name
            },
            removedfile: function(file) {
                console.log('remove file');
                console.log(file);                                
                // remove uploaded file from table and storage folder starts
                var filename = ''
                if (file.hasOwnProperty('upload')) {
                    filename = file.upload.filename;
                }else{
                    filename = file.name;
                }
                $.ajax({
                    type: 'POST',
                    url: "{{ url('image/delete') }}",
                    headers: {
                        'X-CSRF-TOKEN': "{{ csrf_token() }}"
                    },
                    data: {
                        filename: filename,                        
                    },
                    sucess: function(data) {
                        console.log('removed success: ' + data);
                    }
                });

                // remove file name from uploadedDocumentMap object
                Reflect.deleteProperty(uploadedDocumentMap, file.name);
                
                file.previewElement.remove();
                // remove uploaded file from table and storage folder ends
                // additional delete from multiple hidden files
                $('form').find('input[name="document[]"][value="' + filename + '"]').remove()
            },
            maxfilesexceeded: function(file) {
                //this.removeAllFiles();
                //this.addFile(file);
                //myDropZone.removeFile(file);
            },
            init: function() {
                console.log('init calls');
                // maxfiles files limit upload validation starts
                this.on("maxfilesexceeded", function(file) { // Maximum file upload validations                   
                    alert("Maximum " + maxFiles + " files are allowed to upload...!");
                    return false;
                });
                // maximum files limit upload validation ends
                
                // minimum files limit upload validation starts
                var submitButton = document.querySelector("#submit-all");
                myDropzone = this;
                submitButton.addEventListener("click", function(e) {
                    e.preventDefault();
                    var imagelength = Object.keys(uploadedDocumentMap).length;
                    if(imagelength < minFiles ){
                        alert("Minimum "+minFiles+" file needs to upload...!");
                        return false;
                    }else{
                        $('#form-create').submit();
                    }
                    /*Dropzone.forElement(".dropzone").options.autoProcessQueue = false;
                    if (myDropzone.getQueuedFiles().length >= minFiles) {
                        //myDropzone.processQueue();
                        Dropzone.forElement(".dropzone").options.autoProcessQueue = true;                        
                        Dropzone.forElement(".dropzone").processQueue();
                        $('#form-create').submit();
                    } else { // Minimum file upload validations
                        Dropzone.forElement(".dropzone").options.autoProcessQueue = false;
                        alert("Minimum "+minFiles+" file needs to upload...!");
                        return false;
                    }*/
                });
                // minimum files limit upload validation ends
            },
            error: function(file, response) {
                console.log('error file')
                console.log(file)
                console.log(response)
                $(file.previewElement).remove(); // removed files if validation fails
                return false;
            }
        }

    </script>
    <!-- KEY : DROPZONE ends -->
</body>

</html>

Create itemedit.blade.php File

এই blade file টি দিয়ে আমরা বিদ্যমান Item গুলোকে যেন edit করা যায় , সেই ব্যবস্থা করব। এবং একইসাথে dropzone.js ব্যবহার করব , যেন নতুন করে একাধিক ফাইলকে খুব সহজে drag and drop করে আপলোড করা যায়। সেই সাথে বিদ্যমান বা ইতিমধ্যে আপলোডকৃত ফাইলকে যেন রিমুভ করা যায় তার ব্যবস্থা করব

নিচের কোড গুলো দিয়ে itemedit.blade.php ফাইল টি তৈরি করুন :

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="_token" content="{{ csrf_token() }}" />
    <title>Item : Edit</title>

    <!-- Fonts -->
    <link rel="preconnect" href="https://fonts.bunny.net">
    <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet" />

    <!-- KEY : DROPZONE starts -->
    <!-- Styles -->
    <link href="{{ asset('css/bootstrap.min.css') }}" rel="stylesheet">
    <link href="{{ asset('css/dropzone.min.css') }}" rel="stylesheet">
    <!-- KEY : DROPZONE ends -->
    <!-- Styles -->
   
</head>

<body class="antialiased">
    <div class="container-lg mx-auto py-4 px-lg-5">
    
        <div class="mx-auto p-4 p-lg-5">
            <div class="d-flex justify-content-center font-weight-bold h3 text-dark">
                Item Edit
            </div>
        </div>
    
    <div class="py-12">
        <div class="container-lg mx-auto px-sm-5 px-lg-7">
            <div class="bg-white overflow-hidden shadow-lg rounded px-4 py-4">
                <a href="{{ route('item.index') }}" class="btn btn-light">
                    Back
                </a>

                @if ($message = Session::get('error'))
                    <div class="alert alert-success rounded-0 border-0 border-top border-success" role="alert">
                        <div class="d-flex">
                            <div>
                                <p class="text-sm text-danger">{{ $message }}</p>
                            </div>
                        </div>
                    </div>
                @endif
                <form action="{{ route('item.update', $item->id) }}" name="form-edit" id="form-edit" method="POST"
                    enctype="multipart/form-data">
                    @csrf
                    @method('PUT')
                    <div class="mb-4">
                        <label for="name" class="mb-2 font-weight-bold text-muted small">Name<span
                                class="text-danger"> *
                            </span></label>
                        <input type="text" name="name" class="form-control" placeholder="Enter Name"
                            maxlength="100" value="{{ $item->name }}">
                        @error('name')
                            <span class="text-danger">{{ $message }}
                            </span>
                        @enderror
                    </div>

                    <div class="mb-4">
                        <label for="sku" class="mb-2 font-weight-bold text-muted small">Sku<span
                                class="text-danger"> *
                            </span></label>
                        <input type="text" name="sku" class="form-control" placeholder="Enter SKU" maxlength="5"
                            value="{{ $item->sku }}">
                        @error('sku')
                            <span class="text-danger">{{ $message }}
                            </span>
                        @enderror
                    </div>

                    <div class="mb-4">
                        <label for="price" class="mb-2 font-weight-bold text-muted small">Price<span
                                class="text-danger"> *
                            </span></label>
                        <input type="text" name="price" id="price" class="form-control"
                            placeholder="Enter Price" maxlength="50" value="{{ $item->price }}">
                        @error('price')
                            <span class="text-danger">{{ $message }}
                            </span>
                        @enderror
                    </div>
                    <!-- KEY : DROPZONE starts -->
                    <div class="form-group">
                        <label for="document">Documents</label>
                        <div class="needsclick dropzone" id="document-dropzone">
                        </div>
                        <div>
                            <button type="submit" id="submit-all" class="btn btn-primary btn-sm fw-bold text-uppercase rounded-pill mt-2">
                                Update
                            </button>
                        </div>
                    </div>
                    <!-- KEY : DROPZONE ends -->                   
                </form>
                <div class="mb-12 d-flex gap-5">
                    <label for="image" class="block mb-2 text-sm font-bold text-gray-700">Images</label>
                    @foreach ($item->getImagesHasMany as $image)
                        <div class="image-container">
                            <img src="{{ asset('images/' . $image->image) }}" alt="sample" height="150" width="150" />
                            <form action="{{ route('image.delete', $image->id) }}" method="POST">
                                @csrf
                                @method('DELETE')
                                <button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this ?');">Delete</button>
                            </form>
                        </div>
                    @endforeach
                </div>
            </div>
        </div>
    </div>
    </div>

    <!-- KEY : DROPZONE starts -->
    <script type='text/javascript' src="{{ asset('js/jquery-3.6.4.min.js') }}"></script>
    <script type='text/javascript' src="{{ asset('js/bootstrap.min.js') }}"></script>
    <script type='text/javascript' src="{{ asset('js/dropzone.min.js') }}"></script>

    <script type="text/javascript">
        var uploadedDocumentMap = {}
        var minFiles = 0; // minimum file must be to upload
        var maxFiles = 3; // maximum file allows to upload
        var myDropzone = Dropzone.options.documentDropzone = {
            url: "{{ route('uploads') }}",
            minFiles: minFiles,
            maxFiles: maxFiles,
            autoProcessQueue: true,
            maxFilesize: 5, // MB
            addRemoveLinks: true,
            acceptedFiles: ".jpeg,.jpg,.png",
            timeout: 5000,
            headers: {
                'X-CSRF-TOKEN': "{{ csrf_token() }}"
            },
            renameFile: function(file) {
                var dt = new Date();
                var time = dt.getTime();
                return time + file.name;
            },
            success: function(file, response) {
                console.log('success file');
                console.log(file);
                console.log(response);
                $('form').append('<input type="hidden" name="document[]" value="' + response.name + '">')
                uploadedDocumentMap[file.name] = response.name
            },
            removedfile: function(file) {
                console.log('remove calls');
                console.log('remove file');
                console.log(file);
                // remove uploaded file from table and storage folder starts
                var filename = ''
                if (file.hasOwnProperty('upload')) {
                    filename = file.upload.filename;
                } else {
                    filename = file.name;
                }
                $.ajax({
                    type: 'POST',
                    url: "{{ url('image/delete') }}",
                    headers: {
                        'X-CSRF-TOKEN': "{{ csrf_token() }}"
                    },
                    data: {
                        filename: filename,
                    },
                    sucess: function(data) {
                        console.log('removed success: ' + data);
                    }
                });

                // remove file name from uploadedDocumentMap object
                Reflect.deleteProperty(uploadedDocumentMap, file.name);

                file.previewElement.remove();
                // remove uploaded file from table and storage folder ends
                // additional delete from multiple hidden files
                $('form').find('input[name="document[]"][value="' + filename + '"]').remove();
            },
            maxfilesexceeded: function(file) {
                // this.removeAllFiles();
                // this.addFile(file);
                // myDropZone.removeFile(file);
            },
            init: function() {
                console.log('init calls');
                myDropzone = this;
                // Read Files from tables and storage folder starts
                $.ajax({
                    url: "{{ url('readFiles') }}/{{ $item->id }}",
                    type: 'get',
                    dataType: 'json',
                    success: function(response) {
                        $.each(response, function(key, value) {
                            var mockFile = {
                                name: value.name,
                                size: value.size,
                                accepted: true,
                                kind: 'image'
                            };
                            myDropzone.emit("addedfile", mockFile);
                            myDropzone.files.push(mockFile);
                            myDropzone.emit("thumbnail", mockFile, value.path);
                            // myDropzone.createThumbnailFromUrl(mockFile, value.path,
                            //     function() {
                            //         myDropzone.emit("complete", mockFile);
                            //     });
                            myDropzone.emit("complete", mockFile);

                            $('form').append('<input type="hidden" name="document[]" value="' +
                                value.name + '">');
                            uploadedDocumentMap[value.name] = value.name;
                        });
                    }
                });
                // Read Files from tables and storage folder ends

                // maxfiles files limit upload validation starts
                this.on("maxfilesexceeded", function(file) {
                    alert("Maximum " + maxFiles + " files are allowed to upload...!");
                    return false;
                });
                // maximum files limit upload validation ends

                // minimum files limit upload validation starts
                var submitButton = document.querySelector("#submit-all");
                myDropzone = this;
                submitButton.addEventListener("click", function(e) {
                    e.preventDefault();
                    var imagelength = Object.keys(uploadedDocumentMap).length;
                    console.log(imagelength)
                    if (imagelength < minFiles) {
                        alert("Minimum " + minFiles + " file needs to upload...!");
                        return false;
                    } else {
                        $('#form-edit').submit();
                    }
                    /*Dropzone.forElement(".dropzone").options.autoProcessQueue = false;
                    if (myDropzone.getQueuedFiles().length >= minFiles) {
                        //myDropzone.processQueue();
                        Dropzone.forElement(".dropzone").options.autoProcessQueue = true;                        
                        Dropzone.forElement(".dropzone").processQueue();
                        $('#form-create').submit();
                    } else { // Minimum file upload validations
                        Dropzone.forElement(".dropzone").options.autoProcessQueue = false;
                        alert("Minimum "+minFiles+" file needs to upload...!");
                        return false;
                    }*/
                });
                // minimum files limit upload validation ends
            },
            error: function(file, response) {
                console.log('error file')
                console.log(file)
                console.log(response)
                $(file.previewElement).remove(); // removed files if validation fails
                return false;
            }
        }
    </script>
    <!-- KEY : DROPZONE ends -->
</body>

</html>

Mastering Laravel with ReactJS Course

Create itemshow.blade.php File

এই blade file টি দিয়ে আমরা বিদ্যমান Item গুলোর মধ্যে একটি নির্দিষ্ট Item এর বিস্তারিত দেখার কাজ করব।

নিচের কোড গুলো দিয়ে itemshow.blade.php ফাইল টি তৈরি করুন :

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Item : Show</title>

    <!-- Fonts -->
    <link rel="preconnect" href="https://fonts.bunny.net">
    <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet" />

    <!-- Styles -->
    <link href="{{ asset('css/bootstrap.min.css') }}" rel="stylesheet">
    <!-- Styles -->
    
</head>

<body class="antialiased">
    <div class="container-lg mx-auto py-4 px-lg-5">
    <div class="max-w-7xl mx-auto p-6 lg:p-8">
        <div class="flex justify-center font-semibold text-xl text-gray-800 leading-tight">
            Item Show
        </div>
    </div>
    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg px-4 py-4">



                <div class="mb-4">
                    <label for="name" class="block mb-2 text-sm font-bold text-gray-700">Name</label>
                    <input type="text" name="name" class="form-control" value="{{ $item->name }}" readonly
                        disabled>
                </div>

                <div class="mb-4">
                    <label for="sku" class="block mb-2 text-sm font-bold text-gray-700">Sku</label>
                    <input type="text" name="sku" class="form-control" value="{{ $item->sku }}" readonly
                        disabled>
                </div>

                <div class="mb-4">
                    <label for="price" class="block mb-2 text-sm font-bold text-gray-700">Price</label>
                    <input type="text" name="price" id="price" class="form-control" value="{{ $item->price }}"
                        readonly disabled>
                </div>
                
                <div class="mb-12 d-flex gap-5">
                    <label for="image" class="block mb-2 text-sm font-bold text-gray-700">Document</label>
                    @foreach ($item->getImagesHasMany as $image)
                        <img class="justify-content-center" src="{{ asset('images/' . $image->image) }}"
                            alt="sample" height="150" width="150" />
                    @endforeach
                </div>
                <br><br><br>
                <a href="{{ route('item.index') }}"
                    class="inline-flex items-center px-4 py-2 mb-4 text-xs font-semibold tracking-widest uppercase transition duration-150 ease-in-out bg-green-600 border border-transparent rounded-md hover:bg-green-500 active:bg-green-700 focus:outline-none focus:border-green-700 focus:shadow-outline-gray disabled:opacity-25">
                    Back
                </a>
            </div>
        </div>
    </div>
    </div>
    <script type='text/javascript' src="{{ asset('js/bootstrap.min.js') }}"></script>

</body>

</html>

যদি আপনার সবকিছু ঠিকঠাক থাকে , তাহলে আপনি এখন http://localhost:8000/item এই URL দিয়ে আপনার CRUD Operation চেক করতে পারেন।

Show Item List in Datatables
Show Item List in Datatables

আবার যদি আপনি “ADD NEW PRODUCT” এই বাটনে ক্লিক করেন তাহলে নিম্নোক্ত রেজাল্ট দেখতে পাবেন :

Add New Item with Dropzone.js File Upload

একইভাবে আপনি যদি “Edit” এই বাটনে ক্লিক করেন তাহলে নিম্নোক্ত রেজাল্ট দেখতে পাবেন :

Edit Item and Remove Images using Dropzone.js File Upload

এছাড়াও আপনি “Show” এই বাটনে ক্লিক করেন তাহলে নিম্নোক্ত রেজাল্ট দেখতে পাবেন :

Show Item Details

Finally আপনি “Delete” বাটনে ক্লিক করে যেকোন আইটেমের বিস্তারিত ডিলিট করতে পারেন

আমি মাসুদ আলম, বাংলাদেশের ৩৬ তম Zend Certified Engineer । ২০০৯ সালে কম্পিউটার সাইন্স থেকে বেচেলর ডিগ্রী অর্জন করি। দীর্ঘ ১৫ বছর আমি Winux Soft, SSL Wireless, IBCS-PRIMAX, Max Group, Canadian International Development Agency (CIDA), Care Bangladesh, World Vision, Hellen Keller, Amarbebsha Ltd সহ বিভিন্ন দেশি বিদেশী কোম্পানিতে ডেটা সাইন্স, মেশিন লার্নিং, বিগ ডেটা, ওয়েব ডেভেলপমেন্ট এবং সফটওয়্যার ডেভেলপমেন্ট এর উপর বিভিন্ন লিডিং পজিশন এ চাকরি এবং প্রজেক্ট লিড করি। এছাড়াও বাংলাদেশের ১৮৫ জন জেন্ড সার্টিফাইড ইঞ্জিনিয়ার এর মধ্যে ১২০ এরও অধিক ছাত্র আমার হাতে জেন্ড সার্টিফাইড ইঞ্জিনিয়ার হয়েছেন। বর্তমানে w3programmers ট্রেনিং ইনস্টিটিউট এ PHP এর উপর Professional এবং Advance Zend Certified PHP -8.2 Engineering, Laravel Mastering Course with ReactJS, Python Beginning To Advance with Blockchain, Machine Learning and Data Science, Professional WordPress Plugin Development Beginning to Advance কোর্স করাই। আর অবসর সময়ে w3programmers.com এ ওয়েব টেকনোলজি নিয়ে লেখালেখি করি।

Leave a Reply