<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bojan Stanojevic</title>
    <description>The latest articles on DEV Community by Bojan Stanojevic (@dellboyan).</description>
    <link>https://dev.to/dellboyan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F654430%2F09251cde-2931-4eec-8923-7a53fa15bdce.jpg</url>
      <title>DEV Community: Bojan Stanojevic</title>
      <link>https://dev.to/dellboyan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dellboyan"/>
    <language>en</language>
    <item>
      <title>Shadcn infinite scroll example</title>
      <dc:creator>Bojan Stanojevic</dc:creator>
      <pubDate>Wed, 25 Sep 2024 16:08:22 +0000</pubDate>
      <link>https://dev.to/dellboyan/shadcn-infinite-scroll-example-1o70</link>
      <guid>https://dev.to/dellboyan/shadcn-infinite-scroll-example-1o70</guid>
      <description>&lt;p&gt;In this post, let's explore how you can add infinite scroll functionality to your app using shadcn. While infinite scroll isn't available by default in the shadcn/ui library, it's easy to implement yourself. Whether or not this feature will be added in the future is unclear, but for now, you can seamlessly add it to your project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create the Infinite Scroll Component
&lt;/h2&gt;

&lt;p&gt;Start by adding an &lt;a href="https://shadcnui-expansions.typeart.cc/docs/infinite-scroll" rel="noopener noreferrer"&gt;infinite scroll&lt;/a&gt; component in the components/ui folder. Name the file infinite-scroll.tsx. This will serve as the container that listens for scroll events and loads additional content when the user reaches the end of the viewport.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import * as React from 'react';

interface InfiniteScrollProps {
  isLoading: boolean;
  hasMore: boolean;
  next: () =&amp;gt; unknown;
  threshold?: number;
  root?: Element | Document | null;
  rootMargin?: string;
  reverse?: boolean;
  children?: React.ReactNode;
}

