Andreas Ludvigsson

Bridging Code and Commerce

Creating an AI-Powered Resume Maker SaaS with Laravel – Part 2 – Getting started with the Backend

Welcome back to our series on creating an AI-Powered Resume Maker SaaS with Laravel. In the previous part, we created an appealing landing page to attract users to our platform. In this post, we’ll start working on the backend of our application.

Please note that this post will contain a significant amount of files and code. We’ll do our best to explain each step clearly, but if you find yourself lost at any point, don’t worry. The complete codebase will be available on GitHub at the end of the series for your reference.

Since we are using JetStream with Inertia, we already have a basic backend structure in place. However, we’ll be making some adjustments to our layout and creating new components. These components will allow users to input the necessary data for generating their resumes.

Throughout this post, we’ll dive into the process of building these components and integrating them with our existing backend. By the end, you’ll have a better understanding of how to structure your Laravel backend for a SaaS application.

So, let’s get started and continue building our AI-Powered Resume Maker SaaS!

Enhancing the Backend Layout

resume saas dashboard

To enhance the backend layout, we’ll draw inspiration from Tailwind UI and utilize two files: DashboardLayout.vue and DashboardNavigation.vue.

First, let’s take a look at the code for DashboardLayout.vue, located in resources/js/Layouts/DashboardLayout.vue:

<template>
  <div>
    <DashboardNavigation :sidebarOpen="sidebarOpen" />
    <main class="py-10 lg:pl-72">
      <div class="px-4 sm:px-6 lg:px-8">
        <slot></slot>
      </div>
    </main>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { Dialog, DialogPanel, TransitionChild, TransitionRoot } from '@headlessui/vue'
import { AcademicCapIcon, Bars3Icon, BriefcaseIcon, CalendarIcon, ChartBarIcon, ChartPieIcon, ClipboardDocumentListIcon, DocumentDuplicateIcon, EnvelopeOpenIcon, FolderIcon, HomeIcon, UserGroupIcon, UsersIcon, XMarkIcon, } from '@heroicons/vue/24/outline'
import DashboardNavigation from "@/Layouts/Partials/DashboardNavigation.vue";


const sidebarOpen = ref(false)
</script>

This file defines the overall structure of the dashboard layout. It includes the DashboardNavigation component and a main content area where the slot will be rendered. The script section imports the necessary icons and components, defines the navigation items, and sets up a reactive variable called sidebarOpen to control the visibility of the sidebar on mobile devices.

Next, let’s examine the code for DashboardNavigation.vue, located in resources/js/Layouts/Partials/DashboardNavigation.vue:

<script setup>
import { Dialog, DialogPanel, TransitionChild, TransitionRoot } from '@headlessui/vue'
import {
    AcademicCapIcon,
    Bars3Icon,
    BriefcaseIcon,
    ChartBarIcon,
    EnvelopeOpenIcon,
    HomeIcon,
    UserGroupIcon,
    XMarkIcon,
} from '@heroicons/vue/24/outline'

const navigation = [
    { name: 'Dashboard', href: '/', icon: HomeIcon, current: true },
    { name: 'Resumes', href: '/resumes', icon: UserGroupIcon, current: false },
    { name: 'Cover Letters', href: '/cover-letters', icon: EnvelopeOpenIcon, current: false },
    { name: 'Work Experience', href: '/experience', icon: BriefcaseIcon, current: false },
    { name: 'Education', href: '/education', icon: AcademicCapIcon, current: false },
    { name: 'Skills', href: '/skill', icon: ChartBarIcon, current: false },
]


const props = defineProps({
    sidebarOpen: Boolean,
})
</script>

