بناء تطبيق كامل يعمل بالوقت الحقيقي باستخدام Convex و Next.js 15

Convex هي منصة خلفية كخدمة (BaaS) تفاعلية تستبدل قاعدة البيانات ودوال الخادم والبنية التحتية للوقت الحقيقي بمنصة واحدة مبنية على TypeScript. على عكس الخلفيات التقليدية حيث تحتاج لربط نقاط REST ومكتبات ORM وخوادم WebSocket بشكل منفصل، يمنحك Convex قاعدة بيانات تفاعلية تدفع التحديثات تلقائياً لكل عميل متصل في اللحظة التي تتغير فيها البيانات.
في هذا الدليل، ستبني تطبيق ملاحظات تعاوني يعمل بالوقت الحقيقي — يشبه نسخة مبسطة من Notion حيث يمكن لعدة مستخدمين إنشاء وتعديل وتنظيم الملاحظات التي تتزامن فوراً عبر جميع المتصفحات. خلال هذه الرحلة، ستتقن مخططات Convex والاستعلامات والتعديلات والاشتراكات الفورية ورفع الملفات والمصادقة مع Clerk.
المتطلبات الأساسية
قبل البدء، تأكد من أن لديك:
- Node.js 20+ مثبت على جهازك
- حساب Convex مجاني — سجّل في convex.dev
- معرفة أساسية بـ React و TypeScript
- إلمام بـ Next.js App Router (التخطيطات، مكونات الخادم، مكونات العميل)
- محرر أكواد (VS Code مُوصى به)
ما ستبنيه
تطبيق ملاحظات تعاوني يتضمن الميزات التالية:
- مزامنة فورية للملاحظات — تظهر التغييرات فوراً على جميع العملاء المتصلين
- إدارة شاملة للملاحظات — إنشاء وتعديل وحذف وتثبيت والبحث في الملاحظات
- مرفقات الملفات — رفع الصور والمستندات للملاحظات عبر تخزين Convex
- مصادقة المستخدمين — تسجيل الدخول مع Clerk، بيانات مخصصة لكل مستخدم
- أمان أنواع شامل — من مخطط قاعدة البيانات إلى مكونات React، كل شيء مُحدد الأنواع
الخطوة 1: إنشاء مشروع Next.js
ابدأ بإنشاء مشروع Next.js 15 جديد مع TypeScript و Tailwind CSS:
npx create-next-app@latest convex-notes --typescript --tailwind --eslint --app --src-dir --use-npm
cd convex-notesعند ظهور الخيارات، اقبل الإعدادات الافتراضية. هذا ينشئ مشروع Next.js مع App Router وبنية مجلد src/.
الخطوة 2: تثبيت Convex
ثبّت مكتبة Convex وقم بتهيئة مشروعك:
npm install convex
npx convex initأمر convex init ينشئ مجلد convex/ في جذر المشروع. هنا يعيش كل كود الخلفية — تعريفات المخططات والاستعلامات والتعديلات والإجراءات. ينشئ Convex أيضاً ملف .env.local يحتوي على NEXT_PUBLIC_CONVEX_URL تلقائياً.
بنية المشروع تبدو هكذا:
convex-notes/
├── convex/ # كود الخلفية (يعمل على خوادم Convex)
│ ├── _generated/ # أنواع ومراجع API مُولّدة تلقائياً
│ └── tsconfig.json
├── src/
│ └── app/ # صفحات Next.js App Router
├── .env.local # NEXT_PUBLIC_CONVEX_URL
└── package.json
الخطوة 3: تعريف مخطط قاعدة البيانات
يستخدم Convex نظام مخططات يعتمد على TypeScript أولاً. كل جدول وحقل يُعرّف بمدققات توفر التحقق في وقت التشغيل واستنتاج الأنواع في وقت الترجمة.
أنشئ ملف المخطط في convex/schema.ts:
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
notes: defineTable({
userId: v.string(),
title: v.string(),
content: v.string(),
isPinned: v.boolean(),
fileId: v.optional(v.id("_file_storage")),
fileName: v.optional(v.string()),
})
.index("by_user", ["userId"])
.index("by_user_pinned", ["userId", "isPinned"])
.searchIndex("search_notes", {
searchField: "content",
filterFields: ["userId"],
}),
});المفاهيم الأساسية هنا:
- مدققات
v— تعريف أنواع الحقول مع التحقق في وقت التشغيل مثلv.string()وv.boolean()وv.optional()وv.id() - الفهارس —
by_userيُمكّن الاستعلامات الفعّالة المُفلترة بـuserId. يتطلب Convex التصريح بالفهارس مسبقاً - فهرس البحث —
search_notesيُمكّن البحث النصي الكامل على حقلcontent _file_storage— جدول مدمج في Convex للملفات المرفوعة
ادفع المخطط إلى نسخة Convex:
npx convex devأبقِ هذا الأمر يعمل في الطرفية — يراقب التغييرات وينشر كود الخلفية تلقائياً. هذه واحدة من أفضل ميزات Convex: إعادة تحميل فورية للخلفية.
الخطوة 4: كتابة دوال الاستعلام
استعلامات Convex هي دوال TypeScript تعمل على الخادم وتُعاد تشغيلها تلقائياً عند تغير البيانات. يستلم العملاء المتصلون التحديثات بالوقت الحقيقي بدون أي استطلاع أو إعداد WebSocket.
أنشئ convex/notes.ts:
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
// عرض جميع ملاحظات المستخدم الحالي
export const list = query({
args: { userId: v.string() },
returns: v.array(
v.object({
_id: v.id("notes"),
_creationTime: v.number(),
userId: v.string(),
title: v.string(),
content: v.string(),
isPinned: v.boolean(),
fileId: v.optional(v.id("_file_storage")),
fileName: v.optional(v.string()),
})
),
handler: async (ctx, args) => {
const notes = await ctx.db
.query("notes")
.withIndex("by_user", (q) => q.eq("userId", args.userId))
.order("desc")
.collect();
// ترتيب الملاحظات المُثبّتة في الأعلى
return notes.sort((a, b) => {
if (a.isPinned && !b.isPinned) return -1;
if (!a.isPinned && b.isPinned) return 1;
return 0;
});
},
});
// البحث في الملاحظات حسب المحتوى
export const search = query({
args: {
userId: v.string(),
searchTerm: v.string(),
},
handler: async (ctx, args) => {
const results = await ctx.db
.query("notes")
.withSearchIndex("search_notes", (q) =>
q.search("content", args.searchTerm).eq("userId", args.userId)
)
.collect();
return results;
},
});لاحظ كيف تُصرّح الاستعلامات عن args بمدققات. يتحقق Convex منها في وقت التشغيل و يستنتج أنواع TypeScript في وقت الترجمة.
الخطوة 5: كتابة دوال التعديل
التعديلات هي طريقة تغيير البيانات في Convex. مثل الاستعلامات، هي مُتحقق منها ومُحددة الأنواع وتعاملية — كل تعديل يعمل في معاملة قابلة للتسلسل.
أضف هذه التعديلات إلى convex/notes.ts:
// إنشاء ملاحظة جديدة
export const create = mutation({
args: {
userId: v.string(),
title: v.string(),
content: v.string(),
},
handler: async (ctx, args) => {
const noteId = await ctx.db.insert("notes", {
userId: args.userId,
title: args.title,
content: args.content,
isPinned: false,
});
return noteId;
},
});
// تحديث ملاحظة موجودة
export const update = mutation({
args: {
noteId: v.id("notes"),
title: v.optional(v.string()),
content: v.optional(v.string()),
},
handler: async (ctx, args) => {
const { noteId, ...updates } = args;
const cleanUpdates: Record<string, string> = {};
if (updates.title !== undefined) cleanUpdates.title = updates.title;
if (updates.content !== undefined) cleanUpdates.content = updates.content;
await ctx.db.patch(noteId, cleanUpdates);
},
});
// تبديل حالة التثبيت
export const togglePin = mutation({
args: { noteId: v.id("notes") },
handler: async (ctx, args) => {
const note = await ctx.db.get(args.noteId);
if (!note) throw new Error("Note not found");
await ctx.db.patch(args.noteId, { isPinned: !note.isPinned });
},
});
// حذف ملاحظة
export const remove = mutation({
args: { noteId: v.id("notes") },
handler: async (ctx, args) => {
const note = await ctx.db.get(args.noteId);
if (!note) throw new Error("Note not found");
if (note.fileId) {
await ctx.storage.delete(note.fileId);
}
await ctx.db.delete(args.noteId);
},
});نقاط مهمة:
ctx.db.insert()— ينشئ مستنداً جديداً ويُعيد معرّفهctx.db.patch()— يُحدّث المستند جزئياً (الحقول المحددة فقط)ctx.db.delete()— يحذف مستنداًctx.storage.delete()— يحذف ملفاً من تخزين Convex- كل تعديل ذري — إذا فشلت أي خطوة، تُلغى المعاملة بأكملها
الخطوة 6: إعداد مزوّد Convex
لاستخدام Convex في مكونات React، تحتاج لتغليف تطبيقك بـ ConvexProvider.
أنشئ src/components/ConvexClientProvider.tsx:
"use client";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { ReactNode } from "react";
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export default function ConvexClientProvider({
children,
}: {
children: ReactNode;
}) {
return <ConvexProvider client={convex}>{children}</ConvexProvider>;
}ثم غلّف تطبيقك في src/app/layout.tsx:
import type { Metadata } from "next";
import "./globals.css";
import ConvexClientProvider from "@/components/ConvexClientProvider";
export const metadata: Metadata = {
title: "Convex Notes",
description: "ملاحظات تعاونية بالوقت الحقيقي مدعومة بـ Convex",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ar" dir="rtl">
<body>
<ConvexClientProvider>{children}</ConvexClientProvider>
</body>
</html>
);
}الخطوة 7: بناء واجهة الملاحظات
الآن الجزء المثير — بناء مكونات React التي تستهلك خلفية Convex. سحر Convex هو أن useQuery يشترك تلقائياً في التحديثات الفورية. عندما ينشئ أي عميل ملاحظة أو يعدّلها أو يحذفها، يرى كل عميل متصل آخر التغيير فوراً.
أنشئ src/app/page.tsx:
"use client";
import { useQuery, useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
import { useState } from "react";
import { Id } from "../../convex/_generated/dataModel";
const USER_ID = "demo-user";
export default function NotesApp() {
const notes = useQuery(api.notes.list, { userId: USER_ID });
const createNote = useMutation(api.notes.create);
const updateNote = useMutation(api.notes.update);
const togglePin = useMutation(api.notes.togglePin);
const removeNote = useMutation(api.notes.remove);
const [editingId, setEditingId] = useState<Id<"notes"> | null>(null);
const [searchTerm, setSearchTerm] = useState("");
const searchResults = useQuery(
api.notes.search,
searchTerm.length > 0 ? { userId: USER_ID, searchTerm } : "skip"
);
const displayNotes = searchTerm.length > 0 ? searchResults : notes;
const handleCreate = async () => {
await createNote({
userId: USER_ID,
title: "ملاحظة بدون عنوان",
content: "",
});
};
if (notes === undefined) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
</div>
);
}
return (
<main className="max-w-4xl mx-auto p-6">
<header className="flex items-center justify-between mb-8">
<h1 className="text-3xl font-bold">ملاحظات Convex</h1>
<button
onClick={handleCreate}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition"
>
+ ملاحظة جديدة
</button>
</header>
<input
type="text"
placeholder="ابحث في الملاحظات..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full p-3 border rounded-lg mb-6 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{displayNotes?.map((note) => (
<div
key={note._id}
className={`border rounded-lg p-4 hover:shadow-md transition ${
note.isPinned ? "border-yellow-400 bg-yellow-50" : ""
}`}
>
<div className="flex items-start justify-between mb-2">
<h2 className="font-semibold text-lg">{note.title}</h2>
<button
onClick={() => togglePin({ noteId: note._id })}
className="text-xl"
>
{note.isPinned ? "📌" : "📍"}
</button>
</div>
<p className="text-gray-600 mb-4 line-clamp-3">
{note.content || "ملاحظة فارغة..."}
</p>
<div className="flex gap-2">
<button
onClick={() => setEditingId(note._id)}
className="text-sm text-blue-600 hover:underline"
>
تعديل
</button>
<button
onClick={() => removeNote({ noteId: note._id })}
className="text-sm text-red-600 hover:underline"
>
حذف
</button>
</div>
</div>
))}
</div>
{displayNotes?.length === 0 && (
<p className="text-center text-gray-400 mt-12">
لا توجد ملاحظات بعد. انقر على "+ ملاحظة جديدة" للبدء.
</p>
)}
</main>
);
}الفكرة الأساسية هي useQuery. عندما تستدعي useQuery(api.notes.list, ...) يقوم Convex بـ:
- تشغيل دالة الاستعلام على الخادم
- إرسال النتيجة للعميل
- الاشتراك في جميع الجداول التي لمسها الاستعلام
- إعادة تشغيل الاستعلام تلقائياً ودفع النتائج الجديدة عند تغير البيانات
هذا مختلف جذرياً عن واجهات REST أو حتى اشتراكات GraphQL — لا توجد إبطال يدوي للذاكرة المؤقتة، ولا تحديثات تفاؤلية لإدارتها، ولا بيانات قديمة.
الخطوة 8: إضافة رفع الملفات
يملك Convex تخزين ملفات مدمج. يمكنك رفع الملفات مباشرة من العميل والإشارة إليها في مستنداتك.
أضف تعديل رفع الملفات إلى convex/notes.ts:
// توليد رابط رفع للعميل
export const generateUploadUrl = mutation({
handler: async (ctx) => {
return await ctx.storage.generateUploadUrl();
},
});
// إرفاق ملف بملاحظة
export const attachFile = mutation({
args: {
noteId: v.id("notes"),
fileId: v.id("_file_storage"),
fileName: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.patch(args.noteId, {
fileId: args.fileId,
fileName: args.fileName,
});
},
});أنشئ مكون رفع الملفات في src/components/FileUpload.tsx:
"use client";
import { useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
import { Id } from "../../convex/_generated/dataModel";
import { useRef } from "react";
export default function FileUpload({ noteId }: { noteId: Id<"notes"> }) {
const generateUploadUrl = useMutation(api.notes.generateUploadUrl);
const attachFile = useMutation(api.notes.attachFile);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// الخطوة 1: الحصول على رابط رفع مؤقت من Convex
const uploadUrl = await generateUploadUrl();
// الخطوة 2: إرسال الملف إلى رابط الرفع
const response = await fetch(uploadUrl, {
method: "POST",
headers: { "Content-Type": file.type },
body: file,
});
const { storageId } = await response.json();
// الخطوة 3: حفظ مرجع الملف في الملاحظة
await attachFile({
noteId,
fileId: storageId,
fileName: file.name,
});
};
return (
<div>
<input
type="file"
ref={fileInputRef}
onChange={handleUpload}
className="hidden"
/>
<button
onClick={() => fileInputRef.current?.click()}
className="text-sm text-gray-500 hover:text-gray-700"
>
📎 إرفاق ملف
</button>
</div>
);
}الخطوة 9: إضافة المصادقة مع Clerk
للتطبيقات الإنتاجية، تحتاج لتحديد البيانات لكل مستخدم. يتكامل Convex بسلاسة مع Clerk و Auth0 وغيرها.
ثبّت حزم Clerk:
npm install @clerk/nextjsسجّل حساباً مجانياً في Clerk على clerk.com، أنشئ تطبيقاً، وأضف مفاتيحك إلى .env.local:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
NEXT_PUBLIC_CONVEX_URL=https://your-project.convex.cloudحدّث مزوّد Convex للتكامل مع Clerk. استبدل src/components/ConvexClientProvider.tsx:
"use client";
import { ConvexProviderWithClerk } from "convex/react-clerk";
import { ConvexReactClient } from "convex/react";
import { ClerkProvider, useAuth } from "@clerk/nextjs";
import { ReactNode } from "react";
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export default function ConvexClientProvider({
children,
}: {
children: ReactNode;
}) {
return (
<ClerkProvider>
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
{children}
</ConvexProviderWithClerk>
</ClerkProvider>
);
}ثم أنشئ convex/auth.config.ts:
export default {
providers: [
{
domain: process.env.CLERK_JWT_ISSUER_DOMAIN,
applicationID: "convex",
},
],
};الآن حدّث استعلاماتك لاستخدام المستخدمين المُصادق عليهم:
export const list = query({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return [];
const userId = identity.subject;
const notes = await ctx.db
.query("notes")
.withIndex("by_user", (q) => q.eq("userId", userId))
.order("desc")
.collect();
return notes.sort((a, b) => {
if (a.isPinned && !b.isPinned) return -1;
if (!a.isPinned && b.isPinned) return 1;
return 0;
});
},
});مع ctx.auth.getUserIdentity()، يتحقق Convex تلقائياً من رمز JWT من Clerk. كل مستخدم يرى ملاحظاته فقط.
الخطوة 10: التحديثات التفاؤلية
رغم أن Convex سريع أصلاً (التحديثات تصل خلال 20-50 ملي ثانية)، يمكنك جعل الواجهة أكثر استجابة مع التحديثات التفاؤلية:
const createNote = useMutation(api.notes.create).withOptimisticUpdate(
(localStore, args) => {
const existingNotes = localStore.getQuery(api.notes.list, {});
if (existingNotes === undefined) return;
localStore.setQuery(api.notes.list, {}, [
{
_id: crypto.randomUUID() as any,
_creationTime: Date.now(),
userId: args.userId,
title: args.title,
content: args.content,
isPinned: false,
},
...existingNotes,
]);
}
);التحديثات التفاؤلية تعمل عن طريق تعديل الذاكرة المؤقتة المحلية. عندما يؤكد الخادم التعديل، يستبدل Convex النتيجة التفاؤلية بالحقيقية. إذا فشل التعديل، يُلغى التحديث التفاؤلي تلقائياً.
الخطوة 11: النشر للإنتاج
يفصل Convex بين نسخ التطوير والإنتاج:
# نشر الخلفية للإنتاج
npx convex deploy
# بناء ونشر واجهة Next.js
npm run buildخلفية Convex الآن تعمل على بنية Convex المُدارة — موزعة عالمياً، ذاتية التوسع، مع نسخ احتياطية تلقائية. لا يوجد خادم لإدارته أو قاعدة بيانات لضبطها.
اختبار التطبيق
- المزامنة الفورية: افتح التطبيق في نافذتين جنباً إلى جنب. أنشئ ملاحظة في واحدة — يجب أن تظهر فوراً في الأخرى
- البحث: اكتب في شريط البحث وتحقق من تصفية النتائج فوراً
- التثبيت/إلغاء التثبيت: ثبّت ملاحظة وتحقق من انتقالها لأعلى القائمة
- رفع الملفات: أرفق ملفاً وتحقق من بقائه بعد إعادة تحميل الصفحة
- المصادقة: سجّل الخروج وتحقق من عدم إمكانية الوصول للملاحظات
مقارنة Convex مع الخلفيات التقليدية
| الميزة | الخلفية التقليدية | Convex |
|---|---|---|
| تحديثات الوقت الحقيقي | إعداد WebSocket يدوي | تلقائي مع useQuery |
| أمان الأنواع | أنواع ORM + أنواع API منفصلة | شامل من المخطط للعميل |
| المعاملات | إدارة يدوية للمعاملات | كل تعديل هو معاملة |
| التخزين المؤقت | Redis وإبطال الاستعلامات | ذاكرة مؤقتة تفاعلية تلقائية |
| تخزين الملفات | S3 وروابط موقّعة | ctx.storage مدمج |
| النشر | Docker و Kubernetes | npx convex deploy |
| التوسع | توسع أفقي يدوي | تلقائي |
استكشاف الأخطاء
"Could not find module convex/_generated": شغّل npx convex dev لتوليد الأنواع. مجلد _generated يُنشأ تلقائياً عند تشغيل خادم التطوير.
الاستعلامات تُعيد undefined: useQuery يُعيد undefined أثناء التحميل. تعامل دائماً مع حالة التحميل قبل الوصول للبيانات.
أخطاء "Not authenticated": تأكد من ضبط نطاق مُصدر JWT لـ Clerk بشكل صحيح في متغيرات بيئة Convex.
الخطوات التالية
- إضافة تحرير نصي غني مع Tiptap أو Slate لتجربة أشبه بـ Notion
- تنفيذ المشاركة — السماح للمستخدمين بمشاركة الملاحظات عبر روابط فريدة
- إضافة دوال Convex المجدولة لميزات مثل التذكيرات أو الأرشفة التلقائية
- استكشاف إجراءات Convex لاستدعاء واجهات خارجية (إشعارات البريد، تلخيص بالذكاء الاصطناعي)
الخلاصة
لقد بنيت تطبيقاً كاملاً تفاعلياً يعمل بالوقت الحقيقي مع Convex و Next.js 15. النقاط الرئيسية:
- Convex يُزيل الكود الوسيط — لا نقاط REST، لا إعداد ORM، لا تهيئة WebSocket
- الوقت الحقيقي هو الافتراضي — كل
useQueryيشترك تلقائياً في التحديثات الفورية - TypeScript طوال الطريق — المدققات تُولّد أنواعاً تتدفق من قاعدة البيانات للواجهة
- المعاملات تلقائية — كل تعديل قابل للتسلسل، مما يُزيل ظروف السباق
- تخزين الملفات مدمج — لا حاجة لحاوية S3 منفصلة أو تهيئة CDN
يُمثل Convex تحولاً في طريقة تفكيرنا بالخلفيات: بدلاً من بناء بنية تحتية لنقل البيانات، تكتب دوال TypeScript تقرأ وتكتب البيانات، و Convex يتولى كل شيء آخر.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

