بناء تطبيق ويب كامل باستخدام SolidStart: دليل عملي شامل

AI Bot
بواسطة AI Bot ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

ما الذي ستبنيه

في هذا الدليل، ستبني تطبيق إدارة مهام كامل باستخدام SolidStart — الإطار الرسمي لـ SolidJS. في النهاية، سيكون لديك تطبيق يعمل بالكامل مع:

  • توجيه مبني على الملفات مع تخطيطات متداخلة
  • دوال الخادم باستخدام "use server" لمنطق الواجهة الخلفية الآمن
  • تحميل بيانات تفاعلي باستخدام query وcreateAsync
  • تعديل البيانات باستخدام action لمعالجة النماذج
  • تخزين SQLite باستخدام better-sqlite3
  • دعم TypeScript الكامل
  • عرض من جانب الخادم (SSR) مع البث

الوقت المطلوب: 60-90 دقيقة


المتطلبات الأساسية

قبل البدء، تأكد من وجود:

  1. Node.js 20+ — شغّل node --version للتحقق
  2. معرفة أساسية بـ HTML وCSS وJavaScript
  3. إلمام بأُطر العمل التفاعلية (معرفة React أو Solid الأساسية مفيدة)
  4. محرر أكواد — VS Code مع إضافة Solid موصى به

لماذا SolidStart؟

ما هو SolidStart؟

SolidStart هو الإطار الرسمي الكامل لـ SolidJS. يجمع بين التفاعلية الدقيقة لـ Solid وبيئة تشغيل خادم قوية.

الميزةالوصف
تفاعلية دقيقةيُحدّث فقط ما تغيّر — بدون مقارنة DOM افتراضي
توجيه مبني على الملفاتالصفحات تُعرَّف بهيكل نظام الملفات
دوال الخادمتشغيل كود الخادم بتوجيه "use server"
أوضاع عرض متعددةSSR وCSR وSSG والبث مدمجة
مبني على Vinxiيعمل بـ Nitro وVite تحت الغطاء
انشر في أي مكانإعدادات مسبقة لـ Vercel وNetlify وCloudflare وAWS

كيف يقارن SolidStart؟

إذا استخدمت Next.js أو Nuxt أو SvelteKit، فإن SolidStart يؤدي نفس الدور لـ SolidJS. الفرق الرئيسي هو نموذج التفاعلية في Solid — الإشارات والتأثيرات بدلاً من DOM الافتراضي — مما يوفر أداء تشغيل استثنائي.


الخطوة 1: إنشاء مشروع SolidStart جديد

افتح الطرفية وشغّل:

npx create-solid@latest task-manager

عند الطلب، اختر الخيارات التالية:

  • هل هذا مشروع SolidStart؟ → نعم
  • القالبbasic
  • استخدام TypeScript؟ → نعم

انتقل إلى المشروع وثبّت التبعيات:

cd task-manager
npm install

شغّل خادم التطوير:

npm run dev

زر http://localhost:3000 — يجب أن ترى صفحة الترحيب الافتراضية لـ SolidStart.


الخطوة 2: فهم هيكل المشروع

هذا ما أنشأه SolidStart لك:

task-manager/
├── public/                 # الملفات الثابتة
├── src/
│   ├── routes/            # التوجيه المبني على الملفات
│   │   └── index.tsx      # الصفحة الرئيسية (/)
│   ├── components/        # المكونات القابلة لإعادة الاستخدام
│   ├── app.tsx            # غلاف التطبيق الجذري
│   ├── entry-client.tsx   # نقطة دخول العميل
│   └── entry-server.tsx   # نقطة دخول الخادم
├── app.config.ts          # إعدادات SolidStart
├── tsconfig.json
└── package.json

الملفات الرئيسية

src/app.tsx — المكون الجذري الذي يغلف تطبيقك بالكامل. يُعدّ الموجّه ويُعرّف غلاف HTML:

import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
 
export default function App() {
  return (
    <Router
      root={(props) => (
        <Suspense>{props.children}</Suspense>
      )}
    >
      <FileRoutes />
    </Router>
  );
}

src/entry-server.tsx — يتعامل مع العرض من جانب الخادم:

import { createHandler, StartServer } from "@solidjs/start/server";
 