<template>
    <TransitionRoot as="template" :show="sidebarOpen">
        <Dialog as="div" class="relative z-50 lg:hidden" @close="sidebarOpen = false">
            <TransitionChild
                as="template"
                enter="transition-opacity ease-linear duration-300"
                enter-from="opacity-0"
                enter-to="opacity-100"
                leave="transition-opacity ease-linear duration-300"
                leave-from="opacity-100"
                leave-to="opacity-0"
            >
                <div class="fixed inset-0 bg-gray-900/80" />
            </TransitionChild>

            <div class="fixed inset-0 flex">
                <TransitionChild
                    as="template"
                    enter="transition ease-in-out duration-300 transform"
                    enter-from="-translate-x-full"
                    enter-to="translate-x-0"
                    leave="transition ease-in-out duration-300 transform"
                    leave-from="translate-x-0"
                    leave-to="-translate-x-full"
                >
                    <DialogPanel class="relative mr-16 flex w-full max-w-xs flex-1">
                        <TransitionChild
                            as="template"
                            enter="ease-in-out duration-300"
                            enter-from="opacity-0"
                            enter-to="opacity-100"
                            leave="ease-in-out duration-300"
                            leave-from="opacity-100"
                            leave-to="opacity-0"
                        >
                            <div class="absolute left-full top-0 flex w-16 justify-center pt-5">
                                <button type="button" class="-m-2.5 p-2.5" @click="sidebarOpen = false">
                                    <span class="sr-only">Close sidebar</span>
                                    <XMarkIcon class="h-6 w-6 text-white" aria-hidden="true" />
                                </button>
                            </div>
                        </TransitionChild>

                        <!-- Sidebar component, swap this element with another sidebar if you like -->
                        <div class="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-100 px-6 pb-2">
                            <div class="flex h-16 shrink-0 items-center">
                            </div>
                            <nav class="flex flex-1 flex-col">
                                <ul role="list" class="flex flex-1 flex-col gap-y-7">
                                    <li>
                                        <ul role="list" class="-mx-2 space-y-1">
                                            <li v-for="item in navigation" :key="item.name">
                                                <a
                                                    :href="item.href"
                                                    :class="[
                            item.current ? 'bg-teal-50 text-teal-600' : 'text-gray-700 hover:text-teal-600 hover:bg-teal-50',
                            'group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold',
                          ]"
                                                >
                                                    <component
                                                        :is="item.icon"
                                                        :class="[
                              item.current ? 'text-teal-600' : 'text-gray-400 group-hover:text-teal-600',
                              'h-6 w-6 shrink-0',
                            ]"
                                                        aria-hidden="true"
                                                    />
                                                    {{ item.name }}
                                                </a>
                                            </li>
                                        </ul>
                                    </li>
                                </ul>
                            </nav>
                        </div>
                    </DialogPanel>
                </TransitionChild>
            </div>
        </Dialog>
    </TransitionRoot>

    <!-- Static sidebar for desktop -->
    <div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
        <!-- Sidebar component, swap this element with another sidebar if you like -->
        <div class="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-gray-100 px-6">
            <div class="flex h-16 shrink-0 items-center">
            </div>
            <nav class="flex flex-1 flex-col">
                <ul role="list" class="flex flex-1 flex-col gap-y-7">
                    <li>
                        <ul role="list" class="-mx-2 space-y-1">
                            <li v-for="item in navigation" :key="item.name">
                                <a
                                    :href="item.href"
                                    :class="[
                    item.current ? 'bg-teal-50 text-teal-600' : 'text-gray-700 hover:text-teal-600 hover:bg-teal-50',
                    'group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold',
                  ]"
                                >
                                    <component
                                        :is="item.icon"
                                        :class="[
                      item.current ? 'text-teal-600' : 'text-gray-400 group-hover:text-teal-600',
                      'h-6 w-6 shrink-0',
                    ]"
                                        aria-hidden="true"
                                    />
                                    {{ item.name }}
                                </a>
                            </li>
                        </ul>
                    </li>

                    <li class="-mx-6 mt-auto">
                        <a
                            href="/profile"
                            class="flex items-center gap-x-4 px-6 py-3 text-sm font-semibold leading-6 text-gray-900 hover:bg-gray-50"
                        >
                            Profile
                            <span class="sr-only">Your profile</span>
                        </a>
                    </li>
                </ul>
            </nav>
        </div>
    </div>

    <div class="sticky top-0 z-40 flex items-center gap-x-6 bg-gray-100 px-4 py-4 shadow-sm sm:px-6 lg:hidden">
        <button type="button" class="-m-2.5 p-2.5 text-gray-700 lg:hidden" @click="sidebarOpen = true">
            <span class="sr-only">Open sidebar</span>
            <Bars3Icon class="h-6 w-6" aria-hidden="true" />
        </button>
        <div class="flex-1 text-sm font-semibold leading-6 text-gray-900">Dashboard</div>
        <a href="/profile">
            <span class="sr-only">Your profile</span>
            Profile
        </a>
    </div>
