DEV Community

Dmitriy Kasperovich
Dmitriy Kasperovich

Posted on

How I Hacked Together a Fully Functional Settings Page in Under 20 Minutes

Sometimes, you just want a page that isn’t tied to some classic CRUD table. No list, no sorting, no pagination. Just a form. Like… app-wide settings, or something stupidly custom like “set default currency and minimum app version.”
So we built this in Admiral. Turns out, it’s actually dead simple to spin up a custom settings page, the same way you’d define any CRUD screen, just without the CRUD.
Here’s how we did it.
We’ll start by adding a new menu item — for example, Settings.

Step 1: Add It to the Menu

First, wire up a new menu item. Nothing fancy.

$menu->addItem(MenuItem::make(MenuKey::SETTINGS, 'Settings', 'FiSettings', '/settings'));
Enter fullscreen mode Exit fullscreen mode

Now when you click that sucker, we’ll route to /settings.

Step 2: Build the Page Skeleton

Create a new folder: components/Settings/Settings.tsx.

Inside it, scaffold your form page like this:

import React, {useCallback} from 'react'
import {Form, Page} from '@devfamily/admiral'
import api from '../../config/api'


const settingsUri = 'settings'


export default function Settings() {
   const fetchInitialData = useCallback(() => {
       return api.get(settingsUri)
   }, [])


   const _onSubmit = useCallback(async (values) => {
       return api.post(settingsUri, values)
   }, [])


   return (
       <Page title="Settings">
           <Form submitData={_onSubmit} fetchInitialData={fetchInitialData}>
               // Here will be our inputs

               <Form.Footer>
                   <Form.Submit>Save</Form.Submit>
               </Form.Footer>
           </Form>
       </Page>
   )
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Register the Page

Let’s take a look at what our component returns. It renders a Page component along with a Form component from the devfamily/admiral package.

Nothing wild here — Admiral’s

takes care of all the loading/saving boilerplate. You just plug in the fetchInitialData and submitData callbacks, and it knows what to do.
Before we check how the page looks in the browser, let’s register it.
Navigate to the Pages directory and create a new settings folder with an index.tsx file inside.
In that file, we’ll define the following, so the router knows where to find this thing:
import Settings from '@/src/components/Settings/Settings'

export default Settings
Enter fullscreen mode Exit fullscreen mode

Put this in /pages/settings/index.tsx or wherever your pages live.
Open the browser — boom, empty settings page. Let’s add some real fields.

Step 4: Add Form Inputs

Say we want two settings:
A minimum app version (min_version)

A default currency (default_currency) — which we’ll fetch from the server

Here’s the updated component:

import React, {useCallback} from 'react'
import {Form, Page, SelectInput, TextInput} from '@devfamily/admiral'
import api from '../../config/api'


const settingsUri = 'settings'


export default function Settings() {
   const fetchInitialData = useCallback(() => {
       return api.get(settingsUri)
   }, [])


   const _onSubmit = useCallback(async (values) => {
       return api.post(settingsUri, values)
   }, [])


   return (
       <Page title="Settings">
           <Form submitData={_onSubmit} fetchInitialData={fetchInitialData}>


               <TextInput name='min_version' label='Minimum App Version'/>


               <SelectInput name='default_currency' label='Default currency'/>


               <Form.Footer>
                   <Form.Submit>Save</Form.Submit>
               </Form.Footer>
           </Form>
       </Page>
   )
}
Enter fullscreen mode Exit fullscreen mode

The currency options come from the backend, and the form auto-populates thanks to fetchInitialData. You don’t even have to think about state management. It just works.

Step 5: Add the API Wrapper

Admiral needs an api.get and api.post to fetch and save the form. Here’s what that looks like in src/config/api.ts:

import {GetFormDataResult, UpdateResult} from '@devfamily/admiral'
import _ from './request'
import {Any} from '@react-spring/types'


const apiUrl = import.meta.env.VITE_API_URL || '/api'


type apiType = {
   get: (uri: string) => Promise<GetFormDataResult>
   post: (uri: string, data: Any) => Promise<UpdateResult>
}


const api: apiType = {
   get: (uri) => {
       const url = `${apiUrl}/${uri}`
       return _.get(url)({})
   },
   post: (uri, data) => {
       const url = `${apiUrl}/${uri}`
       return _.post(url)({ data })
   },
}


export default api

Enter fullscreen mode Exit fullscreen mode

Here, we defined two methods for sending requests to the server. Each method appends the passed uri (in this case, settings) to the base server URL specified in the environment variables.
It’s a tiny wrapper around your actual HTTP client (probably Axios). VITE_API_URL is where the admin API lives — by default, that’s http://localhost:802/admin.
Now we can move on to defining the routing and controller logic.

Step 6: Backend Time — Store Settings in the DB
You need a place to save this stuff. Here’s the migration:

<?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
{
   /**
    * Run the migrations.
    */
   public function up(): void
   {
       Schema::create('settings', function (Blueprint $table) {
           $table->string('key')->unique();
           $table->text('value')->nullable();
           $table->timestamps();
       });


       DB::table('settings')->insert([
           [
               'key' => 'min_version',
               'value' => '3.0.0',
           ],
           [
               'key' => 'default_currency',
               'value' => 'RUB',
           ],
       ]);
   }


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

Enter fullscreen mode Exit fullscreen mode

Yes, it’s a key-value store. Yes, it’s enough for most settings pages. Don’t overengineer.

Step 7: Make the Model and Controller

<?php


namespace App\Models;


use Illuminate\Database\Eloquent\Model;


class Setting extends Model
{
   protected $fillable = [
      'key',
      'value',
   ];
}

Enter fullscreen mode Exit fullscreen mode

Next, let’s create the controller with two methods:
One for returning the current settings;
One for saving new settings values.

<?php


namespace App\Http\Controllers\Admin;


use App\Http\Controllers\Controller;
use App\Models\Setting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;


class SettingsController extends Controller
{
   public function get(): JsonResponse
   {
       $settings = Setting::query()
           ->whereIn('key', ['min_version', 'default_currency'])
           ->get()
           ->keyBy('key');


       return response()->json([
           'data' => [
               'min_version' => $settings->get('min_version')->value,
               'default_currency' => $settings->get('default_currency')->value,
           ],
           'values' => [
               'default_currency' => [
                   [
                       'label' => 'Dollar',
                       'value' => 'USD',
                   ],
                   [
                       'label' => 'Euro',
                       'value' => 'EUR',
                   ],
                   [
                       'label' => 'Russian ruble',
                       'value' => 'RUB',
                   ],
               ],
           ]
       ]);
   }


   public function store(Request $request): JsonResponse
   {
       Setting::query()->where('key', 'min_version')->update(['value' => $request->input('min_version')]);
       Setting::query()->where('key', 'default_currency')->update(['value' => $request->input('default_currency')]);


       return response()->json();
   }
}

Enter fullscreen mode Exit fullscreen mode

So yeah — data pre-fills your form. values is for dropdown options. That’s how Admiral expects it.

In the get() method, the current settings data is returned under the data key. The keys in this object must match the name attributes defined in the form inputs.

The values key is used to pass the necessary options for the SelectInput. In this case, we’ve defined three currency options.

Next, let’s register our routes.

Step 8: Route It
Add this to /routes/admin.php:

Route::prefix('settings')->as('settings.')->group(function () {
   Route::get('',[SettingsController::class,'get'])->name('get');
   Route::post('',[SettingsController::class,'store'])->name('store');
});

Enter fullscreen mode Exit fullscreen mode

Now your settings form will load values on page load and submit them back on save.

Done. You’ve Got a Custom Page.

So yeah. That’s it. You can now throw any inputs in there — CheckboxInput, DatePickerInput, TextareaInput, you name it. Admiral handles validation, form state, loading states, and submit logic. You just wire up your fields and your backend.

Settings page? Done. No spreadsheets, no state hell, no React-hook-form madness.

When the “Save” button is clicked, the form data is sent to the server.

Want to see it in action? Here’s the demo:

GitHub logo dev-family / admiral

Admiral is a frontend framework for creating back office in React. It provides out-of-the-box components and tools that make developing an admin interface easy and fast.

Admiral logo

Revolutionize Your Workflow with Admiral: A Powerful Solution for Tailored Admin Panels and Business Applications

🌐 Try Live Demo  |  📖 Real-World Use Cases

Twitter Downloads Version License

Admiral Administration panel

Admiral is a frontend framework for creating back office in React. It provides out-of-the-box components and tools that make developing an admin interface easy and fast.

Made with 💜 by dev.family

📖 Table of Contents

✨ Features

  • 📀 Out-of-the-box React components used.
  • 🛡 It is written in TypeScript and has built-in typing.
  • 👨‍💻 Adaptive design: The library interface scales to any screen size. This is convenient when used on mobile devices.
  • 🌍…




.

Let me know if you build something weird with it — that’s where this stuff gets fun.

Top comments (0)