Continuation and Conclusion of MVP #1

Continuation and Conclusion of MVP #1

We have reached the final stage of our journey into establishing components and, more importantly, how they interact with one another. What do we still have on the table? Well, it’s precisely the communication between components on the same web page. For instance, the browse component might inform the form component, "Hey... I've changed the record," allowing the form to adjust its data accordingly. Additionally, there's the aspect of data saving, where the form could notify the browse component, "Hey... the data for this record has been modified."

Synchronization Between Browse and Form

So... we are going to synchronize the browse and the form: when we navigate to a different record, the form's data needs to be refreshed.

We'll start by doing this in a rough manner to establish the guiding principles. After that, we will refine it.

Now... let's recap... we have set an onclick event on each row of the table. We did it this way...

HTML += `<tbody>`;
  for ( let record of records )
  {
    HTML += `<tr onclick="console.log( 'recno is ${record.recno.value}' )">`;
      HTML += `<td class="recno">${record.recno.value}</td>`;
      HTML += `<td class="id">${record.id.value.trim()}</td>`;
      HTML += `<td class="fname">${record.f_fname.value.trim()}</td>`;
      HTML += `<td class="lname">${record.f_lname.value.trim()}</td>`;
      HTML += `<td class="birthdate">${str2date(record.f_birthdate.value)}</td>`;
      HTML += `<td class="rating">${record.f_rating.value}</td>`;
      HTML += `<td class="rewards">${record.f_rewards.value}</td>`;
    HTML += `</tr>`;
  }   /* For each record */
HTML += `</tbody>`;        

And so... we were logging the record number associated with the row to the console (HTML += <tr onclick="console.log( 'recno is ${record.recno.value}' )">;).

If we created a specific function like inform() instead of using console.log(), we could trigger the update of the other module. Shall we give it a try?

HTML += `<tbody>`;
  for ( let record of records )
  {
    HTML += `<tr onclick="inform( ${record.recno.value} );">`;
      HTML += `<td class="recno">${record.recno.value}</td>`;
      HTML += `<td class="id">${record.id.value.trim()}</td>`;
      HTML += `<td class="fname">${record.f_fname.value.trim()}</td>`;
      HTML += `<td class="lname">${record.f_lname.value.trim()}</td>`;
      HTML += `<td class="birthdate">${str2date(record.f_birthdate.value)}</td>`;
      HTML += `<td class="rating">${record.f_rating.value}</td>`;
      HTML += `<td class="rewards">${record.f_rewards.value}</td>`;
    HTML += `</tr>`;
  }   /* For each record */
HTML += `</tbody>`;        

Note that the console.log() statement has been replaced by inform( recno ). Here is teh code of the inform() function:

function inform( recno )
/*--------------------*/
{
    console.log( 'HEY HEY recno is ${record.recno.value}' );
    const formModule = document.getElementById( 'QM_95d40c32-c2e7-4ed2-b21e-b815b521f9c0' );

    if ( formModule )
    {
        formModule.setAttribute( 'data-start-recno',recno );
        formModule.connectedCallback();
    }
}   /* inform( recno )  */        

Here’s what justifies the word 'rough': we are directly referencing the ID of the other module when that ID should be unknown to us. It’s cheating... but it will work! Let’s give it a try...

We are on record 88 in the form, and we can see in the browse that the data is indeed the same. But this... is our starting point.

Let's go to record 97 and see if the form data is updated synchronously...

Oui, c'est le cas! Cela marche. C'est mochement réalisé mais ?a marche.

Next, we will handle the data saving for the form, prioritizing a save operation using an XMLHttpRequest. I will base this on the timeline approach and provide feedback with a brief notification.

Data Saving

As previously mentioned, I want to avoid having the form's submit button trigger a page refresh. This is why we will send the form data to the server using an XMLHttpRequest to the same endpoint. This approach is not only clean and elegant but also significantly faster and smoother for the user experience.

Let's start by modifying the form creation in myHandler2():