</template>

<style scoped>
</style>




This component represents the navigation sidebar of the dashboard. It uses the TransitionRoot and TransitionChild components from Headless UI to create a mobile-friendly sidebar that slides in and out when the sidebarOpen prop is toggled.

The desktop version of the sidebar is always visible and contains the navigation items defined in the navigation array. Each navigation item is rendered as a link with an icon and text. The current active item is highlighted with a different background color and text color.

The mobile version of the sidebar is hidden by default and can be opened by clicking the hamburger menu button. It uses the same navigation items as the desktop version but is displayed as a full-screen overlay.

With these two components, we have enhanced the backend layout to provide a more intuitive and visually appealing navigation experience for our users.

Creating the Resume Information Functionality

Now that we have enhanced the backend layout, it’s time to dive into the core functionality of our AI-Powered Resume Maker SaaS. In this section, we’ll focus on creating the necessary models, controllers, and views to handle the resume information, including skills, education, and work experience.

We’ll start by defining the database structure using Laravel’s migration files and creating the corresponding Eloquent models. Then, we’ll implement the controllers to handle the CRUD (Create, Read, Update, Delete) operations for each resume information type. Finally, we’ll create the views using Vue.js and Inertia to provide a seamless and interactive user experience.

By the end of this section, we’ll have a controllers that allows users to manage their resume information efficiently. So, let’s roll up our sleeves and get started!

Education Integrations for the Resume

  1. EducationController.php:
    • This is the controller responsible for handling the CRUD (Create, Read, Update, Delete) operations for the education records.
    • The index() method retrieves all the education records for the authenticated user and passes them to the Education/Index Inertia view.
    • The create() method renders the Education/Create Inertia view, which displays the form for creating a new education record.
    • The store() method handles the creation of a new education record. It validates the incoming request data and then creates a new Education model instance with the provided data, setting the user_id to the authenticated user’s ID.
    • The edit() method renders the Education/Edit Inertia view, which displays the form for editing an existing education record.
    • The update() method handles the update of an existing education record. It validates the incoming request data and then updates the Education model instance with the new data.
    • The destroy() method handles the deletion of an existing education record.
  2. Education/Index.vue:
    • This is the Inertia view that displays the list of education records for the authenticated user.
    • It uses the EducationTable component to render the table of education records.
    • The Link component is used to create a link to the education.create route, allowing the user to add a new education record.
  3. Education/Create.vue:
    • This is the Inertia view that displays the form for creating a new education record.
    • It uses a ref to manage the form data and the submitForm() method to handle the form submission.
    • The form fields are bound to the form object, and validation errors are displayed using the v-if directive.
  4. Education/Edit.vue:
    • This is the Inertia view that displays the form for editing an existing education record.
    • It uses a ref to manage the form data and the submitForm() method to handle the form submission.
    • The form fields are bound to the form object, and validation errors are displayed using the v-if directive.
    • The watch() function is used to populate the form with the existing education data when the component is loaded.
  5. Education/Partials/EducationTable.vue:
    • This is a reusable component that displays the table of education records.
    • It receives the educations prop, which is an object containing the education records.
    • The formatDate() function is used to format the start and end dates of the education records.
    • The deleteEducation() function is used to handle the deletion of an education record, using the router.delete() method.
  6. Education.php:
    • This is the Eloquent model for the education table.
    • It defines the $fillable property, which specifies the fields that can be mass-assigned.
  7. 2024_04_07_195715_create_education_table.php:
    • This is the migration file that creates the education table in the database.
    • The table has the following columns: idstart_dateend_datedegreeinstitutiondescriptionuser_id, and the standard created_at and updated_at columns.
    • The user_id column is a foreign key that references the id column in the users table, and it is set to cascade on delete.