export default function InfiniteScroll({
  isLoading,
  hasMore,
  next,
  threshold = 1,
  root = null,
  rootMargin = '0px',
  reverse,
  children,
}: InfiniteScrollProps) {
  const observer = React.useRef&amp;lt;IntersectionObserver&amp;gt;();
  // This callback ref will be called when it is dispatched to an element or detached from an element,
  // or when the callback function changes.
  const observerRef = React.useCallback(
    (element: HTMLElement | null) =&amp;gt; {
      let safeThreshold = threshold;
      if (threshold &amp;lt; 0 || threshold &amp;gt; 1) {
        console.warn(
          'threshold should be between 0 and 1. You are exceed the range. will use default value: 1',
        );
        safeThreshold = 1;
      }
      // When isLoading is true, this callback will do nothing.
      // It means that the next function will never be called.
      // It is safe because the intersection observer has disconnected the previous element.
      if (isLoading) return;

      if (observer.current) observer.current.disconnect();
      if (!element) return;

      // Create a new IntersectionObserver instance because hasMore or next may be changed.
      observer.current = new IntersectionObserver(
        (entries) =&amp;gt; {
          if (entries[0].isIntersecting &amp;amp;&amp;amp; hasMore) {
            next();
          }
        },
        { threshold: safeThreshold, root, rootMargin },
      );
      observer.current.observe(element);
    },
    [hasMore, isLoading, next, threshold, root, rootMargin],
  );

  const flattenChildren = React.useMemo(() =&amp;gt; React.Children.toArray(children), [children]);

  return (
    &amp;lt;&amp;gt;
      {flattenChildren.map((child, index) =&amp;gt; {
        if (!React.isValidElement(child)) {
          process.env.NODE_ENV === 'development' &amp;amp;&amp;amp;
            console.warn('You should use a valid element with InfiniteScroll');
          return child;
        }

        const isObserveTarget = reverse ? index === 0 : index === flattenChildren.length - 1;
        const ref = isObserveTarget ? observerRef : null;
        // @ts-ignore ignore ref type
        return React.cloneElement(child, { ref });
      })}
    &amp;lt;/&amp;gt;
  );
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Set Up the Viewport
&lt;/h2&gt;

&lt;p&gt;You'll want to ensure that the infinite scroll viewport is set up correctly so that the component tracks when the user has scrolled to the bottom. You can do this by leveraging event listeners or hooks to detect the position of the viewport relative to the content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Create an Example Page
&lt;/h2&gt;

&lt;p&gt;Next, create an example page, and copy the code provided below into this page. Here, you'll import your InfiniteScroll component and make sure to install the lucide-react library for the loading spinner, or alternatively, use any icon library of your choice. This will handle the loading state while fetching new content.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'use client';
import React from 'react';
import InfiniteScroll from '@/components/ui/infinite-scroll';
import { Loader2 } from 'lucide-react';

interface DummyProductResponse {
  products: DummyProduct[];
  total: number;
  skip: number;
  limit: number;
}

interface DummyProduct {
  id: number;
  title: string;
  price: string;
}

const Product = ({ product }: { product: DummyProduct }) =&amp;gt; {
  return (
    &amp;lt;div className="flex w-full flex-col gap-2 rounded-lg border-2 border-gray-200 p-2"&amp;gt;
      &amp;lt;div className="flex gap-2"&amp;gt;
        &amp;lt;div className="flex flex-col justify-center gap-1"&amp;gt;
          &amp;lt;div className="font-bold text-primary"&amp;gt;
            {product.id} - {product.title}
          &amp;lt;/div&amp;gt;
          &amp;lt;div className="text-sm text-muted-foreground"&amp;gt;{product.price}&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

const InfiniteScrollDemo = () =&amp;gt; {
  const [page, setPage] = React.useState(0);
  const [loading, setLoading] = React.useState(false);
  const [hasMore, setHasMore] = React.useState(true);
  const [products, setProducts] = React.useState&amp;lt;DummyProduct[]&amp;gt;([]);

  const next = async () =&amp;gt; {
    setLoading(true);

    /**
     * Intentionally delay the search by 800ms before execution so that you can see the loading spinner.
     * In your app, you can remove this setTimeout.
     **/
    setTimeout(async () =&amp;gt; {
      const res = await fetch(
        `https://dummyjson.com/products?limit=3&amp;amp;skip=${3 * page}&amp;amp;select=title,price`,
      );
      const data = (await res.json()) as DummyProductResponse;
      setProducts((prev) =&amp;gt; [...prev, ...data.products]);
      setPage((prev) =&amp;gt; prev + 1);

      // Usually your response will tell you if there is no more data.
      if (data.products.length &amp;lt; 3) {
        setHasMore(false);
      }
      setLoading(false);
    }, 800);
  };
  return (
    &amp;lt;div className="max-h-[300px] w-full  overflow-y-auto px-10"&amp;gt;
      &amp;lt;div className="flex w-full flex-col items-center  gap-3"&amp;gt;
        {products.map((product) =&amp;gt; (
          &amp;lt;Product key={product.id} product={product} /&amp;gt;
        ))}
        &amp;lt;InfiniteScroll hasMore={hasMore} isLoading={loading} next={next} threshold={1}&amp;gt;
          {hasMore &amp;amp;&amp;amp; &amp;lt;Loader2 className="my-4 h-8 w-8 animate-spin" /&amp;gt;}
        &amp;lt;/InfiniteScroll&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default InfiniteScrollDemo;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Load Data Dynamically
&lt;/h2&gt;

&lt;p&gt;In the example page, we're using some dummy JSON data, but this setup can easily be extended to handle any type of data source you're working with. The infinite scroll viewport will detect when users reach the bottom and dynamically load more data, ensuring a smooth, seamless scrolling experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;And that's it! With this shadcn infinite scroll viewport example, you don't need any complex dependencies, and the setup is quick and easy. This approach is flexible and can be adapted to various use cases in your app.&lt;/p&gt;

&lt;p&gt;If you enjoyed this post I'd love it if you could give me a follow on Twitter by clicking on the button below, or browse agency website - &lt;a href="http://kodawarians.com/" rel="noopener noreferrer"&gt;Kodawarians &lt;/a&gt;to learn more.&lt;br&gt;
&lt;a href="https://x.com/DellBoyan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijm0wztow2qko3oyvszr.png" alt="Dellboyan Twitter" width="300" height="74"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Nextjs robots txt location</title>
      <dc:creator>Bojan Stanojevic</dc:creator>
      <pubDate>Sat, 17 Aug 2024 15:28:35 +0000</pubDate>
      <link>https://dev.to/dellboyan/nextjs-robots-txt-location-3g9</link>
      <guid>https://dev.to/dellboyan/nextjs-robots-txt-location-3g9</guid>
      <description>&lt;p&gt;One of the main reasons developers use popular JavaScript frameworks is the simplicity and speed to get a website up and running quickly which is also SEO optimized. But then, when you need to configure very basic things like sitemap, robots.txt or any script, turns out you need to investigate a bit to figure out how to do it. I've noticed people asking about the robots.txt location for Next.js so I thought I'd write a short post about it.&lt;/p&gt;

&lt;p&gt;At least for setting up this file, this is very straightforward. All you have to do, if you're using Next.js version 13 or bigger and App router is to go to the App folder which is the root of your app, and inside of it create a new file called robots.txt&lt;/p&gt;

&lt;p&gt;The most basic robots.txt looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-agent: *
Allow: /
Sitemap: https://YOUR_DOMAIN_NAME.com/YOUR_SITEMAP.xml              
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;User-agent: This directive specifies the web crawler to which the rule applies.&lt;/li&gt;
&lt;li&gt;*: The asterisk * is a wildcard that represents "all" user agents. In other words, any web crawler or bot, regardless of its name or origin, should follow the rules defined after this line.&lt;/li&gt;
&lt;li&gt;Allow: /: This explicitly allows web crawlers to access the entire website, starting from the root directory.&lt;/li&gt;
&lt;li&gt;The Sitemap: directive in a robots.txt file is used to provide the location of your website's XML sitemap to search engine crawlers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Inside the robots.txt file you can also define certain routes that should not be crawled with this directive, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Disallow: /admin/
Disallow: /private/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's not really clear to me why the Next.js team didn't include at least a basic robots.txt file, from &lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/metadata/robots" rel="noopener noreferrer"&gt;Next.js docs&lt;/a&gt; they also explain how to generate dynamic robots.js/robots.ts file, but i think static robots.txt is sufficient for most of the websites out there.&lt;/p&gt;

&lt;p&gt;Since we are talking about robots.txt I thought I'd mention sitemaps as well. Like robots.txt, sitemap.xml is also located at the root of your project inside the App folder and is where important for SEO and your website rankings. To generate a static sitemap, deploy your website and use some of the many free services online. I can recommend ScreamingFrog application that includes a sitemap generator tool that I use most often. From my experience this is the best tool to get a sitemap for smaller websites, especially when you are working with a JavaScript project, for larger projects, a better idea is to &lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap#generating-a-sitemap-using-code-js-ts" rel="noopener noreferrer"&gt;generate a sitemap using code.&lt;/a&gt;. When you get your xml file just upload it to the App folder and deploy your app. Finally, once you have your sitemap location you should upload that URL to Google Search Console inside the Sitemaps section to get your website indexed quicker.&lt;/p&gt;

&lt;p&gt;If you need any help with your Next.js or SEO project we can work together, &lt;a href="//kodawarians.com"&gt;learn more here.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you enjoyed this post I'd love it if you could give me a follow on Twitter by clicking on the button below! :)&lt;br&gt;
&lt;a href="https://x.com/DellBoyan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijm0wztow2qko3oyvszr.png" alt="Dellboyan Twitter" width="300" height="74"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to add shadcn to existing project</title>
      <dc:creator>Bojan Stanojevic</dc:creator>
      <pubDate>Thu, 08 Aug 2024 22:19:24 +0000</pubDate>
      <link>https://dev.to/dellboyan/how-to-add-shadcn-to-existing-project-4npn</link>
      <guid>https://dev.to/dellboyan/how-to-add-shadcn-to-existing-project-4npn</guid>
      <description>&lt;p&gt;If you are a web developer, chances are you heard about shadcn/ui, one of the most popular component libraries based of Radix UI. In this post we'll explore how to add shadcn to existing project.&lt;/p&gt;

&lt;p&gt;Depending on how your project is setup and what framework you are using, adding shadcn to your existing project will vary. When using shadcn Typescript is recommended when using this library. Nevertheless, JavaScript version is also available.&lt;/p&gt;

&lt;p&gt;To add shadcn to your project first you will have to install Tailwind CSS if your project is not using it, since shadcn components are styled with it.&lt;/p&gt;

&lt;p&gt;To setup Tailwind CSS &lt;a href="https://tailwindcss.com/docs/installation" rel="noopener noreferrer"&gt;follow the installation instructions from their website.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Shadcn and Frameworks
&lt;/h2&gt;

&lt;p&gt;If you are using Next.js, Vite, Remix, Astro or Laravel, run shadcn-ui to setup your project with this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx shadcn-ui@latest init

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll have to answer a couple of questions to finish the setup depending on your project, like choosing Typescript or Javascript, whatever your project is using.&lt;/p&gt;

&lt;p&gt;Afterwards you'll be able to install &lt;a href="https://ui.shadcn.com/docs/components" rel="noopener noreferrer"&gt;any shadcn component&lt;/a&gt; you want, for example to add button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx shadcn-ui@latest add button

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then just import it from components/ui to use it in your project. &lt;/p&gt;

&lt;h2&gt;
  
  
  Shadcn Manual Installation
&lt;/h2&gt;

&lt;p&gt;To install shadcn manually for example in a React project again make sure Tailwind CSS is installed correctly.&lt;/p&gt;

&lt;p&gt;Then add dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
npm install tailwindcss-animate class-variance-authority clsx tailwind-merge

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add icon library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
npm install lucide-react

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure path aliases:&lt;/p&gt;

&lt;p&gt;tsconfig.json&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure tailwind.config.js&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { fontFamily } = require("tailwindcss/defaultTheme")

/** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: ["class"],
  content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"],
  theme: {
    container: {
      center: true,
      padding: "2rem",
      screens: {
        "2xl": "1400px",
      },
    },
    extend: {
      colors: {
        border: "hsl(var(--border))",
        input: "hsl(var(--input))",
        ring: "hsl(var(--ring))",
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
        primary: {
          DEFAULT: "hsl(var(--primary))",
          foreground: "hsl(var(--primary-foreground))",
        },
        secondary: {
          DEFAULT: "hsl(var(--secondary))",
          foreground: "hsl(var(--secondary-foreground))",
        },
        destructive: {
          DEFAULT: "hsl(var(--destructive))",
          foreground: "hsl(var(--destructive-foreground))",
        },
        muted: {
          DEFAULT: "hsl(var(--muted))",
          foreground: "hsl(var(--muted-foreground))",
        },
        accent: {
          DEFAULT: "hsl(var(--accent))",
          foreground: "hsl(var(--accent-foreground))",
        },
        popover: {
          DEFAULT: "hsl(var(--popover))",
          foreground: "hsl(var(--popover-foreground))",
        },
        card: {
          DEFAULT: "hsl(var(--card))",
          foreground: "hsl(var(--card-foreground))",
        },
      },
      borderRadius: {
        lg: `var(--radius)`,
        md: `calc(var(--radius) - 2px)`,
        sm: "calc(var(--radius) - 4px)",
      },
      fontFamily: {
        sans: ["var(--font-sans)", ...fontFamily.sans],
      },
      keyframes: {
        "accordion-down": {
          from: { height: "0" },
          to: { height: "var(--radix-accordion-content-height)" },
        },
        "accordion-up": {
          from: { height: "var(--radix-accordion-content-height)" },
          to: { height: "0" },
        },
      },
      animation: {
        "accordion-down": "accordion-down 0.2s ease-out",
        "accordion-up": "accordion-up 0.2s ease-out",
      },
    },
  },
  plugins: [require("tailwindcss-animate")],
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update globals.css file with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 47.4% 11.2%;

    --muted: 210 40% 96.1%;
    --muted-foreground: 215.4 16.3% 46.9%;

    --popover: 0 0% 100%;
    --popover-foreground: 222.2 47.4% 11.2%;

    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;

    --card: 0 0% 100%;
    --card-foreground: 222.2 47.4% 11.2%;

    --primary: 222.2 47.4% 11.2%;
    --primary-foreground: 210 40% 98%;

    --secondary: 210 40% 96.1%;
    --secondary-foreground: 222.2 47.4% 11.2%;

    --accent: 210 40% 96.1%;
    --accent-foreground: 222.2 47.4% 11.2%;

    --destructive: 0 100% 50%;
    --destructive-foreground: 210 40% 98%;

    --ring: 215 20.2% 65.1%;

    --radius: 0.5rem;
  }

  .dark {
    --background: 224 71% 4%;
    --foreground: 213 31% 91%;

    --muted: 223 47% 11%;
    --muted-foreground: 215.4 16.3% 56.9%;

    --accent: 216 34% 17%;
    --accent-foreground: 210 40% 98%;

    --popover: 224 71% 4%;
    --popover-foreground: 215 20.2% 65.1%;

    --border: 216 34% 17%;
    --input: 216 34% 17%;

    --card: 224 71% 4%;
    --card-foreground: 213 31% 91%;

    --primary: 210 40% 98%;
    --primary-foreground: 222.2 47.4% 1.2%;

    --secondary: 222.2 47.4% 11.2%;
    --secondary-foreground: 210 40% 98%;

    --destructive: 0 63% 31%;
    --destructive-foreground: 210 40% 98%;

    --ring: 216 34% 17%;

    --radius: 0.5rem;
  }
}

@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
    font-feature-settings: "rlig" 1, "calt" 1;
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally add cn helper to your lib/utils.ts&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then just install the desired component &lt;a href="https://ui.shadcn.com/docs/components" rel="noopener noreferrer"&gt;from here &lt;/a&gt;and follow instructions for each component.&lt;/p&gt;

&lt;p&gt;That's all, I use shadcn in almost all my web projects now, it has become very popular because of how easy it is to use and customize, checkout how I created a custom shadcn-date-picker component from Select and Scrollarea component &lt;a href="https://www.kodawarians.com/resources/shadcn-date-picker" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let me know if you had any issues adding this library to your project.&lt;/p&gt;

&lt;p&gt;If you enjoyed this post I'd love it if you could give me a follow on Twitter by clicking on the button below! :)&lt;br&gt;
&lt;a href="https://x.com/DellBoyan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijm0wztow2qko3oyvszr.png" alt="Dellboyan Twitter" width="300" height="74"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>shadcn</category>
      <category>nextjs</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Shadcn Date Picker with custom implementation</title>
      <dc:creator>Bojan Stanojevic</dc:creator>
      <pubDate>Sun, 04 Aug 2024 14:08:08 +0000</pubDate>
      <link>https://dev.to/dellboyan/shadcn-date-picker-sucks-so-ive-built-my-own-3pgd</link>
      <guid>https://dev.to/dellboyan/shadcn-date-picker-sucks-so-ive-built-my-own-3pgd</guid>
      <description>&lt;p&gt;Shadcn Date Picker that comes with this component library definitely does not suck, but there are a couple of use cases where using this component is not ideal, for example, selecting date of birth, let's explore why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shadcn implementation
&lt;/h2&gt;

&lt;p&gt;If you look at the example from &lt;a href="https://ui.shadcn.com/docs/components/date-picker" rel="noopener noreferrer"&gt;shadcn/ui date picker&lt;/a&gt;, selecting your Date of Birth would mean that you would have to click on the left arrow for ages to get to let's say 1983. It's not possible to select a year nor month. Even if you could, examples of these implementations are not very intuitive for users, from my experience.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flqbn8vwl6ez6pqpcqew4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flqbn8vwl6ez6pqpcqew4.png" alt="shadcn date picker" width="800" height="629"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;Date Picker from shadcn was built using a combination of Popover and the Calendar. Instead of using Calendar I am using Select and ScrollArea components. And that looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftzjp27hcqf3u6wvx9iea.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftzjp27hcqf3u6wvx9iea.png" alt="Shadcn Date Picker with Shadcn" width="387" height="76"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see user can select day, month and a year, which in my opinion is faster and easier. You can check out the demo &lt;a href="https://www.kodawarians.com/resources/shadcn-date-picker" rel="noopener noreferrer"&gt;here.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use
&lt;/h2&gt;

&lt;p&gt;To use this component just install Select and ScrollArea components (and shadcn obviously). Copy shadcn-date-picker.tsx from &lt;a href="https://github.com/stanojevicbojan/shadcn-date-picker/blob/main/src/components/shadcn-date-picker.tsx" rel="noopener noreferrer"&gt;here&lt;/a&gt; and save it inside /components. Finally, import ShadcnDatePicker in your page, and use it, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; &amp;lt;ShadcnDatePicker
    startYear={1930}
    endYear={2030}
    selected={date}
    onSelect={setDate}
 /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, you can select start year and end year based on your needs, date picker also has some validation, for example it will throw an error if user selects 31/2.&lt;/p&gt;

&lt;p&gt;That's it, hope you enjoyed this post! If you tried this simple component If you enjoyed this post I'd love it if you could give me a follow on Twitter by clicking on the button below! :)&lt;br&gt;
&lt;a href="https://x.com/DellBoyan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijm0wztow2qko3oyvszr.png" alt="Dellboyan Twitter" width="300" height="74"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>react</category>
      <category>shadcn</category>
    </item>
    <item>
      <title>How to setup Mixpanel Analytics in Next.js</title>
      <dc:creator>Bojan Stanojevic</dc:creator>
      <pubDate>Mon, 29 Jul 2024 21:48:53 +0000</pubDate>
      <link>https://dev.to/dellboyan/how-to-setup-mixpanel-analytics-in-nextjs-46</link>
      <guid>https://dev.to/dellboyan/how-to-setup-mixpanel-analytics-in-nextjs-46</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Mixpanel is a powerful, user-friendly analytics platform that can help anyone looking to better understand user behavior and make data-driven decisions. Unlike traditional page-view focused analytics tools, Mixpanel specializes in event-based tracking with a dashboard that is so easy to use you don't have to spend any time to learn how to use it. In this post we'll go into why you should consider this tool and how to integrate it with your Next.js application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why to choose Mixpanel
&lt;/h2&gt;

&lt;p&gt;There are many cool features that I love about Mixpanel and you can check some of them below, but main reason for me is a generous free tier. Basically you don't have to spend any money on this tool until you scaled so much that it makes sense. You get 20 million events per month and most reports are available on the free plan. Out of all the other analytics platforms I checked out this is by far the best. &lt;a href="https://amplitude.com/pricing#pricing-slider-container" rel="noopener noreferrer"&gt;Amplitude &lt;/a&gt;focuses on monthly tracked users and is more expensive, &lt;a href="https://www.appsflyer.com/" rel="noopener noreferrer"&gt;Appsflyer &lt;/a&gt;does not focus on product analytics primarily and has a much wider scope of services, &lt;a href="https://posthog.com/pricing" rel="noopener noreferrer"&gt;Posthog &lt;/a&gt;only gives you 1 million events on a free plan if you opt in for cloud hosting, and &lt;a href="https://plausible.io/#pricing" rel="noopener noreferrer"&gt;Plausible &lt;/a&gt; is based on page-view tracking and does not offer free tier. There is also Google Analytics 4 which I avoid like the plague and will not even consider for product analytics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key features of Mixpanel
&lt;/h2&gt;

&lt;p&gt;Besides the pricing Mixpanel has some awesome features that will make tracking easy for you:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event-based tracking&lt;/strong&gt;: With event-based tracking you can track page views, but also anything else you want, you also have full control what page views you want to track which is great because it gives you more control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-time data&lt;/strong&gt;: Unlike GA4 where data can lag quite a bit, you get data right away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linking user data&lt;/strong&gt;: If you are authenticating users, Mixpanel has the ability to link events before their authenticated with events when they authenticated so you get better data flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reports&lt;/strong&gt;: With Mixpanel on the free plan you get four types of reports: Funnels, Insights, Flows and Retention. These reports allow you to track drop offs, specific events and how well your app retains users over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-platform tracking&lt;/strong&gt;: Analyze user behavior across web, android and ios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server-Side Tracking or Client-Side?
&lt;/h2&gt;

&lt;p&gt;Mixpanel has the ability to track events on the server-side and on the cient-side as well. You can also use a hybrid approach where you'll send events from client and server side as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.mixpanel.com/docs/tracking-methods/choosing-the-right-method" rel="noopener noreferrer"&gt;What Mixpanel suggests&lt;/a&gt; is to send every event server-side when possible, the reason for this is data accuracy. Basically Adblockers pose an issue when sending data client-side, they block those events, since Mixpanel is a well known tracking tool. This leads to you losing data even up to 30%. One solution is to use a proxy server, but the best option is to send data server side which is what we'll focus on in this tutorial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Mixpanel in Next.js
&lt;/h2&gt;

&lt;p&gt;To setup Mixpanel first register an account at Mixpanel and get your project token. Then install &lt;a href="https://www.npmjs.com/package/mixpanel" rel="noopener noreferrer"&gt;mixpanel-node library&lt;/a&gt; to be able to send events server-side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i mixpanel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next up, set up an API route inside app/api/mixpanel/route.ts&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { NextResponse } from "next/server";
const Mixpanel = require("mixpanel");
const mixpanel = Mixpanel.init(process.env.MIXPANEL_TOKEN);

export async function POST(request: Request) {
  const data = await request.json();
  try {
    const { event, properties } = data;

    mixpanel.track(event, properties);

    return NextResponse.json({ status: "Event tracked successfully" });
  } catch (error) {
    console.log(error);
    return NextResponse.json(
      { error: "Internal Server Error" },
      { status: 500 }
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside this route we are initializing Mixpanel library, getting data from the application and calling mixpanel.track that consists of event name and other properties we will include in the call.&lt;/p&gt;

&lt;p&gt;Next up, I created a function sendToMixpanel inside lib/sendToMixpanel.tsx that handles data that will be sent to the API. Since we are sending data server side we can't get additional information that Mixpanel collects on the client side automatically like location, device info, and query params. This function will get that information and it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { v4 as uuidv4 } from "uuid";

const sendToMixpanel = async (
  eventName: string,
  eventProperties?: Record&amp;lt;string, any&amp;gt;
) =&amp;gt; {
//here we are getting the location from a separate endpoint /api/proxy
  const locationResponse = await fetch("/api/proxy");
  const locationData = await locationResponse.json();

//this part of code handles getting the UTM parameters that we can't get by default server side
  const urlParams = new URLSearchParams(window.location.search);
  const utmParams = {
    utm_source: urlParams.get("utm_source") || undefined,
    utm_medium: urlParams.get("utm_medium") || undefined,
    utm_campaign: urlParams.get("utm_campaign") || undefined,
    utm_term: urlParams.get("utm_term") || undefined,
    utm_content: urlParams.get("utm_content") || undefined,
    id: urlParams.get("id") || undefined,
  };

//In my application I'm not authenticating users, so here I'm using uuid library to assign a random id that I will assign to users based on their session, if you are authenticating users you will have to do some additional steps
//More info here: https://docs.mixpanel.com/docs/tracking-methods/id-management/identifying-users

function getUserUUID() {
    let userUUID = localStorage.getItem("userUUID");
    if (!userUUID) {
      userUUID = uuidv4();
      localStorage.setItem("userUUID", userUUID);
    }
    return userUUID;
  }
  const userUUID = getUserUUID();

//Here we are including additional data that will be sent to Mixpanel like device information, UTM parameters and location
  const additionalProperties = {
    distinct_id: userUUID,
    $user_id: userUUID,
    $browser: navigator.userAgent,
    $browser_version: navigator.appVersion,
    $city: locationData.city,
    $region: locationData.region_name,
    mp_country_code: locationData.country_name,
    $current_url: window.location.href,
    $device: navigator.platform,
    $device_id: navigator.userAgent,
    $initial_referrer: document.referrer ? document.referrer : undefined,
    $initial_referring_domain: document.referrer
      ? new URL(document.referrer).hostname
      : undefined,
    $os: navigator.platform,
    $screen_height: window.screen.height,
    $screen_width: window.screen.width,
    ...utmParams,
  };
  const properties = {
    ...eventProperties,
    ...additionalProperties,
  };
//Finally we are calling the mixpanel api route
  fetch("/api/mixpanel", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      event: eventName,
      properties: properties,
    }),
  });
};

export default sendToMixpanel;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally we need a proxy route that will get the location data. Here I am using &lt;a href="http://ipapi.com?utm_source=FirstPromoter&amp;amp;utm_medium=Affiliate&amp;amp;fpr=dellboyan" rel="noopener noreferrer"&gt;ipapi API&lt;/a&gt; to get the location data since I found it the most reliable but feel free to use whichever service you want. This endpoint is at api/proxy/route.ts&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { NextResponse, NextRequest } from "next/server";

export async function GET(request: NextRequest) {
  try {
    // We extract client IP address here
    const ip =
      request.headers.get("x-forwarded-for") ||
      request.headers.get("x-real-ip") ||
      request.ip;

    // If the IP address is not found, we return an error
    if (!ip) {
      return NextResponse.json(
        { error: "Unable to determine IP address" },
        { status: 400 }
      );
    }

 // calling api.ipapi API to get the location data based on IP address
    const locationResponse = await fetch(
      `http://api.ipapi.com/${ip}?access_key=${process.env.IPAPI_KEY}&amp;amp;format=1`
    );
    const locationData = await locationResponse.json();
    return NextResponse.json(locationData);
  } catch (error) {
    console.log(error);
    return NextResponse.json(
      { error: "Internal Server Error" },
      { status: 500 }
    );
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sending Events to Mixpanel
&lt;/h2&gt;

&lt;p&gt;Once this is set up, we can call sendToMixpanel() function wherever we want inside our Next.js app. Here are a few examples from my app:&lt;/p&gt;

&lt;p&gt;Page view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; useEffect(() =&amp;gt; {
    sendToMixpanel("page_view");
  }, []);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modal opened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; &amp;lt;FancyButton
   className="shadow-2xl px-4 py-2"
   onClick={() =&amp;gt; sendToMixpanel("contact_opened")}
 &amp;gt;
   Contact us
&amp;lt;/FancyButton&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Link clicked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Link
   href={kodawarian.url}
   target="_blank"
   onClick={() =&amp;gt;
   sendToMixpanel("link_clicked", {
   url: kodawarian.url,
   name: kodawarian.name,
   })
   }
&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, when you open up Mixpanel events page you will be able to see your data flow inside the platform, and then you can start playing around with the reports.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyt5b4xph5olbcgivo968.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyt5b4xph5olbcgivo968.jpg" alt="Mixpanel Events Dashboard" width="800" height="557"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;And that's it, I think this is pretty straightforward but I would love to hear your thoughts and suggestions! Let me know if you would like a tutorial on how to use Mixpanel even though platform is very user-friendly. I stumbled across a couple of tutorials on setting up Mixpanel in Next.js, but none of them explained in more details everything I consider important so I thought I should make one.&lt;/p&gt;

&lt;p&gt;If you need help in setting up product analytics for your project feel free to &lt;a href="https://kodawarians.com/" rel="noopener noreferrer"&gt;book a call here&lt;/a&gt;.&lt;br&gt;
If you enjoyed this post I'd love it if you could give me a follow on Twitter by clicking on the button below! :)&lt;br&gt;
&lt;a href="https://x.com/DellBoyan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijm0wztow2qko3oyvszr.png" alt="Dellboyan Twitter" width="300" height="74"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>nextjs</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>WordPress or Custom Website? 6 Points to Consider</title>
      <dc:creator>Bojan Stanojevic</dc:creator>
      <pubDate>Thu, 18 Jul 2024 11:18:28 +0000</pubDate>
      <link>https://dev.to/dellboyan/wordpress-or-custom-website-6-points-to-consider-2db</link>
      <guid>https://dev.to/dellboyan/wordpress-or-custom-website-6-points-to-consider-2db</guid>
      <description>&lt;p&gt;During the last 15 years I worked with WordPress a lot and built 50+ websites with some custom coding here and there. In the last couple of years my focus is more on building fully custom websites, but I still use WordPress quite a lot. Both paths have their advantages and disadvantages and in this article I'd like to share what I learned so far.&lt;/p&gt;

&lt;p&gt;As developers I think we always feel the need to build something from scratch maybe even look down on something like WordPress (PHP? Blasphemy!), but for any business it's not about showing how capable you are, it's about the value you can bring and how fast you can get it done.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff44egarvj9hjhzvygu91.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff44egarvj9hjhzvygu91.jpg" alt="PHP is a joke" width="460" height="597"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Going with WordPress can be sufficient when you consider it's tried and tested, completely open source, and very fast to set up and use. Of course there are a lot of use cases where going custom is a wiser choice.&lt;/p&gt;

&lt;p&gt;Important points to consider when making this decision:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Scope of the project
&lt;/h2&gt;

&lt;p&gt;This is probably the most important choice. When choosing the tool for the job think about what is the purpose of the website, what kind of functionalities the website should have, and how scalable the website should be.&lt;br&gt;
If you're building a website for a business that'll need a couple of pages, contact form and maybe a map (90% of "features" for most smaller businesses) chances are, that business will not require any complex work in the future, and WordPress is the way to go. But be absolutely sure that is the case. What I saw happening in a lot of cases, people start installing a bunch of plugins because they need more features, website starts to slow down or even break at some point, and then it's a lot of job and frustration to get it into a working order.&lt;/p&gt;

&lt;p&gt;Before beginning, it should be clear what is the requirement for the website and if the project has potential to grow into something more serious, if that is the case, just go with custom development. I recommend WordPress only for static informational websites and simple shops. For all the other use cases like LMS sites, job posting sites, directories etc., you'll probably use some custom WP theme and a bunch of plugins, and that is not a wise way to go because of security, performance and SEO, but more on that later on.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Design
&lt;/h2&gt;

&lt;p&gt;WordPress has come a long way from 2003 when it was created. Default Gutenberg editor can create some nice pages, but I mainly use &lt;a href="https://be.elementor.com/visit/?bta=224099&amp;amp;brand=elementor" rel="noopener noreferrer"&gt;Elementor plugin&lt;/a&gt; for designing pages from scratch where I converted a lot of websites from Figma to WordPress with almost pixel perfect accuracy. WordPress + Elementor can handle 80% designs from my experience, but for some very custom animations and designs go with custom, that depends on client requirements. Whatever you do, don't go with WordPress themes for some specific use cases (WP theme for restaurants) it will probably come unoptimized with a lot of bugs and bad practices.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Performance
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8jnf9vokxm9ajyenk21b.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8jnf9vokxm9ajyenk21b.jpg" alt="WordPress plugins meme" width="430" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;WordPress has a bad rep regarding speed, but it's not really the tool, it's more the question of how it's used. Slow hosting plus 10+ plugins, chances are your website will load slow. That's exactly why scope is important. You don't need anything more then &lt;a href="https://be.elementor.com/visit/?bta=224099&amp;amp;brand=elementor" rel="noopener noreferrer"&gt;Elementor&lt;/a&gt;, a caching plugin and security/backup plugin, which will not affect speed that much. If the project requires more then this, go with custom development, adding more plugins will just slow the website down, and doing custom development on WordPress site in my opinion defeats the purpose of this CMS (except in cases where the website scaled into a huge project).&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Security
&lt;/h2&gt;

&lt;p&gt;This is a big one. One downside of WordPress being so popular is that a lot of people try to hack it by exploiting plugins and themes. You have to track plugins that are on your site to make sure they are always up to date, also PHP version, and make sure to regularly backup your website. Only because of this point, I would go with custom development because I know I have full control of the code and I can fix the bug if something bad happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Pricing
&lt;/h2&gt;

&lt;p&gt;When making this decision make sure every cost is put on paper. When going with WordPress don't choose the cheapest provider, calculate how many visitors the website might have, but it's pretty straightforward regarding the pricing. You'll only have to think about hosting and domain cost (plus of course the cost of themes and plugins if you are purchasing any). Regarding custom development it can get a lot complicated depending if you need frontend app, backend app, any additional services, storage space, CDN, Database hosting etc so make sure to take everything into consideration.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. SEO
&lt;/h2&gt;

&lt;p&gt;Finally, SEO is something that is very important, a lot of WordPress themes claim they are SEO optimized but in most scenarios, that is not the case. For this use case I recommend going with custom path because you have full control what you are doing. From my experience, in a lot of situations working with WordPress websites you'll have to go deep in the code, especially when dealing with different plugins, to make some SEO changes and lose a lot of time in the progress. Custom development allows you to implement SEO best practices from the ground up, ensuring that your site structure, metadata, and content are optimized for search engines. This level of control can be crucial for businesses that rely heavily on organic search traffic for their success.&lt;/p&gt;

&lt;p&gt;Choosing between WordPress and custom development is not always a straightforward decision. It depends on various factors such as project scope, design requirements, performance needs, security concerns, budget, and SEO goals. While WordPress offers a quick and cost-effective solution for simple websites, custom development provides more flexibility, control, and scalability for complex projects. Whether you need a simple WordPress site or a fully custom web solution, my dev agency, Kodawarians has got you covered. &lt;a href="https://kodawarians.com/" rel="noopener noreferrer"&gt;Schedule a call&lt;/a&gt; and let's build something amazing together!&lt;br&gt;
If you enjoyed this post I'd love it if you could give me a follow on Twitter by clicking on the button below! :)&lt;br&gt;
&lt;a href="https://x.com/DellBoyan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijm0wztow2qko3oyvszr.png" alt="Dellboyan Twitter" width="300" height="74"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
    <item>
      <title>Easily copy any Live website to Figma and then to your App</title>
      <dc:creator>Bojan Stanojevic</dc:creator>
      <pubDate>Sun, 14 Jul 2024 13:40:59 +0000</pubDate>
      <link>https://dev.to/dellboyan/easily-copy-any-live-website-to-figma-and-then-to-your-app-436d</link>
      <guid>https://dev.to/dellboyan/easily-copy-any-live-website-to-figma-and-then-to-your-app-436d</guid>
      <description>&lt;p&gt;Picasso was quoted saying “good artists borrow, great artists&lt;br&gt;
steal.” I totally agree with this point, I think you should steal whenever there's opportunity to do so easily, and in this post, I'll show you how you can straight up steal any design you want.&lt;/p&gt;

&lt;p&gt;I'm just kidding, stealing is wrong and blah blah, we should all be decent human beings and respect each other, but let's explore a method how you can easily get any component you want from some website and adjust it for your project and use case.&lt;/p&gt;

&lt;p&gt;Let's say you are browsing a cool website and you saw a date picker component or a profile card that you like. With this approach you can have that component in your app in less then a couple of minutes.&lt;/p&gt;

&lt;p&gt;First tool we'll use is &lt;a href="https://html.to.design/home" rel="noopener noreferrer"&gt;figma.to.html&lt;/a&gt;. This is a really cool tool that will allow us to select any section of a website and open it inside Figma. You'll need to install their &lt;a href="https://chromewebstore.google.com/detail/htmltodesign/ldnheaepmnmbjjjahokphckbpgciiaed?hl=en" rel="noopener noreferrer"&gt;Google Chrome extension&lt;/a&gt; and &lt;a href="https://www.figma.com/community/plugin/851183094275736358/figma-to-html" rel="noopener noreferrer"&gt;Figma plugin&lt;/a&gt; as well. &lt;/p&gt;

&lt;p&gt;First step is to find a component you would like to copy, I was browsing awwwards.com directory and thought I might copy a component for one website.&lt;/p&gt;

&lt;p&gt;To save a certain section inside Figma you need to click on html.to.design chrome extension and then on Selection (or click Alt+Shift+D) and just select the component you want.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsr7lsvbfdlhos1zvyplm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsr7lsvbfdlhos1zvyplm.png" alt="html.to.design chrome extension" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F83t7lg33couy8tbrnk41.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F83t7lg33couy8tbrnk41.png" alt="Awwwards Directory" width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you click on a selection it will automatically download a file that you can import inside Figma.&lt;/p&gt;

&lt;p&gt;Next up, open up Figma and then from plugins click on html.to.design. Once the plugin opens up, select the file you just downloaded and check all the fields.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Furz9hnqv2zbm00esndrs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Furz9hnqv2zbm00esndrs.png" alt="html.to.design figma plugin" width="800" height="551"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2hmjh4iitkls9oejtbz7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2hmjh4iitkls9oejtbz7.png" alt="html.to.design figma plugin options" width="800" height="577"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally click on Go, and you will get your component loaded up inside Figma. Sometimes it will not match the design 100% but I tested a couple of these plugins and I've found that this one works the best. Feel free to fix any styling that didn't import correctly and modify the design to match your liking.&lt;/p&gt;

&lt;p&gt;In my case import was not 100% correct and I had to fix a table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4mkdfutn6jx1glmb8lda.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4mkdfutn6jx1glmb8lda.png" alt="Loaded figma design with bugs" width="800" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After I fixed this table component was perfect and ready for the export.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F95jc3ee7ijxng7gvzq2y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F95jc3ee7ijxng7gvzq2y.png" alt="Fixed component" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To get the code for this component we will use another &lt;a href="https://www.figma.com/community/plugin/747985167520967365/builder-io-ai-powered-figma-to-code-react-vue-tailwind-more" rel="noopener noreferrer"&gt;plugin for Figma from Builder.io&lt;/a&gt;.  Just like for html.to.design there are many plugins for Figma that are used to get code from design. From my experience none of them work perfectly, this one from Builder.io is also not perfect, but I've found it works the best out of all of them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwlj07czjmpulsyzgn72y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwlj07czjmpulsyzgn72y.png" alt="Builder.io Figma plugin" width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just open up the plugin, select the entire component in Figma and click on Generate code. It will open up Builder.io website with your design, this tool will generate the entire code for your component.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxruj9vftjjw7wbjf8v1x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxruj9vftjjw7wbjf8v1x.png" alt="Builder.io Dashboard" width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Inside the Builder.io dashboard you can choose to generate code with React, Qwik, Vue, Svelte, React Native, Angular, HTML, Mitrosis, Solid and Marko with styling in Tailwind and CSS, you are also able to edit the component with their editor.&lt;/p&gt;

&lt;p&gt;Once I generated the code I got three components: ProfileCard.tsx that contains InfoRow.tsx and AwardCounter.tsx.&lt;/p&gt;

&lt;p&gt;Let's examine them:&lt;/p&gt;

&lt;p&gt;ProfileCard.tsx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import React from "react";
import AwardCounter from "@/components/AwardCounter";
import InfoRow from "@/components/InfoRow";

interface ProfileCardProps {
  imageUrl: string;
  avatarUrl: string;
  name: string;
  isPro: boolean;
  location: string;
  website: string;
  awards: {
    type: string;
    count: number;
  }[];
}

const ProfileCard: React.FC&amp;lt;ProfileCardProps&amp;gt; = ({
  imageUrl,
  avatarUrl,
  name,
  isPro,
  location,
  website,
  awards,
}) =&amp;gt; {
  return (
    &amp;lt;article className="flex flex-col bg-white rounded-lg max-w-[435px]"&amp;gt;
      &amp;lt;img
        loading="lazy"
        src={imageUrl}
        alt="Profile cover"
        className="w-full aspect-[1.33]"
      /&amp;gt;
      &amp;lt;header className="flex gap-3 self-start mt-8 ml-8 text-neutral-800"&amp;gt;
        &amp;lt;img
          loading="lazy"
          src={avatarUrl}
          alt={`${name}'s avatar`}
          className="shrink-0 aspect-square w-[60px]"
        /&amp;gt;
        &amp;lt;div className="flex gap-0.5 justify-between my-auto"&amp;gt;
          &amp;lt;h1 className="text-2xl font-bold leading-6"&amp;gt;{name}&amp;lt;/h1&amp;gt;
          {isPro &amp;amp;&amp;amp; &amp;lt;span className="text-xs font-medium leading-6"&amp;gt;PRO&amp;lt;/span&amp;gt;}
        &amp;lt;/div&amp;gt;
      &amp;lt;/header&amp;gt;
      &amp;lt;InfoRow label="Location" value={location} /&amp;gt;
      &amp;lt;InfoRow label="Website" value={website} /&amp;gt;
      &amp;lt;section className="px-8 py-7 w-full border-t border-gray-200 border-solid"&amp;gt;
        &amp;lt;div className="flex gap-5 max-md:flex-col max-md:gap-0"&amp;gt;
          &amp;lt;h2 className="text-sm font-bold leading-5 text-neutral-800 w-[44%] max-md:w-full"&amp;gt;
            Awards
          &amp;lt;/h2&amp;gt;
          &amp;lt;div className="flex grow gap-0 self-stretch text-center whitespace-nowrap text-neutral-800 w-[56%] max-md:w-full"&amp;gt;
            {awards.map((award, index) =&amp;gt; (
              &amp;lt;AwardCounter
                key={index}
                type={award.type}
                count={award.count}
                isLast={index === awards.length - 1}
              /&amp;gt;
            ))}
          &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/section&amp;gt;
    &amp;lt;/article&amp;gt;
  );
};

export default ProfileCard;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;InfoRow.tsx&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import React from "react";

interface InfoRowProps {
  label: string;
  value: string;
}

const InfoRow: React.FC&amp;lt;InfoRowProps&amp;gt; = ({ label, value }) =&amp;gt; {
  return (
    &amp;lt;div className="flex gap-5 justify-between px-8 py-7 w-full border-t border-gray-200 border-solid text-neutral-800"&amp;gt;
      &amp;lt;div className="text-base font-bold leading-5"&amp;gt;{label}&amp;lt;/div&amp;gt;
      &amp;lt;div className="text-sm font-light leading-5"&amp;gt;{value}&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default InfoRow;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AwardCounter.tsx&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import React from "react";

interface AwardCounterProps {
  type: string;
  count: number;
  isLast: boolean;
}

const AwardCounter: React.FC&amp;lt;AwardCounterProps&amp;gt; = ({ type, count, isLast }) =&amp;gt; {
  const borderClasses = isLast
    ? "rounded-none border border-solid border-neutral-800"
    : "border-t border-b border-l border-solid border-neutral-800";

  return (
    &amp;lt;div className={`flex flex-col py-px pl-px ${borderClasses}`}&amp;gt;
      &amp;lt;div className="text-xs font-medium leading-4"&amp;gt;{type}&amp;lt;/div&amp;gt;
      &amp;lt;div className="px-2 pt-3 pb-3 mt-1.5 text-xs font-bold leading-4 border-t border-solid border-neutral-800"&amp;gt;
        {count}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default AwardCounter;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I import ProfileCard component inside my page this is what I get.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import ProfileCard from "@/components/ProfileCard";
export default function Home() {
  return (
    &amp;lt;div className="flex flex-col h-screen"&amp;gt;
      &amp;lt;ProfileCard
        imageUrl="https://assets.awwwards.com/awards/avatar/87991/6656d09421dac549404842.png"
        avatarUrl="https://assets.awwwards.com/awards/media/cache/thumb_user_retina/avatar/87991/620f9ee354b1b133493338.jpeg"
        name="Red Collar"
        isPro={true}
        website="https://redcollar.com"
        location="Los Angeles, CA"
        awards={[
          { type: "BW", count: 5 },
          { type: "MB", count: 19 },
          { type: "APP", count: 3 },
        ]}
      /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the code Builder.io generated created a reusable component with props already defined which is quite impressive!&lt;/p&gt;

&lt;p&gt;The end result looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foci13bpyb7sud1o84gjs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foci13bpyb7sud1o84gjs.png" alt="Generated component with Builder.io" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This might seem like a long post but the entire process took a couple of minutes. I'm not saying you should steal design from other websites, but you can use this method to speed up your workflow for sure when you are inspired by some design that you saw. You can also use builder.io plugin to convert your one Figma designs much faster or to test how a certain component might fit into your designs. Sometimes I use this method to convert simpler components from Figma into code and then I adjust it, I found it speeds up my workflow so hopefully it can help you too.&lt;/p&gt;

&lt;p&gt;I'm interested to hear what you guys think of this approach and do you think it makes sense for you, feel free to comment bellow.&lt;/p&gt;

&lt;p&gt;If you enjoyed this post I'd love it if you could give me a follow on Twitter by clicking on the button below! :)&lt;br&gt;
&lt;a href="https://x.com/DellBoyan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijm0wztow2qko3oyvszr.png" alt="Dellboyan Twitter" width="300" height="74"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>Transmute negative emotions into personal growth</title>
      <dc:creator>Bojan Stanojevic</dc:creator>
      <pubDate>Mon, 08 Jul 2024 22:46:43 +0000</pubDate>
      <link>https://dev.to/dellboyan/transmute-negative-emotions-into-personal-growth-4c1p</link>
      <guid>https://dev.to/dellboyan/transmute-negative-emotions-into-personal-growth-4c1p</guid>
      <description>&lt;p&gt;I had a bad day at work today. Corporate games, lazy management, and unmeaningful work is what got me. There's no need to go into details but this situation left me with a lot of anger. It did help that I was biking from work so a lot of that steam went out, but it also got me thinking, what's the point of this state? After all, this is just another form of energy that I can convert into something useful, and that's just what I'm doing right now with this post. I think this is the healthiest way to handle these situations that anyone can use and train themselves. Here's the list of common negative emotions with examples how you can transmute them into something useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anger
&lt;/h2&gt;

&lt;p&gt;Like I mentioned from the first example, anger is one of the most powerful emotions you can use because it's very actionable. Whenever you notice you are feeling mad, just start coding or working on something else. For me, I notice this is when I'm feeling the most productive. It's like your brain is on fire and you just want to crush whatever's in front of you, and that is just free energy you get, so why not use it for something beneficial for you. Ultimate state is when you can call upon this energy whenever you want when you need it, Goggins style.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5r4mgw9xbg8oii2foru7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5r4mgw9xbg8oii2foru7.jpg" alt="Goggins Meme" width="602" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fear
&lt;/h2&gt;

&lt;p&gt;Fear is also a common one, it's important to recognize fear in certain situations, it can be in the form of anxiety, I usually feel it in the gut. Previously I would just ignore it and let it pass, but now I try to analyze why I feel that way and try to take action. In most cases it's related to fear, and for this emotion the best medicine is action. Scared of speaking with your users? Contact one right away. Afraid of cold calling? Get that phone and call. The point is, fear is just pointing out where you need to grow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current relationship
&lt;/h2&gt;

&lt;p&gt;This is more related to men than women but I'm sure some women can relate too. If you had a fight with your significant other instead of going into those little wars trying to win, try to take the other person's view. You'll be able to recognize your shortcomings and take action instead of just fighting. It's like playing chess with yourself - you might lose, but you'll definitely learn something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Previous relationships
&lt;/h2&gt;

&lt;p&gt;Love is one of the most powerful energies there is. If you were in a relationship that broke your heart, don't waste your time with wishful thinking, instead use that energy to better yourself and improve. Hit the gym, learn a new skill, or code that new app nobody will use. Nothing says "I'm over you" like becoming a better version of yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Greed &amp;amp; Jealousy
&lt;/h2&gt;

&lt;p&gt;These emotions often get a bad rap, but they're just signposts pointing to what you really want. Jealous of your friend's new job? That's your subconscious telling you it's time for a career change. Greedy for more money? Maybe it's time to start that side hustle you've been dreaming about. The trick is to use these feelings as motivation, not let them eat you up inside. Turn that green-eyed monster into a money-making machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doubt from others
&lt;/h2&gt;

&lt;p&gt;I don't know about anyone else, but one of the greatest motivators for me is when someone is doubting me or thinks I can't achieve something. Use this doubt from others as an energy source to become even better. It's like they're handing you free rocket fuel. Next time someone says "You can't do that," mentally thank them for the boost and prove them wrong. Nothing tastes sweeter than success seasoned with a little "I told you so."&lt;/p&gt;

&lt;p&gt;Remember, emotions are just energy. And energy can't be created or destroyed, but it can be transformed. So next time you're feeling down, angry, or scared, don't waste that energy - transmute it. Turn that frown upside down, and then use it to power your personal growth rocket. Who knows? You might just find that your worst days become the launching pad for your best self.&lt;/p&gt;

&lt;p&gt;If you enjoyed this post I'd love it if you could give me a follow on Twitter by clicking on the button below! :)&lt;br&gt;
&lt;a href="https://x.com/DellBoyan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijm0wztow2qko3oyvszr.png" alt="Dellboyan Twitter" width="300" height="74"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>learning</category>
      <category>programming</category>
    </item>
    <item>
      <title>Top 5 Coolest shadcn/ui Extensions</title>
      <dc:creator>Bojan Stanojevic</dc:creator>
      <pubDate>Sun, 30 Jun 2024 22:38:04 +0000</pubDate>
      <link>https://dev.to/dellboyan/top-5-coolest-shadcnui-extensions-4n7i</link>
      <guid>https://dev.to/dellboyan/top-5-coolest-shadcnui-extensions-4n7i</guid>
      <description>&lt;p&gt;Shadcn/ui is without a doubt the most popular component library. It become crazy popular in the previous year or so. Nevertheless it's not perfect and doesn't cover all use cases. In this article we'll explore the most popular extensions I found to make this cool library even better. Let's begin.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/uixmat/onborda" rel="noopener noreferrer"&gt;1. Onborda&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs82blfdm1fupoonn0rb3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs82blfdm1fupoonn0rb3.png" alt="Image description" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Onborda is like finding a secret weapon for your onboarding process. This sleek extension takes the shadcn/ui framework and transforms it into a powerful tool for creating user onboarding experiences. The step-by-step flow builder is intuitive, making it a breeze to craft engaging welcome tours or feature introductions. What really shines is how seamlessly it integrates with existing shadcn/ui projects - you'd think it was part of the original library. While it's fantastic for straightforward onboarding flows, complex, highly customized sequences might require some extra elbow grease. For teams looking to elevate their user onboarding game without starting from scratch, Onborda is a gem that's worth its weight in conversions. Check out the Onborda demo &lt;a href="https://onborda.vercel.app/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/hsuanyi-chou/shadcn-ui-expansions" rel="noopener noreferrer"&gt;2. shadcn-ui-expansions&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb98348tr7ee7glkuqs6t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb98348tr7ee7glkuqs6t.png" alt="Image description" width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Stumbled upon shadcn-ui-expansions while hunting for ways to beef up my shadcn/ui toolkit, and it's been a game-changer. This nifty package serves up a buffet of fresh components that play nice with the shadcn/ui ecosystem. The Date Time Picker and Infinite Scroll components are standout additions, filling gaps I was missing with shadcn. UI could use some improvement compared to the original but still follows the similar style. For devs looking to expand their shadcn/ui arsenal without reinventing the wheel, this extension is a solid bet, even if you might need to roll up your sleeves for some fine-tuning. Browse all components &lt;a href="https://shadcnui-expansions.typeart.cc/docs" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/JaleelB/emblor" rel="noopener noreferrer"&gt;3. Emblor&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsunw1vsyfcau3eiwyorv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsunw1vsyfcau3eiwyorv.png" alt="Image description" width="800" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Emblor is a highly customizable, accessible, and fully-featured tag input component built with Shadcn UI. I recently worked on a project that required a component very similar to Emblor. Unfortunately, I didn't know about Emblor at that time, it could have saved me some time. Component looks great, you wouldn't even notice it doesn't come with shadcn by default and of course it's totally customizable. Check out the demo and docs on official website &lt;a href="https://emblor.jaleelbennett.com/introduction" rel="noopener noreferrer"&gt;here.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/ManishBisht777/file-vault" rel="noopener noreferrer"&gt;4. File Vault&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fociatd4uwwymzzxzx7u2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fociatd4uwwymzzxzx7u2.png" alt="Image description" width="800" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;File Vault brings a much-needed file management solution to the shadcn/ui ecosystem. This extension offers a slick, drag-and-drop interface for handling file uploads, complete with progress tracking and error handling. The UI is clean and intuitive, staying true to shadcn's design ethos. While it excels at basic file operations, power users might find themselves wishing for more advanced features like batch processing or detailed metadata editing. Nevertheless, for developers looking to add polished file management capabilities to their shadcn/ui projects without reinventing the wheel, File Vault is a solid choice that can save countless hours of development time.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/Aslam97/shadcn-minimal-tiptap" rel="noopener noreferrer"&gt;5. Minimal Tiptap Editor&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ob3pu1y9wkgixxpcmns.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ob3pu1y9wkgixxpcmns.png" alt="Image description" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Shadcn Minimal Tiptap brings the power of rich text editing to shadcn/ui with surprising elegance. This extension marries the robust Tiptap editor with shadcn's sleek design principles, resulting in a WYSIWYG editor that doesn't scream "I'm a third-party component!" The minimalist approach is refreshing, offering just enough formatting options without overwhelming users. It's a breeze to integrate, and the TypeScript support is top-notch. However, "minimal" is the operative word here - if you're after advanced features like collaborative editing or complex formatting, you might need to build on top of this foundation. For projects that need a clean, user-friendly text editor that plays nice with shadcn/ui, this Tiptap implementation hits the sweet spot between functionality and simplicity. You can check out the demo &lt;a href="https://shadcn-minimal-tiptap.vercel.app/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/stanojevicbojan/shadcn-date-picker" rel="noopener noreferrer"&gt;Bonus: shadcn-date-picker&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjff0qnrpd2mph38vmnxr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjff0qnrpd2mph38vmnxr.png" alt="Shadcn Date Picker" width="746" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you plan to use default &lt;a href="https://ui.shadcn.com/docs/components/date-picker" rel="noopener noreferrer"&gt;Shadcn Date Picker&lt;/a&gt; it will probably not work well for Date of Birth use cases because user does not have the option to switch year or month. Therefore, the user will have to scroll with arrows until he finds the right year of birth, the older he is the more work he'll have :). That's why I built this simple extension by combining Select and Scrollarea components. You can checkout the demo &lt;a href="https://www.kodawarians.com/resources/shadcn-date-picker" rel="noopener noreferrer"&gt;here&lt;/a&gt; but it works very simple. Just install these components, copy shadcn-date-picker.tsx from &lt;a href="https://github.com/stanojevicbojan/shadcn-date-picker/blob/main/src/components/shadcn-date-picker.tsx" rel="noopener noreferrer"&gt;here&lt;/a&gt;, and import it in your project.&lt;/p&gt;

&lt;p&gt;To work with me, contact me via &lt;a href="https://www.kodawarians.com/" rel="noopener noreferrer"&gt;my website.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you enjoyed this post I'd love it if you could give me a follow on Twitter by clicking on the button below! :)&lt;br&gt;
&lt;a href="https://x.com/DellBoyan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijm0wztow2qko3oyvszr.png" alt="Dellboyan Twitter" width="300" height="74"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>shadcn</category>
      <category>nextjs</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Implement custom endpoint with pagination in Strapi</title>
      <dc:creator>Bojan Stanojevic</dc:creator>
      <pubDate>Sun, 23 Jun 2024 15:00:04 +0000</pubDate>
      <link>https://dev.to/dellboyan/implement-custom-endpoint-with-pagination-in-strapi-41i1</link>
      <guid>https://dev.to/dellboyan/implement-custom-endpoint-with-pagination-in-strapi-41i1</guid>
      <description>&lt;p&gt;&lt;a href="https://strapi.io/" rel="noopener noreferrer"&gt;Strapi &lt;/a&gt; has to be one of the most popular open source CMS platforms currently available. I used it in several projects and loved it's features and what you get for free. It's very easy to host on popular platforms like Digital Ocean or Netlify, very easy to use, and offers a bunch of features out of the box. In this article I will not go into details about how Strapi works and how to set it up, there are plenty of &lt;a href="https://strapi.io/blog/categories/tutorials?page=1" rel="noopener noreferrer"&gt;tutorials available online&lt;/a&gt;. What I would like to discuss is something I did not find online, and that is how you can customize Strapi endpoints to suit your needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strapi Endpoints
&lt;/h2&gt;

&lt;p&gt;Once you setup Strapi, from the admin dashboard (&lt;a href="http://localhost:1337/admin" rel="noopener noreferrer"&gt;http://localhost:1337/admin&lt;/a&gt;) you can access Content Type Builder and create a new type. Type can be a collection, or a single type. Collection would be for example a collection of posts, or collection of reviews, companies, users etc.&lt;/p&gt;

&lt;p&gt;Any type that you create will consist of default values (id, createdAt, updatedAt etc.) and values you define like name, category, city, country etc.&lt;/p&gt;

&lt;p&gt;Once a type is created, you will have a new API endpoint. For example, if you created a hotels type, you will now have an endpoint &lt;a href="https://localhost:1337/api/hotels" rel="noopener noreferrer"&gt;https://localhost:1337/api/hotels&lt;/a&gt; that will list all the hotels that are created and published. With this new endpoint you will have access to HTTPS method like find, findOne, delete, and create. Access to these methods is configurable from the admin dashboard so for example find can be available for all the visitors, while delete method can only access authenticated roles.&lt;/p&gt;

&lt;p&gt;All the options that are editable from the admin are, of course, accessible and editable from the Strapi project as well, for example to customize route /api/hotels you would go to src/api/hotels&lt;/p&gt;

&lt;h2&gt;
  
  
  When to customize Strapi?
&lt;/h2&gt;

&lt;p&gt;While working with Strapi I can honestly say available features covered 90% use cases my clients had. But, depending on the project and stakeholder requirements I encountered situations where default features where not sufficient and I had to update how Strapi handles data in certain cases.&lt;/p&gt;

&lt;p&gt;Let's continue with the example of hotels. Say you are building a directory of hotels and your stakeholder has this request:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For each location a hotel can belong to, I need to have the ability to control first 10 positions inside that location from the admin dashboard.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One way you can solve this request is to create a repeatable component inside Strapi where administrator can choose a location and a position for that location. Since this is a repeatable component, administrator can define positions for all required locations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6pdwp5acuwdvf6rnhh6w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6pdwp5acuwdvf6rnhh6w.png" alt="Strapi Repeatable Component" width="800" height="304"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So when you hit that api endpoint this value will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"position": [
{
"id": 1,
"position": 1,
"locations": [
{
"id": 11,
"name": "Iowa",
"createdAt": "2024-03-03T20:01:28.602Z",
"updatedAt": "2024-04-23T12:37:27.654Z",
"publishedAt": "2024-03-03T20:01:29.526Z",
}
]
},
{
"id": 2,
"position": 3,
"locations": [
{
"id": 14,
"name": "California",
"createdAt": "2024-03-03T20:01:28.602Z",
"updatedAt": "2024-04-23T12:37:27.654Z",
"publishedAt": "2024-03-03T20:01:29.526Z",
}
]
}
],
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to customize an API route
&lt;/h2&gt;

&lt;p&gt;Having this kind of data structure will work to administer first 10 positions for different locations, but you will not be able to apply this type of sorting in Strapi by default, so we will have to do some customization from the Strapi backend.&lt;/p&gt;

&lt;p&gt;In your Strapi project navigate to src/api/hotels and you will see a couple of folders there. The one we are looking for is the controllers folder, we need to edit the file inside that folder.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Controllers are JavaScript files that contain a set of methods, called actions, reached by the client according to the requested route. Whenever a client requests the route, the action performs the business logic code and sends back the response.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can read more about controllers on the &lt;a href="https://docs.strapi.io/dev-docs/backend-customization/controllers" rel="noopener noreferrer"&gt;official Strapi docs&lt;/a&gt;, but essentially editing controllers will allow us to structure the data the way we need it in order to apply custom sorting of the hotels.&lt;/p&gt;

&lt;p&gt;We will be editing the default find method inside the hotels controller, to start your controller file will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use strict";

const { createCoreController } = require("@strapi/strapi").factories;

module.exports = createCoreController("api::hotel.hotel", ({ strapi }) =&amp;gt; ({
  async find(ctx, next) {

  },
}));

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, inside the find function we'll extract the query sort_by_premium_position and save it in a variable premiumSort. So only in case sort_by_premium_position query param is present containing the location id in the request we will apply the logic below.&lt;/p&gt;

&lt;p&gt;We will also define a helper function parseQueryParam. This function is designed to handle and parse query parameters that may come in different formats. It processes the input parameter param based on its type and returns a parsed version of it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use strict";

const { createCoreController } = require("@strapi/strapi").factories;

module.exports = createCoreController("api::hotel.hotel", ({ strapi }) =&amp;gt; ({
  async find(ctx, next) {
    const { query } = ctx;
    const premiumSort = ctx.query.sort_by_premium_position;

    // Utility function to parse query parameters
    const parseQueryParam = (param) =&amp;gt; {
      if (Array.isArray(param)) {
        return param.map((p) =&amp;gt; JSON.parse(p));
      }
      if (typeof param === "string") {
        return JSON.parse(param);
      }
      return param;
    };
  },
}));

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, if premiumSort is true we will get all the hotels, apply custom sorting and build custom pagination while making sure all query parameters like filtering, pagination and populating still works. If premiumSort is not present we will not change anything and just return the data normally.&lt;/p&gt;

&lt;p&gt;Here is the entire code inside the controller file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use strict";

const { createCoreController } = require("@strapi/strapi").factories;

module.exports = createCoreController("api::hotel.hotel", ({ strapi }) =&amp;gt; ({
  async find(ctx, next) {
    const { query } = ctx;
    const premiumSort = ctx.query.sort_by_premium_position;

    // Utility function to parse query parameters
    const parseQueryParam = (param) =&amp;gt; {
      if (Array.isArray(param)) {
        return param.map((p) =&amp;gt; JSON.parse(p));
      }
      if (typeof param === "string") {
        return JSON.parse(param);
      }
      return param;
    };
try {
  // Check if sorting by location is requested, if not we return everything regularly
  if (premiumSort) {
    const locationToSortBy = Array.isArray(premiumSort)
      ? premiumSort[0]
      : premiumSort;

    // Extract and parse query parameters, with fallbacks for undefined values
    const filters = query.filters ? parseQueryParam(query.filters) : {};
    const sort = query.sort ? parseQueryParam(query.sort) : [];
    let populate = [];

    if (query.populate) {
      if (Array.isArray(query.populate)) {
        populate = query.populate;
      } else if (typeof query.populate === "string") {
        populate = query.populate.split(",");
      }
    }

    // Here we parse pagination parameters, supporting both string and array formats
    let pagination = query.pagination ? parseQueryParam(query.pagination) : {};
    if (typeof pagination === "string") {
      pagination = JSON.parse(pagination);
    } else if (Array.isArray(pagination)) {
      pagination = pagination.reduce(
        (acc, p) =&amp;gt; ({ ...acc, ...JSON.parse(p) }),
        {}
      );
    }

    // Default pagination values if not specified
    const page = pagination.page ? parseInt(pagination.page, 10) : 1;
    const pageSize = pagination.pageSize ? parseInt(pagination.pageSize, 10) : 10;

    // Fetch all data without pagination to apply custom sorting logic
    const entities = await strapi.entityService.findMany(
      "api::hotel.hotel",
      {
        filters,
        sort,
        populate: ["position", "position.locations", ...populate],
      }
    );

    // Filter and sort entities based on the specified location ID and position
    let sortedEntities = entities.sort((a, b) =&amp;gt; {
      const aPos = a.position.find((p) =&amp;gt;
        p.locations.some((c) =&amp;gt; c.id === parseInt(locationToSortBy))
      );
      const bPos = b.position.find((p) =&amp;gt;
        p.locations.some((c) =&amp;gt; c.id === parseInt(locationToSortBy))
      );

      // Sort by position values within the specified location
      if (aPos &amp;amp;&amp;amp; bPos) {
        return aPos.position - bPos.position;
      } else if (aPos) {
        return -1;
      } else if (bPos) {
        return 1;
      }
      return 0;
    });

    // Calculate pagination metadata
    const total = sortedEntities.length;
    const pageCount = Math.ceil(total / pageSize);
    const paginatedEntities = sortedEntities.slice(
      (page - 1) * pageSize,
      page * pageSize
    );

    // Construct metadata for pagination response
    const meta = {
      pagination: {
        page,
        pageSize,
        pageCount,
        total,
      },
    };

    // Return sorted and paginated entities along with pagination metadata
    return { data: paginatedEntities, meta };
  } else {
    // If no custom sorting is requested, return default find method 
    const { data, meta } = await super.find(ctx);
    return { data: data, meta };
  }
} catch (error) {
  console.error("Error in find function:", error);
  ctx.throw(400, "Invalid query parameters");
}
},
}));

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;And that's it! Obviously, this example is related to a specific use case, but hopefully this example may help anyone who encounters similar requests while working on Strapi projects.&lt;/p&gt;

&lt;p&gt;To work with me, visit &lt;a href="https://www.kodawarians.com/" rel="noopener noreferrer"&gt;my website.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you enjoyed this post I'd love it if you could give me a follow on Twitter by clicking on the button below! :)&lt;br&gt;
&lt;a href="https://x.com/DellBoyan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijm0wztow2qko3oyvszr.png" alt="Dellboyan Twitter" width="300" height="74"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>strapi</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Every Next.js website is starting to look the same</title>
      <dc:creator>Bojan Stanojevic</dc:creator>
      <pubDate>Sun, 16 Jun 2024 16:20:33 +0000</pubDate>
      <link>https://dev.to/dellboyan/every-nextjs-website-is-starting-to-look-the-same-12a6</link>
      <guid>https://dev.to/dellboyan/every-nextjs-website-is-starting-to-look-the-same-12a6</guid>
      <description>&lt;p&gt;Current state of web development for some time now includes JS frameworks and libraries springing like mushrooms after the rain. Among these, &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; has emerged as the most popular choice for any developer that wants to build a beautiful SEO-friendly website. However, as its popularity grows, I noticed Next.js websites are beginning to look eerily similar. In this article, we'll explore the reasons behind this and is this bad or maybe even a good thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do I even mean?
&lt;/h2&gt;

&lt;p&gt;First of, I got to point out, I love Next.js. It's my go to framework whenever I start a new web project, no other JS framework allows you to build something beautiful that quickly. But quickly is exactly the issue. If you want to build something quickly it's going to come with some trade offs. If you are working with Next.js, when starting a project you'll probably start with some boilerplate or a template, &lt;a href="https://www.google.com/search?q=next.js+boilerplate&amp;amp;rlz=1C1GCEA_enRS1049RS1049&amp;amp;oq=next.js+boilerpl&amp;amp;gs_lcrp=EgZjaHJvbWUqBwgAEAAYgAQyBwgAEAAYgAQyBwgBEAAYgAQyBggCEEUYOTIICAMQABgWGB4yCAgEEAAYFhgeMggIBRAAGBYYHjIGCAYQRRg9MgYIBxBFGD3SAQgyMTU0ajBqN6gCALACAA&amp;amp;sourceid=chrome&amp;amp;ie=UTF-8" rel="noopener noreferrer"&gt;seems like industries are popping up around Next.js boilerplates nowadays&lt;/a&gt;. Next (.js), you'll probably use &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind CSS&lt;/a&gt; and some component library, most probably &lt;a href="https://ui.shadcn.com/" rel="noopener noreferrer"&gt;shadcn/ui&lt;/a&gt;. All of these solutions are great,  but, as more developers gravitate towards these ready-made components, the individuality of websites diminishes, leading to a sea of sites that look and feel the same. Something like shadcn/ui is completely customizable, but if you want to finish a website quickly, you probably won't spent that much time on customization, if you are not working strictly by design.&lt;/p&gt;

&lt;p&gt;I never really noticed this until I started spending more time on Twitter/X. If you open up Twitter/X right now, and start browsing any solopreneur/build in public/next.js &lt;a href="https://x.com/search?q=%23buildinpublic&amp;amp;src=hashtag_click&amp;amp;f=live" rel="noopener noreferrer"&gt;thread&lt;/a&gt;, you will quickly notice a lot of developers asking for feedback on their websites, and they all look kind of Nextjsy.&lt;/p&gt;

&lt;p&gt;The convenience of Next.js/Tailwind/shadcn combo is obvious. It offers a well-structured, easy, and cohesive design system that ensures visual and functional consistency across projects. However, this uniformity comes at the cost of originality.&lt;/p&gt;

&lt;h2&gt;
  
  
  This has happened before
&lt;/h2&gt;

&lt;p&gt;What I noticed with Next.js is not something new, this has happened before with any popular language/framework/CMS. &lt;a href="https://wordpress.org/" rel="noopener noreferrer"&gt;WordPress &lt;/a&gt;being one of the most popular. In the past I worked on a lot of WordPress websites, as the community around WordPress grow, certain WordPress themes and plugins become a default option for a lot of people, like &lt;a href="https://themeforest.net/item/avada-responsive-multipurpose-theme/2833226" rel="noopener noreferrer"&gt;Avada&lt;/a&gt;, &lt;a href="https://themeforest.net/item/betheme-responsive-multipurpose-wordpress-theme/7758048" rel="noopener noreferrer"&gt;Betheme &lt;/a&gt;and &lt;a href="https://themeforest.net/item/the7-responsive-multipurpose-wordpress-theme/5556590" rel="noopener noreferrer"&gt;The7&lt;/a&gt; with millions of sales in downloads. You install the theme, select one from many template dummy options, and in a couple of minutes you'll have a beautiful WordPress website...that looks like all the rest. Now I can identify a WordPress website as soon as the page is loaded, when that happens finally.&lt;/p&gt;

&lt;p&gt;Similar situation happened with &lt;a href="https://getbootstrap.com/" rel="noopener noreferrer"&gt;Bootstrap&lt;/a&gt;, as this toolkit became more and more popular, the websites that used it started looking more and more the same. &lt;/p&gt;

&lt;p&gt;So who is the blame for this, the lazy developer?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu4xii98l38dqn2aczk67.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu4xii98l38dqn2aczk67.gif" alt="Image description" width="400" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I don't think anyone should be blamed for this, and I don't think this is even a bad thing, I think this is great! All these new technologies gave us more options, and having more options is always good. If you want to create something unique strictly following certain design patterns while investing more time, you can certainly do so. On the other side, if you are building a product that you want to get into the hands of users, so you can get feedback as soon as possible, Next.js component libraries with ready-made building blocks are a great choice. At the end of the day the user does not care, I noticed this pattern only because I look at websites every day. Once and if the product grows you can, and you certainly should invest more time in proper design so the website follows certain brand guidelines when it actually becomes a brand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;While Next.js, shadcn/ui, Tailwind CSS, and popular component libraries have undeniably transformed web development for the better, they have also contributed to a trend of sameness among websites, but they are not the first to do so. Speedy developments with AI probably will not help with this trend. But having more options is not a bad thing. Anybody working on a website has now the option to build something fast or to build something unique, it just depends in what stage of development you are and what you are trying to build.&lt;/p&gt;

&lt;p&gt;If you enjoyed this post I'd love it if you could give me a follow on Twitter by clicking on the button below! :)&lt;br&gt;
&lt;a href="https://x.com/DellBoyan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijm0wztow2qko3oyvszr.png" alt="Dellboyan Twitter" width="300" height="74"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>tailwindcss</category>
      <category>react</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to sign in with LinkedIn in a Strapi and Next.js app with custom authentication</title>
      <dc:creator>Bojan Stanojevic</dc:creator>
      <pubDate>Tue, 11 Jun 2024 07:52:27 +0000</pubDate>
      <link>https://dev.to/dellboyan/how-to-sign-in-with-linkedin-in-a-strapi-and-nextjs-app-with-custom-authentication-a53</link>
      <guid>https://dev.to/dellboyan/how-to-sign-in-with-linkedin-in-a-strapi-and-nextjs-app-with-custom-authentication-a53</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;While working on a project for a client, one of the requirements was to allow users to sign up/sign in using their LinkedIn accounts besides registering with their email/password. In this post we will dive into this implementation using Strapi on the backend, Next.js on the frontend (Authentication handled by next-auth on the client side), issues I encountered and how I solved them, so it might help anyone working on similar features.&lt;/p&gt;

&lt;h2&gt;
  
  
  LinkedIn Setup
&lt;/h2&gt;

&lt;p&gt;There are many available providers supported out of the box inside Strapi: auth0, cas, cognito, discord, email, facebook, github, google, instagram, microsoft, patreon, reddit, twitch, twitter, vk, and finally linkedin.&lt;/p&gt;

&lt;p&gt;When you open up Strapi admin and go to Settings — Providers you will see these options for LinkedIn. You will need Client ID and Client Secret, but you can completely ignore these settings and leave Linkedin Provider disabled inside the Strapi admin settings, we will not use them for this guide.&lt;/p&gt;

&lt;p&gt;You can find LinkedIn Client ID and Client Secret by signing up on the &lt;a href="https://developer.linkedin.com/" rel="noopener noreferrer"&gt;LinkedIn Developers website&lt;/a&gt;. On the website you’ll have to create a new app and verify your ownership. That will allow you to get the Client ID and Client Secret and add authorized redirects. Additionally, you’ll have to enable the following product: Sign In with LinkedIn using OpenID Connect which will give you openid, profile and email scopes that will allow you to login.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strapi Setup and problem encountered
&lt;/h2&gt;

&lt;p&gt;While working on implementing LinkedIn authentication I was following along with Strapi docs available on this url. If you look at the section “Setup the frontend” I got stuck at the final step:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Create a frontend route like FRONTEND_URL/connect/${provider}/redirect that have to handle the access_token param and that have to request STRAPI_BACKEND_URL/api/auth/${provider}/callback with the access_token parameter.&lt;br&gt;
The JSON request response will be { "jwt": "...", "user": {...} }.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After I call the backend, I was constantly getting errors that pointed to a authorization problem. After hours of analyzing what could be the issue, I realized LinkedIn changed their scopes required for authentication so they don’t align with what Strapi uses. I opened an issue on Strapi forums and currently there is a Github issue connected with my question, but it’s still not merged and fixed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  body: {
    serviceErrorCode: 100,
    message: 'Not enough permissions to access: GET /me',
    status: 403
  },
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to this error and scopes required for the /me endpoint — &lt;a href="https://learn.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api" rel="noopener noreferrer"&gt;Profile API — LinkedIn | Microsoft Learn&lt;/a&gt; seems like Strapi is calling linkedin /v2/me endpoint which requires r_liteprofile, r_basicprofile, r_compliance scopes, while Linkedin is now using /v2/userinfo for authentication and these scopes: openid, profile, email, and that’s why I’m getting the error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workaround Fix for LinkedIn provider
&lt;/h2&gt;

&lt;p&gt;To make Strapi authentication work with LinkedIn I had to ignore the official authentication flow from Strapi and create a custom auth endpoint.&lt;/p&gt;

&lt;p&gt;Like I pointed, I’m using Next.js on the frontend side and Next-Auth.js to handle authentication.&lt;/p&gt;

&lt;p&gt;To handle authentication on the frontend inside authOptions, LinkedIn provider looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const authOptions = {
  providers: [
    LinkedInProvider({
      clientId: process.env.LINKEDIN_CLIENT_ID || '',
      clientSecret: process.env.LINKEDIN_CLIENT_SECRET || '',
      client: { token_endpoint_auth_method: 'client_secret_post' },
      issuer: 'https://www.linkedin.com',
      profile: (profile: LinkedInProfile) =&amp;gt; ({
        id: profile.sub,
        name: profile.name,
        email: profile.email,
        image: profile.picture
      }),
      wellKnown: 'https://www.linkedin.com/oauth/.well-known/openid-configuration',
      authorization: {
        params: {
          scope: 'openid profile email'
        }
      }
    }),
]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the callback jwt function, if the user is authenticating with LinkedIn I am calling a custom endpoint in Strapi that is handling the authentication and issuing jwt token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;callbacks: {
    async jwt({ token, user, account }: { user: any; token: any; account: any }) {
      if (account?.provider === 'linkedin') {
        const res = await fetch(`${process.env.API_URL}/customLinkedinAuthEndpoint`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(user)
        });
        const data = await res.json();
        token.jwt = data.jwtToken;
      } else {
        if (user) {
          token.jwt = user.jwt;
        }
      }

      return token;
    },
  },
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the Strapi side create your custom endpoint. You will have to update the controller to handle authentication.&lt;/p&gt;

&lt;p&gt;Inside your src/api/customLinkedinAuthEndpoint/controllers/ create a new file customLinkedinAuthEndpoint.js&lt;/p&gt;

&lt;p&gt;Inside the file we define a function findOrCreateUser() that is handling all the logic. The function is checking if the user already exists and handling different use cases. If the user doesn’t exists it creates a new user. Based on this logic I am issuing the jwt token that the frontend can receive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use strict";

module.exports = {
  linkedinAuth: async (ctx) =&amp;gt; {
    try {
      const linkedinData = ctx.request.body;
      const user = await findOrCreateUser(linkedinData);
      // Issue a JWT token
      const jwtToken = strapi.plugins["users-permissions"].services.jwt.issue({
        id: user.id,
      });
      ctx.send({ jwtToken, user });
    } catch (err) {
      ctx.body = err;
    }
  },
};

async function findOrCreateUser(linkedinData) {
  // Try to find the user by their LinkedIn ID
  let user = await strapi.db.query("plugin::users-permissions.user").findOne({
    where: { linkedinId: linkedinData.id },
  });

  if (!user) {
    // If the user doesn't exist, try to find the user by their email
    user = await strapi.db.query("plugin::users-permissions.user").findOne({
      where: { email: linkedinData.email },
    });

    if (user &amp;amp;&amp;amp; !user.linkedinId) {
      // If the user exists but doesn't have a LinkedIn ID, update the user
      user = await strapi.db.query("plugin::users-permissions.user").update({
        where: { id: user.id },
        data: {
          confirmed: true,
          linkedinId: linkedinData.id,
          linkedinImage: linkedinData.image,
        },
      });
    } else if (!user) {
      // If the user doesn't exist, create new user
      user = await strapi.db.query("plugin::users-permissions.user").create({
        data: {
          linkedinId: linkedinData.id,
          username: linkedinData.name,
          email: linkedinData.email,
          linkedinImage: linkedinData.image,
          role: 1,
          confirmed: true,
          provider: "local",
        },
      });
    }
  }

  return user;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally inside the src/api/customLinkedinAuthEndpoint/routes/customLinkedinAuthEndpoint.js update the route&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module.exports = {
  routes: [
    {
      method: "POST",
      path: "/customLinkedinAuthEndpoint",
      handler: "customLinkedinAuthEndpoint.linkedinAuth",
      config: {
        policies: [],
        middlewares: [],
      },
    },
  ],
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will also have to update permissions for this custom endpoint inside the Strapi admin to be able to call this endpoint on the client side.&lt;/p&gt;

&lt;p&gt;And that’s it, you will be able to get the jwt on the client side with LinkedIn and make authenticated requests to the Strapi backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Hope this guide can help anyone who encounters similar problems while working with Strapi and authentication. Hopefully issue with default auth flow for LinkedIn will be fixed soon but this guide can help anyone who wants to build custom authentication flow.&lt;br&gt;
If you enjoyed this post I'd love it if you could give me a follow on Twitter by clicking on the button below! :)&lt;br&gt;
&lt;a href="https://x.com/DellBoyan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fijm0wztow2qko3oyvszr.png" alt="Dellboyan Twitter" width="300" height="74"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