export default createHandler(() => (
  <StartServer
    document={({ assets, children, scripts }) => (
      <html lang="ar" dir="rtl">
        <head>
          <meta charset="utf-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          <link rel="icon" href="/favicon.ico" />
          {assets}
        </head>
        <body>
          <div id="app">{children}</div>
          {scripts}
        </body>
      </html>
    )}
  />
));

الخطوة 3: إعداد قاعدة البيانات

ثبّت better-sqlite3 لقاعدة بيانات بسيطة مبنية على الملفات:

npm install better-sqlite3
npm install -D @types/better-sqlite3

أنشئ ملف قاعدة البيانات في src/lib/db.ts:

import Database from "better-sqlite3";
import { join } from "path";
 
const db = new Database(join(process.cwd(), "tasks.db"));
 
// تفعيل وضع WAL لأداء أفضل
db.pragma("journal_mode = WAL");
 
// إنشاء جدول المهام
db.exec(`
  CREATE TABLE IF NOT EXISTS tasks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    description TEXT DEFAULT '',
    completed INTEGER DEFAULT 0,
    created_at TEXT DEFAULT (datetime('now')),
    updated_at TEXT DEFAULT (datetime('now'))
  )
`);
 
export { db };

الخطوة 4: إنشاء دوال الخادم لعمليات CRUD

أنشئ src/lib/tasks.ts لتعريف جميع عمليات البيانات على الخادم:

import { action, query, redirect } from "@solidjs/router";
import { db } from "./db";
 
// الأنواع
export interface Task {
  id: number;
  title: string;
  description: string;
  completed: number;
  created_at: string;
  updated_at: string;
}
 
// ─── الاستعلامات ──────────────────────────────────────
 
export const getTasks = query(async (filter?: string) => {
  "use server";
  let sql = "SELECT * FROM tasks";
 
  if (filter === "active") {
    sql += " WHERE completed = 0";
  } else if (filter === "completed") {
    sql += " WHERE completed = 1";
  }
 
  sql += " ORDER BY created_at DESC";
  return db.prepare(sql).all() as Task[];
}, "tasks");
 
export const getTask = query(async (id: number) => {
  "use server";
  return db.prepare("SELECT * FROM tasks WHERE id = ?").get(id) as Task | undefined;
}, "task");
 
export const getTaskStats = query(async () => {
  "use server";
  const total = db.prepare("SELECT COUNT(*) as count FROM tasks").get() as { count: number };
  const completed = db.prepare("SELECT COUNT(*) as count FROM tasks WHERE completed = 1").get() as { count: number };
  return {
    total: total.count,
    completed: completed.count,
    active: total.count - completed.count,
  };
}, "taskStats");
 
// ─── الإجراءات ──────────────────────────────────────
 
export const addTask = action(async (formData: FormData) => {
  "use server";
  const title = formData.get("title")?.toString().trim();
  const description = formData.get("description")?.toString().trim() ?? "";
 
  if (!title) {
    throw new Error("العنوان مطلوب");
  }
 
  db.prepare("INSERT INTO tasks (title, description) VALUES (?, ?)").run(title, description);
  throw redirect("/");
});
 
export const toggleTask = action(async (formData: FormData) => {
  "use server";
  const id = Number(formData.get("id"));
  db.prepare(
    "UPDATE tasks SET completed = CASE WHEN completed = 0 THEN 1 ELSE 0 END, updated_at = datetime('now') WHERE id = ?"
  ).run(id);
  throw redirect("/");
});
 
export const deleteTask = action(async (formData: FormData) => {
  "use server";
  const id = Number(formData.get("id"));
  db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
  throw redirect("/");
});
 
export const updateTask = action(async (formData: FormData) => {
  "use server";
  const id = Number(formData.get("id"));
  const title = formData.get("title")?.toString().trim();
  const description = formData.get("description")?.toString().trim() ?? "";
 
  if (!title) {
    throw new Error("العنوان مطلوب");
  }
 
  db.prepare(
    "UPDATE tasks SET title = ?, description = ?, updated_at = datetime('now') WHERE id = ?"
  ).run(title, description, id);
  throw redirect(`/tasks/${id}`);
});