app/Http/Controllers/EducationController.php

<?php

namespace App\Http\Controllers;

use App\Models\Education;
use App\Models\Experience;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;

class EducationController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        $education = Education::where('user_id', '=', Auth::id())->get();
        return Inertia::render('Education/Index', ['education' => $education]);
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        return Inertia::render('Education/Create');


    }

    public function store(Request $request)
    {
        // Validate the incoming request data
        $request->validate([
            'start_date' => 'required|date',
            'end_date' => 'nullable|date|after_or_equal:start_date',
            'degree' => 'required|string|max:255',
            'institution' => 'required|string|max:255',
            'description' => 'required|string',
        ]);

        // Assuming you're using authenticated user's ID
        // Adjust according to your application's logic
        $userId = Auth::id();

        // Create and save the new Education record
        $education = new Education;
        $education->start_date = $request->start_date;
        $education->end_date = empty($request->end_date) ? null : $request->end_date;
        $education->degree = $request->degree;
        $education->institution = $request->institution;
        $education->description = $request->description;
        $education->user_id = $userId; // Set the user_id to the authenticated user's id
        $education->save();

        // Return a success response, adjust the route to your education list/index view
        return to_route('education.index');
    }


    /**
     * Display the specified resource.
     */
    public function show(Education $education)
    {

    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(Education $education)
    {
        return Inertia::render('Education/Edit', ['education' => $education]);

    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, Education $education)
    {
        // Validate the incoming request data
        $request->validate([
            'start_date' => 'required|date',
            'end_date' => 'nullable|date|after_or_equal:start_date',
            'degree' => 'required|string|max:255',
            'institution' => 'required|string|max:255',
            'description' => 'required|string',
        ]);

        // Update the existing Education record with new data
        $education->start_date = $request->start_date;
        $education->end_date = $request->end_date; // Assuming you allow null if still studying
        $education->degree = $request->degree;
        $education->institution = $request->institution;
        $education->description = $request->description;

        // Save the updated Education record
        $education->save();

        // Return a success response, typically redirecting to a list or detail view
        return to_route('education.index'); // Adjust the redirect as necessary for your application
    }


    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Education $education)
    {
        $education->delete();

        // Return a response or redirect.

        return to_route('education.index');
    }
}

Education/Index.vue

<script setup>
import {Link} from "@inertiajs/vue3";
import EducationTable from "@/Pages/Education/Partials/EducationTable.vue";

const props = defineProps({
    education: Object,
})
import DashboardLayout from "@/Layouts/DashboardLayout.vue";
</script>

<template>
    <DashboardLayout title="Education">
        <education-table :educations="education"/>
        <Link  :href="route('education.create')" class="mt-5 relative block w-full rounded-lg bg-white border-2 border-gray-300 p-4 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
            <div class="flex items-center justify-center">
                <svg class="h-6 w-6 text-gray-600" stroke="currentColor" fill="none" viewBox="0 0 24 24" aria-hidden="true">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
                </svg>
                <span class="ml-2 text-base font-medium text-gray-700">Add New Education Record</span>
            </div>
        </Link>
    </DashboardLayout>
</template>

<style scoped>

</style>

Education/Create.vue

<script setup>
import { ref } from 'vue'
import DashboardLayout from "@/Layouts/DashboardLayout.vue";
import {router} from "@inertiajs/vue3";

defineProps({ errors: Object })

const form = ref({
    start_date: '',
    end_date: '',
    degree: '',
    institution: '',
    description: '',
    user_id: '' // Assuming this is set elsewhere in your application
})

const submitForm = () => {
    router.post('/education', form.value)
}
</script>


<template>
    <DashboardLayout>
        <form @submit.prevent="submitForm" class="space-y-6">
            <div>
                <label for="degree" class="block text-sm font-medium text-gray-700">Degree</label>
                <input type="text" id="degree" v-model="form.degree" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
                <div v-if="errors.degree" class="text-red-500 text-sm">{{ errors.degree }}</div>
            </div>

            <div>
                <label for="institution" class="block text-sm font-medium text-gray-700">Institution</label>
                <input type="text" id="institution" v-model="form.institution" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
                <div v-if="errors.institution" class="text-red-500 text-sm">{{ errors.institution }}</div>
            </div>

            <div>
                <label for="start_date" class="block text-sm font-medium text-gray-700">Start Date</label>
                <input type="date" id="start_date" v-model="form.start_date" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
                <div v-if="errors.start_date" class="text-red-500 text-sm">{{ errors.start_date }}</div>
            </div>

            <div>
                <label for="end_date" class="block text-sm font-medium text-gray-700">End Date</label>
                <input type="date" id="end_date" v-model="form.end_date" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
                <!-- End date can be null, indicating ongoing education, so no error check here -->
            </div>

            <div>
                <label for="description" class="block text-sm font-medium text-gray-700">Description</label>
                <textarea id="description" v-model="form.description" rows="3" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"></textarea>
                <div v-if="errors.description" class="text-red-500 text-sm">{{ errors.description }}</div>
            </div>

            <div>
                <button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
                    Submit
                </button>
            </div>
        </form>
    </DashboardLayout>
</template>


<style scoped>

</style>

Education/Edit.vue

<script setup>
import { ref, watch } from 'vue'
import DashboardLayout from "@/Layouts/DashboardLayout.vue";
import { router } from "@inertiajs/vue3";

const { errors, education } = defineProps({
    errors: Object,
    education: Object
});

const form = ref({
    id: '',
    start_date: '',
    end_date: '',
    degree: '',
    institution: '',
    description: ''
});

// Populate form with education data if it exists
watch(() => education, (newEducation) => {
    if (newEducation) {
        form.value = {
            ...form.value,
            ...newEducation
        };
    }
}, { immediate: true });

const submitForm = async () => {
    const method = form.value.id ? 'put' : 'post';
    const url = form.value.id ? `/education/${form.value.id}` : '/education';

    router[method](url, form.value)
        .then(response => {
            // Handle success
        })
        .catch(error => {
            // Handle error
        });
}
</script>

<template>
    <DashboardLayout>
        <form @submit.prevent="submitForm" class="space-y-6">
            <div>
                <label for="degree" class="block text-sm font-medium text-gray-700">Degree</label>
                <input type="text" id="degree" v-model="form.degree" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
                <div v-if="errors.degree" class="text-red-500 text-sm">{{ errors.degree }}</div>
            </div>

            <div>
                <label for="institution" class="block text-sm font-medium text-gray-700">Institution</label>
                <input type="text" id="institution" v-model="form.institution" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
                <div v-if="errors.institution" class="text-red-500 text-sm">{{ errors.institution }}</div>
            </div>

            <div>
                <label for="start_date" class="block text-sm font-medium text-gray-700">Start Date</label>
                <input type="date" id="start_date" v-model="form.start_date" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
                <div v-if="errors.start_date" class="text-red-500 text-sm">{{ errors.start_date }}</div>
            </div>

            <div>
                <label for="end_date" class="block text-sm font-medium text-gray-700">End Date</label>
                <input type="date" id="end_date" v-model="form.end_date" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500">
            </div>

            <div>
                <label for="description" class="block text-sm font-medium text-gray-700">Description</label>
                <textarea id="description" v-model="form.description" rows="3" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"></textarea>
                <div v-if="errors.description" class="text-red-500 text-sm">{{ errors.description }}</div>
            </div>

            <div>
                <button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
                    Submit
                </button>
            </div>
        </form>
    </DashboardLayout>
