הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

איך להעלות תמונה (או כל קובץ אחר) ב Next.JS ב 2025

19/03/2025

ריאקט 19 ו next 15 עובדים יחד בהרמוניה כדי לאפשר לנו המתכנתים הפשוטים לכתוב אפליקציות Full Stack במהירות. אחד המקומות שקיבל טיפול באינטגרציה הזו הוא הטפסים. בואו נראה איך עובדים עם טפסים ב 2025 ב React ו Next ובפרט איך להעלות תמונות לשרת.

המשך קריאה

ואז פתחתי את הקוד

18/03/2025

ביקשתי מ Lovable לכתוב אפליקציה שתיקח קישור ל URL ותייצר לי בוחן אוצר מילים על המילים שם, ככה בשביל לתרגל את הספרדית. בשביל שלא יהיה קל מדי ביקשתי ממנו גם שיבקש את הרמה הנוכחית שלי בספרדית ויבנה בוחן על מילים מה URL שמתאימות לרמה שלי. זה לקח לו קצת זמן אבל בסוף הוא הוציא את האפליקציה כאן:

https://colorlingo-quizzy.lovable.app/

אחרי כמה משחקים הרגשתי שהוא קצת מרמה אז הלכתי לקוד כדי למצוא את הפונקציה הבאה שמוציאה את המילים מהלינק:

// This would be populated with real data from the URL and filtered by level
// For now, we're creating dummy words for the custom quiz
const generateDummyWords = (url: string, level: SpanishLevel) => {
  // Extract domain name for demonstration
  let domain = '';
  try {
    domain = new URL(url).hostname.replace('www.', '');
  } catch {
    domain = 'example';
  }

  // These would actually come from scraping and NLP processing
  const allWordPairs = [
    // A1 level words (beginner)
    { english: "hello", spanish: "hola", level: "A1" },
    { english: "goodbye", spanish: "adiós", level: "A1" },
    { english: "thank you", spanish: "gracias", level: "A1" },
    { english: "please", spanish: "por favor", level: "A1" },

    // A2 level words (elementary)
    { english: "welcome", spanish: "bienvenido", level: "A2" },
    { english: "friend", spanish: "amigo", level: "A2" },
    { english: "family", spanish: "familia", level: "A2" },
    { english: "today", spanish: "hoy", level: "A2" },

    // B1 level words (intermediate)
    { english: "website", spanish: "sitio web", level: "B1" },
    { english: domain, spanish: domain, level: "B1" },
    { english: "language", spanish: "idioma", level: "B1" },
    { english: "content", spanish: "contenido", level: "B1" },

    // B2 level words (upper intermediate)
    { english: "experience", spanish: "experiencia", level: "B2" },
    { english: "develop", spanish: "desarrollar", level: "B2" },
    { english: "improve", spanish: "mejorar", level: "B2" },
    { english: "progress", spanish: "progreso", level: "B2" },

    // C1 level words (advanced)
    { english: "fluency", spanish: "fluidez", level: "C1" },
    { english: "proficient", spanish: "competente", level: "C1" },
    { english: "articulate", spanish: "articular", level: "C1" },
    { english: "communicate", spanish: "comunicar", level: "C1" },

    // C2 level words (proficiency)
    { english: "mastery", spanish: "dominio", level: "C2" },
    { english: "nuance", spanish: "matiz", level: "C2" },
    { english: "sophisticated", spanish: "sofisticado", level: "C2" },
    { english: "eloquent", spanish: "elocuente", level: "C2" },
  ];

  // Filter words based on selected level or lower
  const levelOrder = ["A1", "A2", "B1", "B2", "C1", "C2"];
  const levelIndex = levelOrder.indexOf(level);
  const filteredWords = allWordPairs.filter(word => 
    levelOrder.indexOf(word.level as SpanishLevel) <= levelIndex
  );

  // Shuffle the array to make it more random and take 10 words
  return filteredWords
    .sort(() => 0.5 - Math.random())
    .slice(0, 10)
    .map(({ english, spanish }) => ({ english, spanish }));
};

בקיצור הוא לגמרי עבד עליי. אף מילה מהלינק לא מגיעה לבוחן אוצר מילים. (הגירסה השנייה שלו גם לא היתה נכונה אבל מסיבה אחרת).

עכשיו הבאג הזה יחסית נחמד כי קל לזהות אותו, אבל אני חושב שהוא ממחיש את היצירתיות של ה AI, שהיא הכח והחולשה שלו: כשהוא לא יודע הוא לא עוצר לשאול או לחשוב, פשוט משלים קוד מהמאגר האינסופי של קודים שהוא אומן עליהם. מאוד יעיל ל POC-ים קטנים כשאנחנו בתוך הצוות או חוקרים רעיון, הרבה פחות רלוונטי למערכת שצריכה לפגוש משתמשים אמיתיים.

אז מה עושים? כלי AI הם פה כדי להישאר, עם היתרונות והחסרונות שלהם. שימוש נכון בכלים אלה בהחלט ישפר את מהירות הפיתוח שלכם. שימוש לא נכון, גם אם מלהיב באותו רגע, יכול בקלות לגמור לנו אחר צהריים. ביום חמישי שבוע הבא (ה 27.3 ב 20:00) ערן גולדמן-מלכא הזמין אותי לדבר איתו בוובינר על שימוש נכון בכלי AI לפיתוח - נראה שם את lovable, base44, קופיילוט, זנקוד, רדי ועוד המון חברים ואספר מה ואיך עובד עבורי. מוזמנים להצטרף בלינק:

https://calendly.com/aumint/10xdevwith_ai?month=2025-03&date=2025-03-27

זה לקח רק שתי הודעות

17/03/2025

כולם מדברים על base44. חבר אחד עשה איתו משחק כזה וחבר אחר משחק אחר, אז אמרתי יאללה גם אני רוצה לנסות. אחרי שתי הודעות קיבלתי ממנו את התשובה:

However, I don't see an integration available for audio transcription in the current platform. This would significantly limit the tool's usefulness.

אני מכבד את הכנות. כלים אחרים היו יכולים לקחת אותי ב Loop דקות ארוכות עד שהייתי מבין שאנחנו בדרך ללא מוצא. והכנות הזאת היא בדיוק הסיפור של ה AI בפיתוח שאני חושב שלא מספיק מדברים עליו.

כן זה כלי מדהים.

כן אפשר לכתוב קוד יותר מהר.

כן זה מקצר זמנים והופך פרויקטי צד להרבה יותר קלים וכיפים.

אבל גם היום פיתוח מוצר תוכנה (גם פרויקט צד) דורש ידע בתוכנה, לפחות החל מנקודה מסוימת. הדרך המהירה ביותר קדימה היא לא בהכרח זו שמצריכה הכי פחות "לכתוב קוד" אלא זו שמאפשרת לנו לשלב בין קוד ויכולות שלנו לבין המהירות של ה AI. כשאנחנו לא מפחדים לקודד ומוכנים לצלול ולהבין את הקוד של ה AI, אנחנו יכולים לקחת את הרעיונות המהירים שלו ולשלב אותם לתוך מבנים נכונים שמתאימים ליישום שלנו.

וכן אלה בדיוק העקרונות שאנחנו מתרגלים בתוכנית הליווי החדשה שיצרתי. הקבוצה הבאה יוצאת לדרך ביוני ואפשר ללמוד עוד בדף התוכנית פה באתר.

מבט נוסף על Jotai

16/03/2025

כתבתי בעבר על jotai ואני מודה שבאותו זמן הוא לא היה ספריית ניהול הסטייט המועדפת עליי. חלפה שנה וחצי והיום אני כבר הרבה יותר אוהב אותו וממליץ עליו לפרויקטים חדשים. בואו נראה למה.

המשך קריאה

ארבעה דברים שאהבתי בפליירייט (ואחד שאהבתי פחות)

15/03/2025

פליירייט היא ספריית כתיבת בדיקות מקצה לקצה לאפליקציות ווב שמתאימה לפייתון, TypeScript / JavaScript, ג'אווה ודוטנט. בזכות כמה פיתרונות ורעיונות ייחודיים ועוד כמה רעיונות טובים שהם אספו ממקומות אחרים העבודה עם פליירייט מרגישה נכונה מהרגע הראשון. הנה 4 דברים שאני מאוד אהבתי בספריה ואחד שפחות. נתחיל בטובים:

  1. תיעוד - אני יודע זה כאילו לא חלק מהספריה אבל האמת שכשמתחילים לעבוד עם ספריה ומגלים עמודי תיעוד מושקעים שכוללים גם הסבר מפורט, גם וידאו כשצריך וגם מדריכים ו Best Practice אתה כבר מרגיש בידיים טובות.

  2. כתיבת בדיקות באמצעות הקלטה - מתוך שורת הפקודה כתבתי npx playwright test --debug ונכנסתי למצב הדיבאג של הבדיקות, שם היה לי דפדפן שהראה את העמוד וכפתור "הקלטה" שפשוט מתרגם את הפעולות שלי לקוד בדיקה. נכון הוא לא ידע לכתוב את ה expect כי הוא לא ידע למה לצפות, אבל כל השאר עבד ממש בסדר. זו בדיקה לדוגמה שכתבתי לאתר טוקוד באמצעות המקליט שלהם:

test('sign up to receive daily posts', async ({page}) => {
    await page.goto('https://www.tocode.co.il/blog');
    await page.getByRole('textbox', { name: 'you@wherever.you.are' }).click();
    await page.getByRole('textbox', { name: 'you@wherever.you.are' }).fill('ynon@tocode.co.il');
    await page.getByRole('button', { name: 'שלחו לי למייל' }).click();
    await expect(page.getByText('נשלח אליך מייל עם קישור לאישור ההרשמה. יש לפתוח את המייל וללחוץ על הכפתור לאישור')).toBeVisible();
})
  1. תמיכה מלאה בכל הדפדפנים וגדלי המסך - באמצעות הגדרה בקובץ קונפיגורציה הצלחתי מאוד מהר לבדוק על יותר או פחות דפדפנים וגם לבחור אמולטורים לגדלי מסך שונים באמצעות פיצ'ר Device Mode של הדפדפנים. הנה דוגמה לקונפיגורציה:

import { defineConfig, devices } from '@playwright/test'; // import devices

export default defineConfig({
  projects: [
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
      },
    },
    {
      name: 'Mobile Safari',
      use: {
        ...devices['iPhone 13'],
      },
    },
  ],
});
  1. העדפה לכתיב ה Role - למרות שבתור כותב בדיקה זה יכול לעייף, אבל ההעדפה של פליירייט לכיוון Locators שמשתמשים ב Aria Role עוזרת להדגיש את ה Role של כל דבר באתר ולראות מהר כשהגדרות ה Aria לא נכונות, וזה אפילו לפני שסיימנו לכתוב את הבדיקות. אגב יש להם גם תוסף לבדיקת בעיות נגישות והכל משולב בפנים שימו לב לדוגמה הזו:
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright'; // 1

test.describe('homepage', () => { // 2
  test('should not have any automatically detectable accessibility issues', async ({ page }) => {
    await page.goto('https://your-site.com/'); // 3

    const accessibilityScanResults = await new AxeBuilder({ page }).analyze(); // 4

    expect(accessibilityScanResults.violations).toEqual([]); // 5
  });
});
  1. ומה פחות? אני מצאתי את כתיב ה expect מבלבל במיוחד כשחלק מהבדיקות סינכרוניות ואחרות אסינכרוניות לדוגמה שתי הבדיקות האלה אינן זהות:
await expect(page).toHaveURL('...');
await expect(page.url()).toBe('...');

בראשונה toHaveURL היא בדיקה אסינכרונית שמחכה עד שה URL ישתנה בעקבות לחיצה או הפניה, אבל הבדיקה השניה היא בדיקה מיידית כי page.url היא פונקציה סינכרונית ו toBe היא בדיקה סינכרונית. ה await בשורה השנייה לא עושה כלום כיוון שאין שם שום Promise. בפועל מבנה זה דורש להסתכל על הטיפוסים כל פעם שכותבים בדיקה כדי להבין אם פונקציית הבדיקה היא סינכרונית או אסינכרונית.

סך הכל מאוד אהבתי את פליירייט ואם אתם צריכים ספריית בדיקות End To End לפרויקט אין ספק שמדובר בבחירה טובה ובטוחה.

היום למדתי: אקורדיון ללא JavaScript

14/03/2025

הנה טריק HTML נחמד שמסתבר שנתמך כבר תקופה ואיכשהו דילגתי עליו - לא צריך יותר JavaScript (או אפילו אם כבר יש לכם JavaScript לא צריך לשמור משתנה סטייט) רק בשביל לנהל אקורדיון עם דברים שנפתחים ונסגרים. כל מה שצריך זה אלמנט details של HTML וזה נראה ככה:

<details >
<summary>Toggle me</summary>
  <div > 
    <p>Look there's no JavaScript involved ... yay</p> 
  </div> 
</details>

והקודפן:

ואם אתם צריכים לעצב אותו אפשר להשתמש ב :open ב CSS כדי לתפוס את המצב הפתוח (לא עובד בספארי בינתיים) לדוגמה:

details:open > summary {
  background-color: pink;
}

:is(select, input):open {
  background-color: pink;
}

או ב open בתוך סוגריים מרובעים שכן עובד גם בספארי:

details summary {
  background: pink;
}

details {
  background: lightgreen;
}

details[open] summary {
  background: red;
}

ובנושא אחר - אני פותח היום את הרישום לקבוצת פיתוח הפרויקטים הבאה שתצא לדרך ביוני. זה מסלול מעולה שנותן למשתתפים את המסגרת, הידע והמוטיבציה לבנות פרויקט מקצה לקצה. אם מעניין אתכם שווה להעיף מבט בדף הפרויקט ולקבוע שם פגישת היכרות.

הפיתרון הנכון

13/03/2025

הפיתרון הנכון הוא לא תמיד הקל ביותר. רוב הזמן זה הפוך, הפיתרון הנכון הוא הקשה ביותר ליישום כי הוא דורש ארגון מחדש של הקוד.

הפיתרון הנכון הוא לא תמיד המהיר ביותר לביצוע. רוב הזמן זה הפוך, הפיתרון הנכון לוקח הכי הרבה זמן כי הוא כולל גם זמן לימוד. אם היית יודע איך לעשות את זה נכון היית עושה את זה נכון בפעם הראשונה.

הפיתרון הנכון דורש להודות בטעות, לקחת צעד אחורה ולמצוא דרך מפה לשם. רוב הזמן כשיש לי מערכת שכבר נבנתה בצורה לא נכונה יהיה לי מאוד קשה לעבור לפיתרון הנכון, קשה כמעט כמו לבנות את כל המנגנון מהתחלה.

הפיתרון הנכון הוא הפיתרון הנכון. הוא נכון כי אחרי יישום שלו יהיה יותר קל להתקדם. הפיתרון הנכון לוקח אותך אחורה כדי שתוכל להתקדם מהר יותר. והפיתרון הנכון הוא בלתי נמנע. להמשיך לנסוע עם גלגלי עזר לא באמת יחסוך לך את הצורך ללמוד לרכב על אופניים. ואם ברור מה צריך לעשות אז גם ברור שככל שנחכה זה רק יהיה יותר קשה.

יש רק בעיה אחת.

אתה אף פעם לא יודע שהגעת לפיתרון הנכון, וכנראה שבעולם האמיתי אי אפשר להגיע אליו. פיתרונות לא נכונים? כאלה אני מזהה מקילומטר, זה כח העל שלנו המתכנתים - להיות לא מרוצים ולהגיד כמה קוד מסוים גרוע. הפיתרון הנכון הוא חלום וכנראה לא אחד שאפשר להגשים. בינתיים מה שאפשר לעשות זה לזהות עוד בעיות בקוד ולשפר, באג אחד בכל פעם.

כן ריאקט נהיה מסובך מדי

12/03/2025

ריאקט תמיד היה מסובך אבל סוג הסיבוך השתנה. פעם היה קשה להבין את הרעיון של ריאקט אבל ה API שלו היה יחסית קטן, בטח בהשוואה לאנגולר. מפתחי ריאקט שהכרתי לפני 8 שנים ידעו בעל פה מה עושה כל פקודה בספריה. היום הסיפור קצת יותר מסובך, ובשביל להוכיח את זה אני רק אזרוק פה רשימה של Hooks מהתיעוד. נסו לספור כמה אתם מכירים או בכמה השתמשתם:

  1. useId

  2. useDeferredValue

  3. useDebugValue

  4. useInsertionEffect

  5. useImperativeHandle

  6. useOptimistic

  7. useSyncExternalStore

  8. useActionState

  9. useFormStatus

הבעיה עם עומס ה Hooks היא שכל Hook נועד לפתור בעיה מאוד ספציפית. חלקם באמת פשוטים כמו useDebugValue אבל כל אחד מהרשימה הזאת מתאר "מעקף" או בעיה שאי אפשר לפתור עם הכלים הרגילים של ריאקט. בעולם מושלם כל ה-9 האלה וכנראה גם חלק מה Hooks היותר שגרתיים היו בנויים בתור ספריות חיצוניות עבור אותם מקרים שצריכים אותם.

מה עושים במקום URL Params באפליקציית צד לקוח עם Next.js

11/03/2025

הבאג הזה ב Next עורר דיון סוער:

https://github.com/vercel/next.js/discussions/64660

בקצרה כשאני בונה אפליקציית צד-לקוח בלבד עם output: 'export' ב next.js אי אפשר להשתמש בנתיבים דינמיים, כלומר נתיב כזה לא יעבוד:

/blog/[slug]

ההסבר הוא שכשנקסט רואה כזה דבר הוא לא יודע איזה קובץ HTML לבנות, כי צריך לזכור שכל מנגנון הניתוב של נקסט באפליקציית צד לקוח בנוי על זה שהוא מריץ את קבצי הקומפוננטות לפי מערכת הקבצים ומייצר מערכת קבצי HTML מקבילה לקבצי ה JavaScript, כלומר מרנדר את כל קבצי ה page.tsx ושומר את התוצאות בתור HTML-ים.

ואם נשים בצד את כל הפיתרונות המתוחכמים ואת כל התקוות שבקרוב החברים ב Vercel יפתרו את זה (הם הבטיחו פיתרון באזור מאי-יוני), אבל כבר בינתיים אפשר לבנות פיתרון מקביל ודי פשוט - במקום להשתמש ב Route Params נשתמש ב Search Params, כלומר הנתיב יהיה:

/blog/post?slug=hello-world

בקוד next קצת נודניק ובשביל לגשת ל Search Params אני צריך לעטוף את הקומפוננטה ב Suspense לכן מבחינת קוד ב page.tsx אני כותב:

'use client'
import ShowPost from './show-post';
import { Suspense } from 'react';

export default function BlogPost() {
  return (
    <div>
      <Suspense fallback={<p>Loading post</p>}>
        <ShowPost />
      </Suspense>
    </div>
  )
}

וב show-post.tsx יש לי:

'use client'
import Link from 'next/link';
import { getPost } from '@/lib/blog';
import { useSearchParams, notFound } from 'next/navigation';

export default function BlogPost() {
  const slug = useSearchParams().get('slug')!;    
  const post = getPost(slug);
  if (!post) {
    return <p>Not flund. slug = {slug}</p>
  }

  return (
    <div>
      <h1>{post.title}</h1>
      <p><Link href="/blog">Back to blog</Link></p>
      <p>{post.fulltext}</p>
    </div>
  )
}

ובהנחה שאתם יודעים איך לקבל פוסט לפי slug הקוד יעבוד ויש לכם בלוג בתור אפליקציית עמוד יחיד.

זה לא אני, זה ה AI כתב

10/03/2025

אין דבר כזה. מרגע שלקחתי קוד מ AI ושמתי אותו בריפו הוא הופך לקוד שלי. השם שלי כתוב על הודעת הקומיט לא של Gemini. האחריות היא שלי לדאוג שה AI לא עושה שטויות ולקרוא את הדברים שהוא כותב, ואם אני לא מבין משהו האחריות היא שלי ללמוד את זה ולהפעיל חשיבה ביקורתית כלפי ה AI.

מהבחינה הזאת ה AI לא מחדש כלום מעבר ל Stack Overflow או למאגר סקריפטים שמצאתי ברשת. רק בגלל ש AI כתב את זה לא הופך את זה לנכון או רצוי. וכן אני מסכים אתכם שיש פה בעיה כי AI יוצר אינסוף קוד שנראה הגיוני בכלום זמן. בדיוק בשביל זה משלמים לכם יותר ממה שאתם משלמים ל AI.

אם אתם לא מסוגלים להבין את הקוד של ה AI עדיף שתכתבו בעצמכם. ואם אין לכם זמן לקרוא את הקוד של ה AI, איך תמצאו זמן לתקן את הבאגים שלו?