HTML += `<form method="POST" onsubmit="return saveContact(this);">`;
    HTML += `<fieldset>`;
        HTML += `<legend>Contact Form</legend>`;
        HTML += `<li><label for="txtID">ID:                 </label><input type="text"     class="${record['id']         .field.type}" name="txtID"             id="txtID"          value="${record['id'].value.trim()}" /></li>`;
        HTML += `<li><label for="txtf_fname">First name:    </label><input type="text"     class="${record['f_fname']    .field.type}" name="txtf_fname"        id="txtf_fname"     value="${record['f_fname'].value.trim()}" /></li>`;
        HTML += `<li><label for="txtf_lname">Last name:     </label><input type="text"     class="${record['f_lname']    .field.type}" name="txtf_lname"        id="txtf_lname"     value="${record['f_lname'].value.trim()}" /></li>`;
        HTML += `<li><label for="txtf_birthdate">Birthdate: </label><input type="date"     class="${record['f_birthdate'].field.type}" name="txtf_birthdate"    id="txtf_birthdate" value="${str2date(record['f_birthdate'].value.trim())}" /></li>`;
        HTML += `<li><label for="txtf_rating">Rating:       </label><input type="number"   class="${record['f_rating']   .field.type}" name="txtf_rating"       id="txtf_rating"    value="${record['f_rating'].value}" /></li>`;
        HTML += `<li><label for="txtf_rewards">Rewards:     </label><input type="number"   class="${record['f_rewards']  .field.type}" name="txtf_rewards"      id="txtf_rewards"   value="${record['f_rewards'].value}" /></li>`;

        HTML += `<input type="hidden" name="illicoDB3-table-name"  id="illicoDB3-table-name"  value="${table}" />`;
        HTML += `<input type="hidden" name="illicoDB3-table-recno" id="illicoDB3-table-recno" value="${record['recno'].value}" />`;
    HTML += `</fieldset>`;

    HTML += '<button type="submit" class="btn btn-primary">Submit</button>';

HTML += `</form>`;        

What we need to pay attention to is the onsubmit clause that we attached to the form: onsubmit="return saveContact(this);

So... this is where we need to focus! Here’s an initial code snippet for the saveContact() function; we won't be doing much here: we’ll simply log a small piece of information and return the value 'false', which is essential to prevent the form submission from causing a page refresh.

function saveContact( form )
{
    console.log( "WE MUST SAVE THE DATA OF THE FORM. IT'S NOT DONE AT THE MOMENT" );
    return ( false );
}   /* saveContact( form ) */        

Let's check if this works as anticipated...

Yes, it works perfectly! We continue to embrace TDD (Test Driven Development) to test the functionality immediately, as close to the code creation as possible, or even before it.

Let's go further by introducing our XMLHttpRequest...

    function saveContact( form )
    {
        const data  = new FormData( form );
        const xhr   = new XMLHttpRequest();
        const url   = '/services/module.php';

        xhr.addEventListener("readystatechange",function ()
                                                {
                                                	if ( this.readyState === this.DONE )
                                                	{
                                                        let inError = false;

                                                        if ( this.responseText )
                                                        {
                                                            try
                                                            {
                                                                oJSON = JSON.parse( this.responseText );
                                                            }
                                                            catch ( error )
                                                            {
                                                                console.error( error );
                                                                inError = true;
                                                            }
                                                        }   /* if ( this.responseText ) */
                                                        else    /* Else of ... if ( this.responseText ) */
                                                        {
                                                            inError = true;
                                                        }   /* End of ... Else of ... if ( this.responseText ) */

                                                        if ( ! inError )
                                                        {
                                                            console.log( oJSON );
                                                        }
                                                	}   /* if ( this.readyState === this.DONE ) */
                                                } );
        xhr.open( "POST",url );
        xhr.send( data );

        return ( false );
    }   /* saveContact( form )  */        