شرح المفاهيم الرئيسية

  • query() — يغلف دالة الخادم لجلب البيانات. يتم تخزين النتائج مؤقتاً ويمكن تحميلها مسبقاً عند التنقل
  • action() — يغلف دالة الخادم للتعديلات (إنشاء، تحديث، حذف). تُعيد الإجراءات تحقق ذاكرة التخزين المؤقت تلقائياً
  • "use server" — هذا التوجيه يخبر SolidStart بتشغيل الدالة حصرياً على الخادم
  • throw redirect("/") — بعد التعديل، إعادة التوجيه. في SolidStart، تستخدم عمليات إعادة التوجيه من الإجراءات throw

الخطوة 5: بناء التخطيط الجذري مع التنقل

حدّث src/app.tsx لتضمين شريط التنقل:

import { A, Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
import "./app.css";
 
export default function App() {
  return (
    <Router
      root={(props) => (
        <div class="app">
          <header class="header">
            <div class="container">
              <A href="/" class="logo">
                مدير المهام
              </A>
              <nav>
                <A href="/" end>جميع المهام</A>
                <A href="/tasks/new">مهمة جديدة</A>
              </nav>
            </div>
          </header>
          <main class="container">
            <Suspense fallback={<div class="loading">جارٍ التحميل...</div>}>
              {props.children}
            </Suspense>
          </main>
        </div>
      )}
    >
      <FileRoutes />
    </Router>
  );
}

الخطوة 6: بناء الصفحة الرئيسية — قائمة المهام

استبدل محتوى src/routes/index.tsx بصفحة قائمة المهام:

import { For, Show } from "solid-js";
import { A, useSearchParams } from "@solidjs/router";
import { createAsync, type RouteDefinition } from "@solidjs/router";
import { getTasks, getTaskStats, toggleTask, deleteTask } from "~/lib/tasks";
 
export const route = {
  preload: () => {
    getTasks();
    getTaskStats();
  },
} satisfies RouteDefinition;
 
export default function Home() {
  const [searchParams, setSearchParams] = useSearchParams();
  const filter = () => searchParams.filter as string | undefined;
 
  const tasks = createAsync(() => getTasks(filter()));
  const stats = createAsync(() => getTaskStats());
 
  return (
    <div>
      <div class="page-header">
        <h1>مهامي</h1>
        <A href="/tasks/new" class="btn btn-primary">
          + إضافة مهمة
        </A>
      </div>
 
      <Show when={stats()}>
        {(s) => (
          <div class="stats">
            <div class="stat">
              <span class="stat-value">{s().total}</span>
              <span class="stat-label">الإجمالي</span>
            </div>
            <div class="stat">
              <span class="stat-value">{s().active}</span>
              <span class="stat-label">نشطة</span>
            </div>
            <div class="stat">
              <span class="stat-value">{s().completed}</span>
              <span class="stat-label">مكتملة</span>
            </div>
          </div>
        )}
      </Show>
 
      <div class="filters">
        <button
          class={`filter-btn ${!filter() ? "active" : ""}`}
          onClick={() => setSearchParams({ filter: undefined })}
        >
          الكل
        </button>
        <button
          class={`filter-btn ${filter() === "active" ? "active" : ""}`}
          onClick={() => setSearchParams({ filter: "active" })}
        >
          نشطة
        </button>
        <button
          class={`filter-btn ${filter() === "completed" ? "active" : ""}`}
          onClick={() => setSearchParams({ filter: "completed" })}
        >
          مكتملة
        </button>
      </div>
 
      <Show
        when={tasks()?.length}
        fallback={
          <div class="empty-state">
            <p>لا توجد مهام بعد. أنشئ مهمتك الأولى!</p>
            <A href="/tasks/new" class="btn btn-primary">
              إنشاء مهمة
            </A>
          </div>
        }
      >
        <ul class="task-list">
          <For each={tasks()}>
            {(task) => (
              <li class={`task-item ${task.completed ? "completed" : ""}`}>
                <form action={toggleTask} method="post" class="toggle-form">
                  <input type="hidden" name="id" value={task.id} />
                  <button type="submit" class="checkbox" aria-label="تبديل المهمة">
                    {task.completed ? "✓" : ""}
                  </button>
                </form>
 
                <A href={`/tasks/${task.id}`} class="task-content">
                  <span class="task-title">{task.title}</span>
                  <Show when={task.description}>
                    <span class="task-desc">{task.description}</span>
                  </Show>
                </A>
 
                <form action={deleteTask} method="post">
                  <input type="hidden" name="id" value={task.id} />
                  <button type="submit" class="btn btn-danger btn-sm">
                    حذف
                  </button>
                </form>
              </li>
            )}
          </For>
        </ul>
      </Show>
    </div>
  );
}

كيف يعمل تحميل البيانات

  1. route.preload — عندما ينتقل المستخدم إلى هذه الصفحة، يبدأ SolidStart في جلب البيانات مبكراً
  2. createAsync — ينشئ مورداً تفاعلياً يُعلّق العرض حتى تصبح البيانات جاهزة
  3. <For> — مكون الحلقة المحسّن في Solid. يُعيد عرض العناصر المتغيرة فقط
  4. <Show> — عرض شرطي يتجنب إنشاء DOM غير ضروري

الخطوة 7: إنشاء صفحة المهمة الجديدة

أنشئ src/routes/tasks/new.tsx:

import { A } from "@solidjs/router";
import { addTask } from "~/lib/tasks";
 
export default function NewTask() {
  return (
    <div>
      <A href="/" class="back-link">
        → العودة إلى المهام
      </A>
 
      <h1>إنشاء مهمة جديدة</h1>
 
      <form action={addTask} method="post" class="task-form">
        <div class="form-group">
          <label for="title">العنوان *</label>
          <input
            type="text"
            id="title"
            name="title"
            placeholder="ما الذي يجب القيام به؟"
            required
            autofocus
          />
        </div>
 
        <div class="form-group">
          <label for="description">الوصف</label>
          <textarea
            id="description"
            name="description"
            placeholder="أضف تفاصيل (اختياري)"
            rows="4"
          />
        </div>
 
        <div class="form-actions">
          <A href="/" class="btn btn-danger">
            إلغاء
          </A>
          <button type="submit" class="btn btn-primary">
            إنشاء المهمة
          </button>
        </div>
      </form>
    </div>
  );
}

كيف تعمل النماذج في SolidStart

لاحظ أنه لا يوجد معالج onSubmit أو useState. يستخدم النموذج action HTML أصلي يشير إلى إجراء الخادم addTask. عند الإرسال:

  1. يعترض SolidStart إرسال النموذج
  2. يُسلسل FormData
  3. يرسله إلى دالة الخادم
  4. تعالج دالة الخادم البيانات وتُعيد التوجيه
  5. يتم إبطال جميع ذاكرة التخزين المؤقت لـ query تلقائياً

هذا هو التحسين التدريجي — النموذج يعمل حتى بدون تفعيل JavaScript.


الخطوة 8: إنشاء صفحة تفاصيل المهمة

أنشئ src/routes/tasks/[id].tsx — الأقواس تجعل id معامل مسار ديناميكي:

import { Show } from "solid-js";
import { A, useParams } from "@solidjs/router";
import { createAsync, type RouteDefinition } from "@solidjs/router";
import { getTask, toggleTask, deleteTask, updateTask } from "~/lib/tasks";
import { createSignal } from "solid-js";
 
export const route = {
  preload: ({ params }) => getTask(Number(params.id)),
} satisfies RouteDefinition;
 
export default function TaskDetail() {
  const params = useParams();
  const task = createAsync(() => getTask(Number(params.id)));
  const [editing, setEditing] = createSignal(false);
 
  return (
    <div>
      <A href="/" class="back-link">
        → العودة إلى المهام
      </A>
 
      <Show when={task()} fallback={<p>المهمة غير موجودة.</p>}>
        {(t) => (
          <div class="task-detail">
            <Show
              when={!editing()}
              fallback={
                <form action={updateTask} method="post" class="edit-form">
                  <input type="hidden" name="id" value={t().id} />
                  <div class="form-group">
                    <label for="title">العنوان</label>
                    <input type="text" id="title" name="title" value={t().title} required />
                  </div>
                  <div class="form-group">
                    <label for="description">الوصف</label>
                    <textarea id="description" name="description" rows="4">
                      {t().description}
                    </textarea>
                  </div>
                  <div class="form-actions">
                    <button type="button" class="btn btn-danger" onClick={() => setEditing(false)}>
                      إلغاء
                    </button>
                    <button type="submit" class="btn btn-primary">
                      حفظ التغييرات
                    </button>
                  </div>
                </form>
              }
            >
              <div class="detail-header">
                <h1 class={t().completed ? "completed-title" : ""}>
                  {t().title}
                </h1>
                <div class="detail-actions">
                  <button class="btn btn-primary btn-sm" onClick={() => setEditing(true)}>
                    تعديل
                  </button>
                  <form action={toggleTask} method="post" style="display:inline">
                    <input type="hidden" name="id" value={t().id} />
                    <button type="submit" class="btn btn-sm" style={`background: ${t().completed ? "var(--border)" : "var(--success)"}; color: white;`}>
                      {t().completed ? "تنشيط" : "إكمال"}
                    </button>
                  </form>
                  <form action={deleteTask} method="post" style="display:inline">
                    <input type="hidden" name="id" value={t().id} />
                    <button type="submit" class="btn btn-danger btn-sm">
                      حذف
                    </button>
                  </form>
                </div>
              </div>
 
              <Show when={t().description}>
                <div class="detail-description">
                  <h3>الوصف</h3>
                  <p>{t().description}</p>
                </div>
              </Show>
 
              <div class="detail-meta">
                <p><strong>الحالة:</strong> <span class={`status ${t().completed ? "done" : "active"}`}>{t().completed ? "مكتملة" : "نشطة"}</span></p>
                <p><strong>تاريخ الإنشاء:</strong> {new Date(t().created_at).toLocaleDateString("ar")}</p>
                <p><strong>آخر تحديث:</strong> {new Date(t().updated_at).toLocaleDateString("ar")}</p>
              </div>
            </Show>
          </div>
        )}
      </Show>
    </div>
  );
}

شرح المسارات الديناميكية

  • [id].tsx — الأقواس تنشئ جزءاً ديناميكياً. /tasks/42 يربط id بـ "42"
  • useParams() — يصل إلى المعاملات الديناميكية بشكل تفاعلي
  • createSignal — مكافئ useState في React عند Solid، لكن مع تفاعلية دقيقة

الخطوة 9: إضافة مسار API

يدعم SolidStart مسارات API لبناء نقاط نهاية REST. أنشئ src/routes/api/tasks.ts:

import type { APIEvent } from "@solidjs/start/server";
import { db } from "~/lib/db";
import type { Task } from "~/lib/tasks";
 
export async function GET(event: APIEvent) {
  const url = new URL(event.request.url);
  const filter = url.searchParams.get("filter");
 
  let sql = "SELECT * FROM tasks";
  if (filter === "active") sql += " WHERE completed = 0";
  else if (filter === "completed") sql += " WHERE completed = 1";
  sql += " ORDER BY created_at DESC";
 
  const tasks = db.prepare(sql).all() as Task[];
 
  return new Response(JSON.stringify(tasks), {
    headers: { "Content-Type": "application/json" },
  });
}
 
export async function POST(event: APIEvent) {
  const body = await event.request.json();
 
  if (!body.title?.trim()) {
    return new Response(JSON.stringify({ error: "العنوان مطلوب" }), {
      status: 400,
      headers: { "Content-Type": "application/json" },
    });
  }
 
  const result = db
    .prepare("INSERT INTO tasks (title, description) VALUES (?, ?)")
    .run(body.title.trim(), body.description?.trim() ?? "");
 
  const task = db.prepare("SELECT * FROM tasks WHERE id = ?").get(result.lastInsertRowid);
 
  return new Response(JSON.stringify(task), {
    status: 201,
    headers: { "Content-Type": "application/json" },
  });
}

يمكنك الآن اختبار الـ API:

# جلب جميع المهام
curl http://localhost:3000/api/tasks
 
# إنشاء مهمة عبر API
curl -X POST http://localhost:3000/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "تعلم SolidStart", "description": "بناء مدير مهام"}'

الخطوة 10: إضافة معالجة الأخطاء

أنشئ صفحة حدود الخطأ في src/routes/*404.tsx:

import { A } from "@solidjs/router";
 
export default function NotFound() {
  return (
    <div class="not-found">
      <h1>404</h1>
      <p>الصفحة التي تبحث عنها غير موجودة.</p>
      <A href="/" class="btn btn-primary">
        العودة للرئيسية
      </A>
    </div>
  );
}

الخطوة 11: إعداد وبناء للإنتاج

حدّث app.config.ts:

import { defineConfig } from "@solidjs/start/config";
 
export default defineConfig({
  server: {
    preset: "node-server",
  },
});

ابنِ وشغّل:

npm run build
node .output/server/index.mjs

النشر على منصات أخرى

المنصةالإعداد المسبق
Vercelvercel
Netlifynetlify
Cloudflare Pagescloudflare-pages
AWS Lambdaaws-lambda
Node.jsnode-server
ثابت (SSG)static

اختبار التطبيق

تحقق من أن كل شيء يعمل:

  1. إنشاء مهام — انتقل إلى /tasks/new، املأ العنوان والوصف، أرسل
  2. عرض المهام — الصفحة الرئيسية تعرض جميع المهام مع الإحصائيات
  3. تصفية المهام — انقر "نشطة" أو "مكتملة" للتصفية
  4. تبديل الإكمال — انقر مربع الاختيار لتحديد المهام
  5. عرض التفاصيل — انقر عنوان المهمة لرؤية صفحة التفاصيل
  6. تعديل المهام — في صفحة التفاصيل، انقر "تعديل"
  7. حذف المهام — انقر "حذف" لإزالة مهمة
  8. اختبار الـ API — استخدم curl لاختبار /api/tasks

استكشاف الأخطاء

المشاكل الشائعة

"Cannot find module 'better-sqlite3'"

تأكد من تثبيته: npm install better-sqlite3 @types/better-sqlite3

"tasks.db is locked"

يحدث عند وصول عمليات متعددة لقاعدة البيانات. وضع WAL الذي فعّلناه يساعد، لكن تأكد من تشغيل خادم تطوير واحد فقط.

أخطاء TypeScript مع معاملات المسار

حوّل معاملات المسار دائماً: Number(params.id) — المعاملات دائماً سلاسل نصية.


مقارنة SolidStart مع أُطر العمل الأخرى

الميزةSolidStartNext.jsSvelteKitNuxt
التفاعليةإشارات دقيقةDOM افتراضيوقت التجميعDOM افتراضي (Vue)
حجم الحزمة~7 KB~85 KB~15 KB~60 KB
دوال الخادم"use server"Server ActionsForm actionsمجلد server/
تحميل البياناتquery + createAsyncfetch في RSCدوال loaduseFetch

الخطوات التالية

بعد بناء تطبيق كامل مع SolidStart، إليك بعض الأفكار للتوسع:

  • إضافة المصادقة — استخدم Clerk أو Lucia
  • التبديل إلى PostgreSQL — استبدل SQLite بقاعدة بيانات إنتاجية باستخدام Drizzle ORM
  • إضافة تحديثات فورية — استخدم WebSockets أو Server-Sent Events
  • تطبيق السحب والإفلات — أضف إعادة ترتيب المهام
  • النشر في الإنتاج — جرّب النشر على Vercel أو Cloudflare Pages

موارد مفيدة


الخلاصة

لقد بنيت مدير مهام كامل باستخدام SolidStart، وغطّيت:

  • التوجيه المبني على الملفات مع المعاملات الديناميكية
  • دوال الخادم باستخدام توجيه "use server" للوصول الآمن للبيانات
  • تحميل البيانات التفاعلي مع query وcreateAsync
  • تعديلات مبنية على النماذج مع action والتحسين التدريجي
  • مسارات API لنقاط نهاية REST
  • تخزين SQLite للاستمرارية
  • نشر الإنتاج مع إعدادات مسبقة قابلة للتكوين

يجلب SolidStart مزايا أداء التفاعلية الدقيقة لـ SolidJS إلى التطوير الكامل، مع تجربة مطور ممتازة. حجم حزمته الصغير وتشغيله السريع وواجهاته البرمجية البديهية تجعله خياراً مقنعاً لتطبيقات الويب الحديثة.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على تبسيط نظام التصميم الخاص بك: دليل لاستخدام shadcn Registry MCP.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة