Carmen Dominguez, Author at ProdSens.live https://prodsens.live/author/carmen-dominguez/ News for Project Managers - PMI Sun, 30 Jun 2024 12:20:34 +0000 en-US hourly 1 https://wordpress.org/?v=6.5.5 https://prodsens.live/wp-content/uploads/2022/09/prod.png Carmen Dominguez, Author at ProdSens.live https://prodsens.live/author/carmen-dominguez/ 32 32 Creating a Synchronized Vertical and Horizontal Scrolling Component for Web Apps https://prodsens.live/2024/06/30/creating-a-synchronized-vertical-and-horizontal-scrolling-component-for-web-apps/?utm_source=rss&utm_medium=rss&utm_campaign=creating-a-synchronized-vertical-and-horizontal-scrolling-component-for-web-apps https://prodsens.live/2024/06/30/creating-a-synchronized-vertical-and-horizontal-scrolling-component-for-web-apps/#respond Sun, 30 Jun 2024 12:20:34 +0000 https://prodsens.live/2024/06/30/creating-a-synchronized-vertical-and-horizontal-scrolling-component-for-web-apps/ creating-a-synchronized-vertical-and-horizontal-scrolling-component-for-web-apps

Introduction Microsoft Teams’ mobile agenda page offers a sleek and intuitive interface with synchronized vertical and horizontal scrolling.…

The post Creating a Synchronized Vertical and Horizontal Scrolling Component for Web Apps appeared first on ProdSens.live.

]]>
creating-a-synchronized-vertical-and-horizontal-scrolling-component-for-web-apps

Introduction

Microsoft Teams’ mobile agenda page offers a sleek and intuitive interface with synchronized vertical and horizontal scrolling. This design allows users to scroll through dates horizontally and see the corresponding events in a vertical list. Inspired by this elegant solution, I decided to create a similar component using modern web technologies. While there are many libraries and blogs about synchronized scrolling, they typically handle scrolling in the same direction. This article will show you how to achieve synchronized scrolling in both vertical and horizontal directions.

You can also checkout the live demo

Demo gif

Prerequisites

Before diving in, you should have a basic understanding of React, JavaScript, and Tailwind CSS. Make sure you have Node.js and npm installed on your machine.

Setting Up the Project

First, create a new React project using Create React App or your preferred method.

npm create vite@latest my-sync-scroll-app -- --template react
cd my-sync-scroll-app 
npm install

Next, install Tailwind CSS (optional).

npm install -D tailwindcss npx tailwindcss init

Configure Tailwind CSS by adding the following content to your tailwind.config.js file:

module.exports = { 
    purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'], 
    darkMode: false, 
    theme: { 
        extend: {}, 
    }, 
    variants: { 
        extend: {}, 
    }, 
    plugins: [], 
};

Add the Tailwind directives to your CSS file (src/index.css):

@tailwind base;
@tailwind components;
@tailwind utilities;

Utility Function for Date Generation

Let’s create a utility function to generate a list of dates starting from a given date.

export const generateDates = (startDate, days) => {
  const dates = [];
  for (let i = 0; i < days; i++) {
    const date = new Date(startDate);
    date.setDate(startDate.getDate() + i);
    dates.push(date.toISOString().split("T")[0]); // Format date as YYYY-MM-DD
  }
  return dates;
};

Creating the Horizontal Scroll Component

Let's start by creating the HorizontalScroll component. This component will allow users to scroll through dates horizontally and select a date.

import React, { useEffect, useRef } from "react";

const HorizontalScroll = ({
  dates,
  selectedDate,
  setSelectedDate,
  setSelectFromHorizontal,
}) => {
  const containerRef = useRef();

  useEffect(() => {
    // Automatically scroll to the selected date and center it in the view
    const selectedElement = containerRef.current.querySelector(`.date-item.selected`);
    if (selectedElement) {
      const containerWidth = containerRef.current.offsetWidth;
      const elementWidth = selectedElement.offsetWidth;
      const elementOffsetLeft = selectedElement.offsetLeft;
      const scrollTo = elementOffsetLeft - containerWidth / 2 + elementWidth / 2;
      containerRef.current.scrollTo({
        left: scrollTo,
        behavior: "smooth",
      });
    }
  }, [selectedDate]);

  const handleDateSelection = (index) => {
    setSelectedDate(dates[index]);
    setSelectFromHorizontal(true);
  };

  const onWheel = (e) => {
    const element = containerRef.current;
    if (element) {
      if (e.deltaY === 0) return;
      element.scrollTo({
        left: element.scrollLeft + e.deltaY,
      });
    }
  };

  return (
    
{dates.map((date, index) => { const day = new Date(date).toLocaleString([], { month: "short" }); const d = new Date(date).toLocaleString([], { day: "2-digit" }); return (
handleDateSelection(index)} style={{ backgroundColor: selectedDate === date ? "#90cdf4" : "#f7fafc", borderRadius: selectedDate === date ? "4px" : "0px", }} >

{day}

{d}

); })}
); }; export default HorizontalScroll;

Creating the Vertical Scroll Component

Next, create the VerticalScroll component to display the events for the selected date. This component will synchronize with the HorizontalScroll component to update the displayed events when a date is selected.

import React, { useEffect, useRef, useState } from "react";

const VerticalScroll = ({
  dates,
  onDateChange,
  selectedDate,
  selectFromHorizontal,
  setSelectFromHorizontal,
}) => {
  const containerRef = useRef();
  const [visibleDates, setVisibleDates] = useState([]);
  const [isProgrammaticScroll, setIsProgrammaticScroll] = useState(false);

  useEffect(() => {
    const container = containerRef.current;
    const handleScroll = () => {
      if (isProgrammaticScroll) {
        setIsProgrammaticScroll(false);
        return;
      }
      if (!selectFromHorizontal) {
        // Calculate the date at the top of the vertical scroll
        const topDateIndex = Math.floor(container.scrollTop / 100);
        const topDate = dates[topDateIndex];
        onDateChange(topDate);
      }
      // Calculate the visible dates based on the current scroll position
      const start = Math.floor(container.scrollTop / 100);
      const end = start + Math.ceil(container.clientHeight / 100);
      const visible = dates.slice(start, end);
      setVisibleDates(visible);
    };

    container.addEventListener("scroll", handleScroll);

    return () => container.removeEventListener("scroll", handleScroll);
  }, [dates, isProgrammaticScroll, onDateChange]);

  useEffect(() => {
    setTimeout(() => setSelectFromHorizontal(false), 1000);
  }, [selectedDate]);

  useEffect(() => {
    const selectedIndex = dates.indexOf(selectedDate);
    if (selectedIndex !== -1) {
      // Scroll to the selected date in the vertical scroll
      const scrollTo = selectedIndex * 100;
      setIsProgrammaticScroll(true);
      containerRef.current.scrollTo({
        top: scrollTo,
        behavior: "smooth",
      });
    }
  }, [selectedDate, dates]);

  return (
    
{dates.map((date) => (
{new Date(date).toLocaleString([], { month: "short", day: "2-digit", weekday: "short" })}
{visibleDates.includes(date) ? ( ) : (

No events

)}
))}
); }; const DateContent = ({ date }) => { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const fetchData = async () => { const selectDate = new Date(date); selectDate.setHours(6, 0, 0, 0); const epochStartTimestamp = Math.floor(selectDate.getTime() / 1000); selectDate.setDate(selectDate.getDate() + 3); selectDate.setHours(23, 59, 59, 999); const epochEndTimestamp = Math.floor(selectDate.getTime() / 1000); const queryParams = `?start_timestamp=${epochStartTimestamp}&end_timestamp=${epochEndTimestamp}`; try { const response = await fetch(`https://example.com/api/upcomingShifts${queryParams}`); if (response.status === 200) { const result = await response.json(); setLoading(false); setData((prevData) => [...prevData, ...result.upcomingShifts]); } } catch (error) { console.error("Error fetching data:", error); } }; fetchData(); }, [date]); if (!data) return

Loading...

; return (
{loading ? (
) : ( data.map((d) => (

{d.id}

)) )}
); }; export default VerticalScroll;

Bringing It All Together

Now, let's integrate these components in the main App component.

import React, { useState } from "react";
import HorizontalScroll from "./components/HorizontalScroll";
import VerticalScroll from "./components/VerticalScroll";

const App = () => {
    const dates = generateDates(new Date(), 90);
    const [selectedDate, setSelectedDate] = useState(dates[0]);
    const [selectFromHorizontal, setSelectFromHorizontal] = useState(false);

  // Function to handle date changes from the vertical scroll component
  const handleDateChange = (date) => {
    if (!selectFromHorizontal) {
      setSelectedDate(date);
    }
  };

  return (
    
); }; export default App;