There is nothing magical: we create data to send via POST using new FormData, passing the 'form' parameter to it, and we build our request without any further intelligence. When we receive the server's response, we display the received JSON in the console.

Here is the rescue code: the endpoint does not change, and to prevent things from getting messy on this side, we will start managing the different calls in an organized manner, which has led me to reorganize the code. The interesting aspect begins with ... elseif ( isset( $_POST['illicoDB3-table-name'] ) ).

<?php
use \trql\vaesoli\Vaesoli               as v;
use \trql\db\IllicoDB3\IllicoDB3        as IllicoDB3;

if ( ! defined( 'VAESOLI_CLASS_VERSION' ) )
    require_once( '/home/vaesoli/snippet-center/trql.vaesoli.class.php' );

if ( ! defined( 'ILLICODB3_CLASS_VERSION' ) )
    require_once( '/home/vaesoli/snippet-center/trql.illicodb3.class.php' );

if ( ! defined( 'HTTP_STATUS_CODES' ) )
    require_once( '/home/vaesoli/snippet-center/trql.http-error-codes.h.php' );

$errorCode  = HTTP_STATUS_CODE_NOT_IMPLEMENTED;
$fields     = null;
$records    = null;
$tableName  = null;
$messages   = null;
$service    = null;

$DBName     = 'Quitus';
$folder     = '/home/vaesoli/domains/trql.io/quitus/databases/';

$t1 = microtime( true );

if ( true )
{
    if ( isset( $_POST['data-module'] ) )
    {
        $module = trim( $_POST['data-module'] );

        if ( $module === 'clifou' )
        {
            $messages[] = 'CliFou module';

            $oDB = new IllicoDB3();

            //var_dump( $_POST );
            if ( $success = $oDB->open( $DBName,$folder ) )
            {
                $messages[] = 'DB open';

                if ( $table = $oDB->contacts ?? null )
                {
                    $tableName      = $table->name;
                    $messages[]     = 'Contacts table found';
                    $fields         = $table->fields;
                    $table->recno   = (int) $_POST['data-start-recno'] ?? 1;

                    if ( ( $operation = $_POST['data-function'] ) === 'browse' )
                    {
                        $messages[] = 'BROWSE requested';

                        while ( ! $table->eof() )
                        {
                            $records[] = $table->record();
                            $table->skip();
                        }   /* while ( ! $table->eof() )*/

                        $errorCode = HTTP_STATUS_CODE_OK;
                    }   /* if ( ( $operation = $_POST['data-function'] ) === 'browse' ) */
                    elseif ( ( $operation = $_POST['data-function'] ) === 'form' )
                    {
                        $messages[] = 'FORM requested';
                        $records[]  = $table->record();
                        $errorCode = HTTP_STATUS_CODE_OK;
                    }
                }   /* if ( $table = $oDB->contacts ?? null ) */
            }   /* if ( $success = $oDB->open( $DBName,$folder ) ) */
        }   /* if ( $module === 'clifou' ) */
        else
        {
            // Other cases not treated at the moment
        }
    }
    elseif ( false )
    {
        // No worries
    }
    elseif ( isset( $_POST['illicoDB3-table-name'] ) )
    {
        // I only test a well specific form

        $tableName  = trim( $_POST['illicoDB3-table-name'] );

        $messages[] = 'Service received. Not treated at the moment= data NOT saved yet';
        $messages[] = 'Check the prolog.data member to see if all data you set have been recieved successfully';

        $oDB = new IllicoDB3();

        //var_dump( $_POST );
        if ( $success = $oDB->open( $DBName,$folder ) )
        {
            $messages [] = "DB open";
            
            if ( $table = $oDB->$tableName )
            {
                $messages [] = "{$tableName} FOUND";
                $recno = (int) trim( $_POST['illicoDB3-table-recno'] );
                if ( $recno > 0 && $recno <= $table->reccount() )
                {
                    $table->recno   = $recno;

                    $table->f_fname     =                $_POST['txtf_fname'  ];
                    $table->f_lname     =                $_POST['txtf_lname'  ];
                    $table->f_birthdate = v::STR_dionly( $_POST['txtf_birthdate'] );
                    $table->f_rating    =        (float) $_POST['txtf_rating' ];
                    $table->f_rewards   =        (int)   $_POST['txtf_rewards'];

                    $errorCode  = HTTP_STATUS_CODE_OK;
                    $messages[] = 'Data saved';
                    $service    = 'CLIFOU UPDATE RECORD';
                }
            }
        }
    }
    else 
    {
        // Empty at the moment
    }
}

$t2 = microtime( true );

$response = [ 'prolog'  => [ 'status'   => $errorCode   ,
                             'service'  => $service     ,
                             'params'   => $_GET        ,
                             'data'     => $_POST       ,
                             'perf'     => $t2 - $t1    ,
                             'debug'    => $messages    ,

                           ],
              'payload' => [ 'table'    => $tableName   ,
                             'fields'   => $fields      ,
                             'records'  => $records     ,
                           ]
            ];
echo json_encode( $response );
?>        

And ... OK ... it works! We're thrilled! It's done! We have synchronized one module with another!

  • You: How? It’s finished?
  • Me: Well, yes, the browser is well synchronized with the form, right?
  • You: I didn’t see it that way. And, deep down, it will work from the browser to the form but not the other way around! For example, if I update the data with the form, the browser will still have the old values!
  • Me: That will be fine for now.
  • You: No way! It can’t stay like this. It’s true that it’s a slapdash job!
  • Me: [grumbling] ... okay, we’ll fix that!

And you would be a thousand times right!

So ... how are we going to sort this out?" If you need further assistance or adjustments, just let me know!

Publication / Subscription (PubSub)

We will achieve this by implementing a robust Publish/Subscribe mechanism, a system through which modules can publish information (publication aspect) and also subscribe to information publications (subscription aspect).

To keep this article simple and focused on the important principles through examples, I won't be refining the code too much. However, you will have the opportunity to see polished code at the very end of my articles: it's all about my reputation! ??

Let's start by specifying which events can be published. The test code becomes:

<section class="modules">
   <quitus-module id="QM_ac0dd673-82ba-4e61-b021-c5b0443b0f75"
                  class="quitus-module"
                  data-module="clifou"
                  data-function="browse"
                  data-start-recno="1"
                  data-handler="myHandler1"
                  data-publish-event-name="CliFouModuleBrowseChanged"
                  data-listen-event-name="CliFouModuleFormChanged">blablabla</quitus-module>

    <hr />

    <quitus-module id="QM_95d40c32-c2e7-4ed2-b21e-b815b521f9c0"
                  class="quitus-module"
                  data-module="clifou"
                  data-function="form"
                  data-start-recno="88"
                  data-handler="myHandler2"
                  data-publish-event-name="CliFouModuleFormChanged"
                  data-listen-event-name="CliFouModuleBrowseChanged">blablabla</quitus-module>
</section>        

Take a look at these two additional attributes: "data-publish-event-name" and "data-listen-event-name". Actually, the way the two modules have been conceived, one listens to the publish event of the other: this should permit a sort of bi-direction dialog based on events.

To take that into consideration, I needed to update the QuitusModule class:

class QuitusModule extends HTMLElement
/*----------------------------------*/
{
    static observedAttributes = ["data-recno"];

    listenEventName  = null;
    publishEventName = null;


    constructor()
    /*---------*/
    {
        super();
    }

    connectedCallback()
    /*---------------*/
    {
        // Get all attributes
        const attributes = this.attributes;
        const params = [];

        // Get the event names from the attributes
        this.publishEventName = this.getAttribute( 'data-publish-event-name' );
        this.listenEventName  = this.getAttribute( 'data-listen-event-name'  );

        // Add each attribute and its value to aan array of parameters
        for ( let i = 0; i < attributes.length; i++ )
        {
            const attr = attributes[i];
            params.push(`${encodeURIComponent(attr.name)}=${encodeURIComponent(attr.value)}`);
        }

        // Listen for the specified event
        document.addEventListener( this.listenEventName,this.handleEvent.bind( this ) );

        // Create the XMLHttpRequest
        const xhr = new XMLHttpRequest();
        // Endpoint of my service
        const url = '/services/module.php';

        // POST, url, async
        xhr.open( 'POST',url,true );
        xhr.setRequestHeader( 'Content-Type','application/x-www-form-urlencoded' );

        // Handling the response
        xhr.onreadystatechange = () => {
                                            if ( xhr.readyState === 4 && xhr.status === 200 )
                                            {
                                                //console.log('Réponse:',xhr.responseText );
                                                let oJSON = JSON.parse( xhr.responseText );

                                                const handlerName = this.getAttribute('data-handler');

                                                if ( handlerName && typeof window[handlerName] === 'function' )
                                                {
                                                    window[handlerName]( oJSON );
                                                }
                                                else
                                                {
                                                    console.log( 'NO HANDLER',oJSON );
                                                }
                                            }
                                      };

        // Send the request with all parameters
        xhr.send( params.join( '&' ) );
    }

    attributeChangedCallback( name,oldValue,newValue )
    /*----------------------------------------------*/
    {
        console.log(`Attribute ${name} has changed to ${newValue}`);
    }

    // Method to handle incoming events
    handleEvent( event )
    /*----------------*/
    {
        // this refers to the targetModule (the one handling the event)
        this.setAttribute( 'data-start-recno',event.detail.value );
        this.connectedCallback();
    }

    // Method to publish an event with some detail
    broadcastChange( eventName,newValue )
    /*-------------------------------*/
    {
        // Create a custom event with some details (you can customize this as needed)
        const detail = {  name      : eventName     ,
                          value     : newValue      ,
                       };

        const customEvent = new CustomEvent( eventName,{ detail, bubbles: true, composed: true } );

        // Dispatch the custom event
        document.dispatchEvent( customEvent );
    }
}        

What matters are the two properties listenEventName and publishEventName which are set to null. These will hold the names of the events we want to listen to and publish. Such names are extracted from the attributes of the modules:

// Get the event names from the attributes
this.publishEventName = this.getAttribute( 'data-publish-event-name' );
this.listenEventName  = this.getAttribute( 'data-listen-event-name'  );        

The code also makes sure that an event listener is positioned for the event we are interested in:

// Listen for the specified event
document.addEventListener( this.listenEventName,this.handleEvent.bind( this ) );        

There are also these two methods that make it possible to raise an event (publication) and to respond to an event:

 // Method to handle incoming events
handleEvent( event )
/*----------------*/
{
    // this refers to the targetModule (the one handling the event)
    this.setAttribute( 'data-start-recno',event.detail.value );
    this.connectedCallback();
}

// Method to publish an event with some detail
broadcastChange( eventName,newValue )
/*-------------------------------*/
{
    // Create a custom event with some details (you can customize this as needed)
    const detail = {  name      : eventName     ,
                      value     : newValue      ,
                   };

    const customEvent = new CustomEvent( eventName,{ detail, bubbles: true, composed: true } );

    // Dispatch the custom event
    document.dispatchEvent( customEvent );
}        

Since there is only 1 event we can listen to per module (but the detail of the event can vary), there are very few things to do except respond to it by setting the data-start-recno attribute to a new value and then refresh the module by a call to connectedCallback().

How is the event of the browse broadcasted? Well, simply by positioning it in the handler of the browse, naming by updating the myHandler1() function just a bit.

HTML += `<tbody>`;
    for ( let record of records )
    {
        HTML += `<tr onclick="publishEvent( '${id}',${record.recno.value} )">`;
            HTML += `<td class="recno">${record.recno.value}</td>`;
            HTML += `<td class="id">${record.id.value.trim()}</td>`;
            HTML += `<td class="fname">${record.f_fname.value.trim()}</td>`;
            HTML += `<td class="lname">${record.f_lname.value.trim()}</td>`;
            HTML += `<td class="birthdate">${str2date(record.f_birthdate.value)}</td>`;
            HTML += `<td class="rating">${record.f_rating.value}</td>`;
            HTML += `<td class="rewards">${record.f_rewards.value}</td>`;
        HTML += `</tr>`;
    }   /* For each record */
HTML += `</tbody>`;        

Please notice the onclick event of the <tr>...</tr> element: it class the publishEvent() function with the ID of the originating module and the new recno value. Here's the code of publishEvent():

function publishEvent( originatingModuleID,recno )
/*----------------------------------------------*/
{
    const module = document.getElementById( originatingModuleID );

    module.broadcastChange( module.publishEventName,recno );
}        

... which in turns calls the brodcastChange() method of the module: that's the mechanism that will indeed publish the event other modules may be listening to: remember this thing...

// Method to publish an event with some detail
broadcastChange( eventName,newValue )
/*-------------------------------*/
{
    // Create a custom event with some details (you can customize this as needed)
    const detail = {  name      : eventName     ,
                          value     : newValue      ,
                       };

    const customEvent = new CustomEvent( eventName,{ detail, bubbles: true, composed: true } );

    // Dispatch the custom event
    document.dispatchEvent( customEvent );
}        

When this event gets fired, the listeners get triggered which means that the handleEvent( event ) of the 'form' module gets activated. As explained before, this sets a new value in the data-start-recno attribute and forces a refresh (call to connectedCallback()). Ouf. We have arrived.

We Can Go Further

Yep ... we can go further by having a bus of events to serve. This would permit to listen to multiple events, respond to multiple events and the likes. It would be fantastic to have it and actually I started something like that but I refrained from going into that direction because it -could have been overcomplicated for the case we needed to handle.

Here's the tentative code that I have created (NO TEST CONDUCTED):

/* Publication/Subscription Mechanism */
// no need for now class PubSub
// no need for now /*--------*/
// no need for now {
// no need for now     // Constructor method to initialize the PubSub instance
// no need for now     constructor()
// no need for now     {
// no need for now         // Initialize an empty object to hold subscribers for different events
// no need for now         this.subscribers = {};
// no need for now     }
// no need for now 
// no need for now     // Method to subscribe a function to a specific event
// no need for now     subscribe( event,fn )
// no need for now     {
// no need for now         // Check if there are no subscribers for the given event
// no need for now         if ( ! this.subscribers[event] )
// no need for now         {
// no need for now             // If not, create an empty array for that event
// no need for now             this.subscribers[event] = [];
// no need for now         }
// no need for now         // Add the provided function (subscriber) to the array of subscribers for that event
// no need for now         this.subscribers[event].push(fn);
// no need for now     }
// no need for now 
// no need for now     // Method to unsubscribe a function from a specific event
// no need for now     unsubscribe( event,fn )
// no need for now     {
// no need for now         // Check if there are subscribers for the given event
// no need for now         if ( ! this.subscribers[event] )
// no need for now             return; // If not, exit the method
// no need for now 
// no need for now         // Filter out the function (subscriber) from the array of subscribers for that event
// no need for now         this.subscribers[event] = this.subscribers[event].filter(subscriber => subscriber !== fn);
// no need for now     }
// no need for now 
// no need for now     // Method to publish an event and notify all its subscribers with data
// no need for now     publish( event,data )
// no need for now     {
// no need for now         // Check if there are subscribers for the given event
// no need for now         if ( ! this.subscribers[event] )
// no need for now             return; // If not, exit the method
// no need for now 
// no need for now         // Iterate over each subscriber and call it with the provided data
// no need for now         this.subscribers[event].forEach( subscriber => subscriber( data ) );
// no need for now     }
// no need for now }        

Conclusion / Summary

We have developed a suite of ten articles to create a compact business solution called Quitus. This suite comprises four modules: Clients/Suppliers (essentially contacts), Invoices, Products, and Warehouses (stocks). All modules are built on a unified structure, thanks to the PHP classes QuitusSuite and QuitusModule. These modules are so versatile that simply modifying the main table allows them to adapt seamlessly to the data structure implemented in their respective main tables. It goes without saying that our data management class is entirely based on IllicoDB3, which has undergone numerous changes that I will need to document soon in a new article (indeed, there have been many modifications and enhancements).

After demonstrating how PHP code could generate browsing and forms, I wanted to take you on a journey into new horizons: the realm of custom HTML elements. This led us to introduce the concept of components and special tags in HTML, such as <quitus-module></quitus-module>. The fundamental reason for this approach was clear: I wanted a component to communicate bidirectionally with another, much like frameworks such as Angular do. We also had to tackle the challenge of data saving, and I embraced this task wholeheartedly, crafting code that is both elegant and simple enough for anyone to understand.

With all this in our toolkit, we can now expand Quitus with additional modules while continuing to enhance those already developed. I promised you: I won’t abandon this suite! I plan to utilize it as much as possible because, ultimately, only real-world experience can illuminate the actual needs we face. There are two modules calling out to us: DocumentsModule and ExpensesModule. Honestly, I can’t wait to dive into both of them. It seems that expense reports will require a way to store images or PDFs representing them. I already have a solid plan for managing these documents, as I've previously developed something similar for a PMO tool. Regarding expense reports, I also have some experience in this area; however, here I want to integrate Sermo (my gateway to ChatGPT) to recognize documents such as contracts, invoices, purchase orders, restaurant receipts, etc. And the cherry on top would be generating accounting entries based on such documents. Clearly, ideas are flowing!

Take some time to rest; I certainly need it too. And most importantly... let’s keep things uncomplicated! Oh yes ... code is coming now ! I simply need a bit of time to package it to something that makes sense.

Previous article - Next article




要查看或添加评论,请登录

Patrick Boens的更多文章

  • Forging a ChatBot in the Digital Wilderness

    Forging a ChatBot in the Digital Wilderness

    In the relentless landscape of technological evolution, silence is not the absence of action—it is the crucible of…

  • AI Scanning an Invoice

    AI Scanning an Invoice

    This new issue, dedicated to ChatGPT and Sermo (my programming gateway to ChatGPT, Perplexity, and Claude 3.5 Sonnet)…

  • Software Engineers and AI

    Software Engineers and AI

    Through numerous articles written on the subject of artificial intelligence in the context of digital transformations…

  • Using templates to mimic sophisticated object

    Using templates to mimic sophisticated object

    The Power of Templates in IllicoDB3: Revolutionizing Database Structure In the realm of traditional databases, the…

  • More, Faster, Easier...

    More, Faster, Easier...

    In my previous article, I showed you how to create a small todo management app in record time. Now, based on this first…

  • AI at the Developers' Bedside

    AI at the Developers' Bedside

    Let's dive into the world of AI-powered app creation, showcasing just how effortless it can be to bring your ideas to…

    1 条评论
  • AI and IT

    AI and IT

    Here's the question I asked to "Sonar Huge", the AI model of Perplexity.ai: With the advent of AI taking by storm the…

  • The Imperative of AI in Digital Transformation Projects: A Wake-Up Call for Businesses

    The Imperative of AI in Digital Transformation Projects: A Wake-Up Call for Businesses

    In today's rapidly evolving digital landscape, companies that continue to approach their digital transformation and…

  • Data Migration Projects

    Data Migration Projects

    "Mise en bouche" My first interesting encounter with a data migration project dates back to 1999. The tech world was…

  • IllicoDB3 and AI

    IllicoDB3 and AI

    I have mentioned in the past that IllicoDB3 allows the use of artificial intelligence to generate code related to…

社区洞察

其他会员也浏览了