بناء تطبيق فوري مع Supabase و Next.js 15: الدليل الشامل
تعلّم كيفية بناء تطبيق full-stack فوري باستخدام Supabase و Next.js 15 App Router. يغطي هذا الدليل المصادقة وإعداد قاعدة البيانات و Row Level Security والاشتراكات الفورية.

بناء تطبيق متكامل باستخدام Drizzle ORM و Next.js 15: قاعدة بيانات آمنة الأنواع من الصفر إلى الإنتاج
تعلّم كيفية بناء تطبيق متكامل آمن الأنواع باستخدام Drizzle ORM مع Next.js 15. يغطي هذا الدليل العملي تصميم المخططات والترحيلات وServer Actions وعمليات CRUD والنشر مع PostgreSQL.

إضافة المصادقة لتطبيق Next.js 15 باستخدام Auth.js v5: البريد الإلكتروني وOAuth والتحكم بالأدوار
تعلم كيفية إضافة نظام مصادقة جاهز للإنتاج لتطبيق Next.js 15 باستخدام Auth.js v5. يغطي هذا الدليل الشامل تسجيل الدخول عبر Google OAuth وبيانات الاعتماد بالبريد الإلكتروني وكلمة المرور والمسارات المحمية والتحكم بالوصول حسب الأدوار.