source-code/
portofolio-neo-gruv
Public
codeCodeinfoIssues 0call_splitPull Requestsplay_circleActions
portofolio-neo-gruv/src/app/dashboard/components/ConfigEditor.tsx
typescript151 lines5.9 KB
"use client";

import React from 'react';
import { Title } from '../../../ui/Title';
import { Button } from '../../../ui/Button';
import { Input } from '../../../ui/Input';
import { useConfigEditor } from '../../../hooks/useConfigEditor';

interface FieldConfig {
  key: string;
  label: string;
  type: 'text' | 'textarea' | 'nested' | 'root_string_array' | 'object_array';
  fields?: FieldConfig[]; // for nested objects or object_array
  defaultItem?: Record<string, any>; // for object_array
}

interface ConfigEditorProps {
  configKey: string;
  title: string;
  fields: FieldConfig[];
}

export function ConfigEditor({ configKey, title, fields }: ConfigEditorProps) {
  const {
    data,
    setData,
    loading,
    saving,
    message,
    handleSave,
    handleChange,
    handleArrayAction,
  } = useConfigEditor(configKey);

  /**
   * Recursively renders a configuration input field based on its dynamic type mapping.
   * 
   * Formats handled:
   * 1. 'root_string_array' - A leaf element mapping arrays of strings directly to newline-separated textareas.
   * 2. 'object_array' - A list of nested objects. This maps each item dynamically to sub-fields using index segments
   *    in the path, allowing additions and deletions in place.
   * 3. 'nested' - An object structure containing sub-fields. Calls renderField recursively for nested children.
   * 4. Leaf fields ('text' | 'textarea') - Renders basic HTML Input/textarea elements mapping to string values.
   * 
   * @param field - The schema configurations of the target field.
   * @param parentPath - The parent path segments in the nested state object.
   */
  const renderField = (field: FieldConfig, parentPath: string[] = []) => {
    if (field.type === 'root_string_array') {
      const val = Array.isArray(data) ? data.join('\n') : '';
      return (
        <div key="root_array" className="flex flex-col gap-2">
          <label className="font-label-bold text-sm uppercase opacity-80">{field.label}</label>
          <textarea 
            className="w-full p-3 font-mono text-sm neo-border bg-surface text-on-surface focus:outline-none focus:ring-4 focus:ring-theme-yellow min-h-[150px]"
            value={val}
            onChange={(e) => setData(e.target.value.split('\n'))}
          />
        </div>
      );
    }

    const path = [...parentPath, field.key];
    const value = path.reduce((obj, key) => (obj && obj[key] !== undefined) ? obj[key] : '', data) || '';

    if (field.type === 'object_array') {
      const arr = (path.reduce((obj, key) => (obj && obj[key] !== undefined) ? obj[key] : null, data) || []) as Record<string, any>[];
      return (
        <div key={path.join('.')} className="flex flex-col gap-4 border-l-4 border-theme-yellow pl-4">
          <label className="font-label-bold text-sm uppercase opacity-80">{field.label}</label>
          {arr.map((item, index) => (
            <div key={index} className="flex flex-col gap-3 p-4 border-2 border-surface-variant relative">
              <button 
                type="button" 
                onClick={() => handleArrayAction(path, 'remove', index)}
                className="absolute top-2 right-2 text-theme-red hover:text-white uppercase font-bold text-xs"
              >
                [X] Remove
              </button>
              {field.fields?.map(subField => renderField(subField, [...path, String(index)]))}
            </div>
          ))}
          <button 
            type="button" 
            onClick={() => handleArrayAction(path, 'add', undefined, field.defaultItem)}
            className="w-full py-2 border-2 border-dashed border-surface-variant hover:border-theme-yellow text-sm uppercase font-bold"
          >
            + Add Item
          </button>
        </div>
      );
    }

    if (field.type === 'nested' && field.fields) {
      return (
        <div key={path.join('.')} className="p-4 bg-theme-grey neo-border flex flex-col gap-4">
          <label className="font-label-bold uppercase text-theme-red">{field.label}</label>
          {field.fields.map(subField => renderField(subField, path))}
        </div>
      );
    }

    return (
      <div key={path.join('.')} className="flex flex-col gap-2">
        <label className="font-label-bold text-sm uppercase opacity-80">{field.label}</label>
        {field.type === 'textarea' ? (
          <textarea 
            className="w-full p-3 font-mono text-sm neo-border bg-surface text-on-surface focus:outline-none focus:ring-4 focus:ring-theme-yellow min-h-[100px]"
            value={value}
            onChange={(e) => handleChange(path, e.target.value)}
          />
        ) : (
          <Input 
            className="w-full"
            value={value}
            onChange={(e) => handleChange(path, e.target.value)}
          />
        )}
      </div>
    );
  };

  if (loading) return <div className="p-8 font-label-bold uppercase animate-pulse">Loading {title}...</div>;

  return (
    <div className="bg-surface p-6 md:p-10 neo-border-heavy neo-shadow-sm mb-12">
      <Title 
        as="h3" 
        prefix="" 
        highlight={title.toUpperCase()} 
        highlightColorClass="bg-theme-yellow text-on-surface" 
        highlightRotateClass="rotate-0"
        className="mb-8 uppercase tracking-tighter text-[28px] md:text-[36px] font-display-2xl leading-none" 
      />
      
      {message && (
        <div className={`p-4 mb-6 neo-border font-bold uppercase ${message.includes('Error') ? 'bg-theme-red text-surface' : 'bg-theme-green text-on-surface'}`}>
          {message}
        </div>
      )}

      <form onSubmit={handleSave} className="flex flex-col gap-6">
        {fields.map(f => renderField(f))}
        <Button type="submit" disabled={saving} className="bg-theme-blue text-surface py-4 neo-border hover:-translate-y-1 hover:shadow-[4px_4px_0px_0px_#1e1b19] transition-all self-start px-8 mt-4 disabled:opacity-50">
          {saving ? 'SAVING...' : 'SAVE CHANGES'}
        </Button>
      </form>
    </div>
  );
}

About

Custom portfolio frontend designed using retro Neo-Brutalist styling. Features server-rendered pages, persistent codebase layout, interactive file explorer tree, and Shiki code syntax highlighting.

TypeScriptNext.jsReact 19Tailwind CSSShiki

Contributors

1