</template>

<style scoped>
</style>

Education/Partials/EducationTable.vue

<script setup>
import { defineProps } from 'vue'
import { Link } from "@inertiajs/vue3";

const { props } = defineProps({
    educations: Object,
})

import { router } from '@inertiajs/vue3'

function formatDate(date) {
    if (!date) return 'Present';
    const options = { year: 'numeric', month: 'long', day: 'numeric' };
    return new Date(date).toLocaleDateString(undefined, options);
}

function deleteEducation(educationId) {
    if (confirm("Are you sure you want to delete this education record?")) {
        router.delete(route('education.destroy', {id: educationId}), {
            onSuccess: () => {
                alert('Education record deleted successfully.');
            },
            onError: () => {
                alert('There was a problem deleting the education record.');
            }
        });
    }
}
</script>

<template>
    <div class="bg-white shadow-md rounded-lg">
        <ul role="list" class="divide-y divide-gray-200">
            <li v-for="education in educations" :key="education.id" class="py-4 px-6">
                <div class="flex justify-between items-center mb-2">
                    <h3 class="text-xl font-semibold text-indigo-600">{{ education.degree }}</h3>
                    <div class="flex items-center space-x-2">
                        <Link :href="route('education.edit', {education: education.id})" class="px-3 py-1 text-sm text-white bg-indigo-500 rounded-md hover:bg-indigo-600 transition duration-300 ease-in-out">Edit</Link>
                        <button @click="deleteEducation(education.id)" class="px-3 py-1 text-sm text-white bg-red-500 rounded-md hover:bg-red-600 transition duration-300 ease-in-out">Delete</button>
                    </div>
                </div>
                <div class="mb-2">
                    <p class="text-gray-500">{{ education.institution }}</p>
                    <p class="text-sm text-gray-400">{{ education.description }}</p>
                </div>
                <div class="flex justify-between items-center text-sm text-gray-500">
                    <p>
                        From <time :datetime="education.start_date" class="font-semibold">{{ formatDate(education.start_date) }}</time>
                        to <time :datetime="education.end_date" class="font-semibold">{{ education.end_date ? formatDate(education.end_date) : 'Present' }}</time>
                    </p>
                    <p v-if="!education.end_date" class="px-2 py-1 text-green-800 bg-green-200 rounded-full">Currently Studying</p>
                </div>
            </li>
        </ul>
    </div>
</template>


<style scoped>

</style>

app/Models/Education.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Education extends Model
{

    protected $fillable = [
        'start_date',
        'end_date',
        'degree',
        'institution',
        'description',
        'user_id',
    ];
    use HasFactory;
}

database/migrations/2024_04_07_195715_create_education_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('education', function (Blueprint $table) {
            $table->id();
            $table->date('start_date');
            $table->date('end_date')->nullable();
            $table->string('degree');
            $table->string('institution');
            $table->text('description');
            $table->unsignedBigInteger('user_id');
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('educations');
    }
};

Now that we’ve explored the Education module, let’s move on to the Skills and Work Experience components. These will follow a similar structure, but with their own unique requirements and functionality.

For the Skills module, we’ll create a new controller, model, and Inertia views to handle the user’s skills. This will allow them to add, edit, and manage the skills they want to showcase on their resume.

Similarly, the Work Experience module will have its own controller, model, and views to capture the user’s employment history, job titles, responsibilities, and dates of employment. This information is crucial for building a comprehensive resume.

To keep this tutorial focused and easy to follow, I won’t be walking through the full implementation of these modules in the article. Instead, you can find the complete code for the Skills and Work Experience components in the GitHub repository at the end of this series. This way, you can reference the implementation details as needed and focus on understanding the overall architecture and design decisions.

By providing the full code in the repository, you’ll have a solid foundation to build upon and can explore the implementation details at your own pace. This approach allows us to maintain a clear and concise flow in the article, while still giving you access to the complete solution.

Leave a Reply

Your email address will not be published. Required fields are marked *