Conclusion

By following this guide, you can create a synchronized vertical and horizontal scrolling component for your web application. This design pattern, inspired by Microsoft Teams' mobile agenda page, enhances the user experience by providing an intuitive and efficient way to navigate through dates and events. Experiment with the components, adjust the styles, and integrate them into your projects to meet your specific needs. Happy coding!

Live Demo

For a live demonstration of the synchronized vertical and horizontal scrolling component, you can explore the demo on CodeSandbox. This interactive sandbox allows you to see the code in action and experiment with the functionality described in this blog.

The post Creating a Synchronized Vertical and Horizontal Scrolling Component for Web Apps appeared first on ProdSens.live.

]]>
https://prodsens.live/2024/06/30/creating-a-synchronized-vertical-and-horizontal-scrolling-component-for-web-apps/feed/ 0
LinKeeper – Lesson 03 – Tests and ressources https://prodsens.live/2024/04/28/linkeeper-lesson-03-tests-and-ressources/?utm_source=rss&utm_medium=rss&utm_campaign=linkeeper-lesson-03-tests-and-ressources https://prodsens.live/2024/04/28/linkeeper-lesson-03-tests-and-ressources/#respond Sun, 28 Apr 2024 09:20:19 +0000 https://prodsens.live/2024/04/28/linkeeper-lesson-03-tests-and-ressources/ linkeeper-–-lesson-03-–-tests-and-ressources

In this episode, we’re going to take FilamentPHP a step further. But first, we’re going to set up…

The post LinKeeper – Lesson 03 – Tests and ressources appeared first on ProdSens.live.

]]>
linkeeper-–-lesson-03-–-tests-and-ressources

In this episode, we’re going to take FilamentPHP a step further.

But first, we’re going to set up our testing environment.

To do this, we’re going to use PestPHP. PestPHP is already present because it was installed at the same time as Laravel.

To get to grips with Pest, we’re going to create two very simple tests. Once we have the basics down, we can develop the rest of our application based on the tests.

What we’ll be looking at today :

  • Prior configuration of Pest

    • Using SQLite for testing
    • Refresh the database for each test
    • Load a default account before each test
    • Remove the tests provided by default by PestPHP
  • Checking the administration home page

    • Running the test
    • Correct the 403 error
    • Run the test again
  • Testing the user profile

    • Run the tests
  • a user can see the list of links associated with them

    • Let’s create our tests for the Links page
    • Let’s create the Links page
  • Conclusion

Prior configuration of Pest

Using SQLite for testing

In order to separate your application’s database from the one used for testing, we’re going to modify the phpunit.xml file located at the root of your project.

Simply uncomment the following two lines:

        
        
         name="DB_CONNECTION" value="sqlite"/>
         name="DB_DATABASE" value=":memory:"/>

This will allow Pest, which is a PHPUnit overlay, to use SQLite as its database engine and use it directly in memory.

Refresh the database for each test

Load a default account before each test

We’re going to be doing a lot of work with FilamentPHP administration. So to avoid repeating for each test that we want to connect with a registered user, we’ll simply tell Pest that for each test, it will act as if it were connected with the user with id #1.

To make this user ex

To do this, we’ll edit the testsTestCase.php file and add the following setUp function:


    protected function setUp(): void
    {
        parent::setUp();

        $this->actingAs(User::find(1));
    }

So before each test is run, the actingAs function will define the user with id #1 as the logged-in user. Here it’s my HappyToDev account.

Remove the tests provided by default by PestPHP

When you install Pest, it provides you with two default tests:

  • testsFeatureExampleTest.php
  • testsUnitExampleTest.php

You can delete them or keep them for inspiration.

Checking the administration home page

In this test, we’ll go to the /admin page to check that

  • an HTTP 200 code is received
  • we can see ‘Dashboard’ on the page
  • we can see the name of our application ‘LinKeeper’ on the page

We’ll start by creating a test file using Artisan.

php artisan pest:test GeneralAdminPageTest

By default, the tests created will be stored directly in the testsFeature directory.

Here are the contents of the file :



use IlluminateSupportFacadesConfig;

it('can view admin page', function () {
    $response = $this->get('/admin');

    $response
        ->assertStatus(200)
        ->assertSee('Dashboard')
        ->assertSee(Config('APP_NAME'));
});

Explanation:

We ask for get access to the /admin page and we get the response.

In this response, we check that we do indeed have an HTTP 200 code, that in the HTML code returned we do indeed have the word Dashboard and that we do indeed see the name of the application defined in our .env file, in this case LinKeeper. To do this, use the config() helper.

Running the test

To run the test, at the root of your project, run the following command:

./vendor/bin/pest

Normally you should get a 403 error, so your test will not pass.

Error 403

Correct the 403 error

To correct this, you need to read the FilamentPHP documentation which indicates that when you go into production, you need to modify the User model to avoid hitting a wall with the 403 error.

I’ll leave it to you to implement the necessary changes so that your test can pass.

If you encounter the slightest difficulty, let me know in the comments.

Run the test again

If you have correctly made the changes requested in the documentation, you should get this result when you run your tests.

Test OK

Testing the user profile

We saw in lesson 2 that we normally have access to the user’s profile. Let’s set up a test to check this.

php artisan pest:test UserProfileTest

and add the following code:

it('has user profile page', function () {
    actingAs(User::find(1))->get('/admin/profile')
    ->assertStatus(200)
    ->assertSee('Profile')
    ->assertSee('Save changes');
});

This will allow us to check that when I’m logged in with the user with id #1 (my user) I do indeed get an HTTP 200 code when I go to the /admin/profile page and I can see the words: ‘Profile’ and ‘Save changes’.

This is a very simple test, but it validates that the user does indeed have access to this page.

Run the tests

As before, run the appropriate command in your terminal:

./vendor/bin/pest

and you should get the following result:

Tests OK

Now that we’ve taken our first steps with tests, we’ll be able to take a test-driven development approach using PestPHP throughout this project.

This is the next stage in the development of our application.

As we are using FilamentPHP for the backend, a number of features are being developed by the FilamentPHP teams and the aim here will not be to test how well Filament works as I have no doubt that this has been tested extensively.

To view the list of links, the user will have a Links link in the dashboard menu and this link, by FilamentPHP convention, will lead to an admin/links address.

Let’s create a new test, using artisan :

php artisan pest:test PageLinksTest

First of all, we want to make sure that the page is accessible.

To do this, we’re going to add the following code to our PageLinksTest file:

it("can access the user's links page and see 'Links' on the page", function () {
    $response = $this->get('/admin/links');

    $response
        ->assertOk()
        ->assertSeeText('Links');
});

This code will ask PestPHP to load the /admin/links page and to check firstly that the page returns the HTTP code 200 and then to ensure that the word ‘Links’ is present in the page.

You can run the tests with the --filter option to execute only the test that matches the regex passed in parameter (see doc). For example with :

./vendor/bin/pest --filter "links page"

PestPHP will only run our last test because ‘links page’ is present in its description.

This avoids having to run all the tests each time, especially when you have to iterate several times when setting up your test.

Tests KO

The test fails and that’s normal because the Links page doesn’t exist at the moment.

But now that the test is here and has failed, it tells us exactly what to do:

Expected response status code [200] but received 404.

We were expecting an HTTP 200 code and instead we get a 404, which means that the requested page doesn’t exist.

We simply need to create it.

The ‘Links’ page will be a FilamentPHP administration page. It will therefore be a resource.

If I take the definition from the tutorial provided with PestPHP :

In Filament, resources are static classes used to build CRUD interfaces for your Eloquent models. They describe how administrators can interact with data from your panel using tables and forms.

We therefore understand that each Eloquent model will be associated with a resource in FilamentPHP.

It is therefore time to create our resource associated with our Link model, which is itself linked to our ‘links’ table in our database.

php artisan make:filament-resource Link

In our application tree, under app/Filament/Resources, we can see a LinkResource directory and a LinkResource.php file.

Arborescence Filament Resources

The LinkResource directory itself has a Pages sub-directory which contains the CreateLink.php, EditLink.php and ListLinks.php files. We’ll talk about these later in this tutorial, so don’t worry about these extra files for now.

FilamentPHP, via this Artisan command, will generate the following skeleton for you:



namespace AppFilamentResources;

use AppFilamentResourcesLinkResourcePages;
use AppFilamentResourcesLinkResourceRelationManagers;
use AppModelsLink;
use FilamentForms;
use FilamentFormsForm;
use FilamentResourcesResource;
use FilamentTables;
use FilamentTablesTable;
use IlluminateDatabaseEloquentBuilder;
use IlluminateDatabaseEloquentSoftDeletingScope;

class LinkResource extends Resource
{
    protected static ?string $model = Link::class;

    protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';

    public static function form(Form $form): Form
    {
        return $form
            ->schema([
                //
            ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                //
            ])
            ->filters([
                //
            ])
            ->actions([
                TablesActionsEditAction::make(),
            ])
            ->bulkActions([
                TablesActionsBulkActionGroup::make([
                    TablesActionsDeleteBulkAction::make(),
                ]),
            ]);
    }

    public static function getRelations(): array
    {
        return [
            //
        ];
    }

    public static function getPages(): array
    {
        return [
            'index' => PagesListLinks::route('/'),
            'create' => PagesCreateLink::route('/create'),
            'edit' => PagesEditLink::route('/{record}/edit'),
        ];
    }
}

Let’s not worry about that for the moment. But let’s run our test again:

./vendor/bin/pest --filter "links page"

We can see that this time our test is green.

Tests OK

If we go to the admin/links page, we can see that the page does exist (HTTP code 200) and that it does indeed say ‘Links’. This is why our test turns green.

Page admin/links

However, we soon realise that something isn’t quite right. There seem to be records but nothing is visible in the table that shows us the links.

This is normal, we’ll need to configure our resource file.

Conclusion

We’ll stop here for episode 3.

In this chapter, we started doing TDD for our application development and touched on resources in FilamentPHP.

In the next episode, we’ll continue our testing strategy with PestPHP and get to know FilamentPHP’s resources better.

As usual, I look forward to your comments below.

See you soon.

The post LinKeeper – Lesson 03 – Tests and ressources appeared first on ProdSens.live.

]]>
https://prodsens.live/2024/04/28/linkeeper-lesson-03-tests-and-ressources/feed/ 0
SUNDAY REWIND: The art of product management with Ken Norton https://prodsens.live/2024/04/28/sunday-rewind-the-art-of-product-management-with-ken-norton/?utm_source=rss&utm_medium=rss&utm_campaign=sunday-rewind-the-art-of-product-management-with-ken-norton https://prodsens.live/2024/04/28/sunday-rewind-the-art-of-product-management-with-ken-norton/#respond Sun, 28 Apr 2024 09:20:02 +0000 https://prodsens.live/2024/04/28/sunday-rewind-the-art-of-product-management-with-ken-norton/ sunday-rewind:-the-art-of-product-management-with-ken-norton

This week’s Sunday Rewind is an ask-me anything interview from 2022 with product coach Ken Norton. In it,…

The post SUNDAY REWIND: The art of product management with Ken Norton appeared first on ProdSens.live.

]]>
sunday-rewind:-the-art-of-product-management-with-ken-norton

This week’s Sunday Rewind is an ask-me anything interview from 2022 with product coach Ken Norton. In it, he answers questions on the importance of soft skills, dealing with imposter syndrome and more besides. Read more »

The post SUNDAY REWIND: The art of product management with Ken Norton appeared first on Mind the Product.

The post SUNDAY REWIND: The art of product management with Ken Norton appeared first on ProdSens.live.

]]>
https://prodsens.live/2024/04/28/sunday-rewind-the-art-of-product-management-with-ken-norton/feed/ 0
How to Write AI Content Optimized for E-E-A-T https://prodsens.live/2024/04/24/how-to-write-ai-content-optimized-for-e-e-a-t/?utm_source=rss&utm_medium=rss&utm_campaign=how-to-write-ai-content-optimized-for-e-e-a-t https://prodsens.live/2024/04/24/how-to-write-ai-content-optimized-for-e-e-a-t/#respond Wed, 24 Apr 2024 10:19:50 +0000 https://prodsens.live/2024/04/24/how-to-write-ai-content-optimized-for-e-e-a-t/ how-to-write-ai-content-optimized-for-e-e-a-t

Worried that your AI content might spook your users or not meet Google’s standards? Use this guide to…

The post How to Write AI Content Optimized for E-E-A-T appeared first on ProdSens.live.

]]>
how-to-write-ai-content-optimized-for-e-e-a-t

Worried that your AI content might spook your users or not meet Google’s standards? Use this guide to build trust and authority into your AI content.

The post How to Write AI Content Optimized for E-E-A-T appeared first on ProdSens.live.

]]>
https://prodsens.live/2024/04/24/how-to-write-ai-content-optimized-for-e-e-a-t/feed/ 0