أصبح Supabase البديل مفتوح المصدر الأول لـ Firebase، حيث يوفر واجهة خلفية متكاملة مع PostgreSQL والمصادقة والاشتراكات الفورية والتخزين — كل ذلك جاهز للاستخدام. عند دمجه مع Next.js 15 و App Router، تحصل على إطار عمل full-stack قوي يتعامل مع العرض من جانب الخادم و Server Actions والتفاعلات السلسة مع العميل.
في هذا الدليل، ستبني لوحة مهام تعاونية فورية — نسخة مصغرة من Trello حيث يمكن لعدة مستخدمين إضافة وتعديل وحذف المهام التي تتزامن فوريًا عبر جميع المتصفحات المتصلة. ستتعلم إتقان Supabase Auth و Row Level Security (RLS) و triggers قاعدة البيانات ونظام Realtime Broadcast.
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js 20+ مثبّت
- حساب Supabase — مجاني على supabase.com
- معرفة أساسية بـ React و TypeScript
- إلمام بـ Next.js App Router (الصفحات، التخطيطات، Server Components)
- محرر أكواد (يُنصح بـ VS Code)
ما الذي ستبنيه
لوحة مهام تعاونية بالميزات التالية:
- مصادقة بالبريد الإلكتروني/كلمة المرور مع مسارات محمية
- تحديثات فورية للمهام عبر جميع العملاء المتصلين
- Row Level Security لضمان أن كل مستخدم يرى بياناته فقط
- Server Components لتحميل البيانات الأولي
- Server Actions للعمليات
- Middleware لإدارة الجلسات
الخطوة 1: إنشاء مشروع Supabase
توجه إلى supabase.com/dashboard وأنشئ مشروعًا جديدًا. بمجرد جاهزية مشروعك، سجّل هذه القيم من Settings > API:
Project URL— عنوان URL لنسخة Supabase الخاصة بكanon public— مفتاح آمن للاستخدام في جانب العميلservice_role— وصول المسؤول (حافظ على سريته)
الخطوة 2: إعداد مخطط قاعدة البيانات
اذهب إلى SQL Editor في لوحة تحكم Supabase ونفّذ الكود التالي:
-- إنشاء جدول الملفات الشخصية المرتبط بـ auth.users
create table public.profiles (
id uuid references auth.users on delete cascade not null primary key,
email text,
display_name text,
created_at timestamptz default now() not null
);
-- إنشاء جدول المهام
create table public.tasks (
id uuid default gen_random_uuid() primary key,
user_id uuid references auth.users on delete cascade not null,
title text not null,
description text,
status text default 'todo' check (status in ('todo', 'in_progress', 'done')),
created_at timestamptz default now() not null,
updated_at timestamptz default now() not null
);
-- تفعيل Row Level Security
alter table public.profiles enable row level security;
alter table public.tasks enable row level security;
-- الملفات الشخصية: يمكن للمستخدمين قراءة وتعديل ملفهم الشخصي
create policy "Users can view own profile"
on public.profiles for select
using (auth.uid() = id);
create policy "Users can update own profile"
on public.profiles for update
using (auth.uid() = id);
-- المهام: يمكن للمستخدمين إدارة مهامهم الخاصة
create policy "Users can view own tasks"
on public.tasks for select
using (auth.uid() = user_id);
create policy "Users can create tasks"
on public.tasks for insert
with check (auth.uid() = user_id);
create policy "Users can update own tasks"
on public.tasks for update
using (auth.uid() = user_id);
create policy "Users can delete own tasks"
on public.tasks for delete
using (auth.uid() = user_id);
-- إنشاء ملف شخصي تلقائيًا عند التسجيل
create or replace function public.handle_new_user()
returns trigger as $$
begin
insert into public.profiles (id, email, display_name)
values (new.id, new.email, split_part(new.email, '@', 1));
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row execute function public.handle_new_user();
-- تحديث updated_at تلقائيًا
create or replace function public.update_updated_at()
returns trigger as $$
begin
new.updated_at = now();
return new;
end;
$$ language plpgsql;
create trigger tasks_updated_at
before update on public.tasks
for each row execute function public.update_updated_at();يُنشئ هذا المخطط:
- جدول
profilesيُملأ تلقائيًا عند تسجيل المستخدم - جدول
tasksمع تتبع الحالة - سياسات RLS لضمان وصول كل مستخدم لبياناته فقط
- Triggers للطوابع الزمنية التلقائية
الخطوة 3: إعداد البث الفوري (Realtime Broadcasting)
للتحديثات الفورية القابلة للتوسع، يوصي Supabase بـ Broadcast عبر triggers قاعدة البيانات بدلاً من نهج postgres_changes القديم. نفّذ هذا في SQL Editor:
-- Trigger البث لجدول المهام
create or replace function broadcast_task_changes()
returns trigger as $$
begin
perform realtime.broadcast_changes(
'tasks:updates',
TG_OP,
TG_OP,
TG_TABLE_NAME,
TG_TABLE_SCHEMA,
NEW,
OLD
);
return null;
end;
$$ language plpgsql security definer;
create trigger tasks_broadcast_trigger
after insert or update or delete on public.tasks
for each row execute function broadcast_task_changes();الخطوة 4: تهيئة مشروع Next.js
npx create-next-app@latest task-board --typescript --tailwind --eslint --app --src-dir
cd task-boardثبّت حزم Supabase:
npm install @supabase/supabase-js @supabase/ssrأنشئ ملف .env.local:
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-hereالخطوة 5: إنشاء عملاء Supabase
تحتاج إلى عميلين: واحد لجانب الخادم (Server Components, Server Actions, Middleware) وآخر للمتصفح.
عميل المتصفح
// src/lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}عميل الخادم
// src/lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// تم الاستدعاء من Server Component — يمكن تجاهله
// إذا كان الـ middleware يتولى تحديث الجلسات
}
},
},
}
)
}الخطوة 6: إضافة Middleware لإدارة الجلسات
يقوم الـ Middleware بتحديث جلسة المستخدم مع كل طلب، مما يمنع انتهاء صلاحية الرموز:
// src/middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value)
)
supabaseResponse = NextResponse.next({ request })
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
const {
data: { user },
} = await supabase.auth.getUser()
// إعادة توجيه المستخدمين غير المصادق عليهم إلى صفحة تسجيل الدخول
if (
!user &&
!request.nextUrl.pathname.startsWith('/login') &&
!request.nextUrl.pathname.startsWith('/signup') &&
!request.nextUrl.pathname.startsWith('/auth')
) {
const url = request.nextUrl.clone()
url.pathname = '/login'
return NextResponse.redirect(url)
}
// إعادة توجيه المستخدمين المصادق عليهم بعيدًا عن صفحات المصادقة
if (user && (request.nextUrl.pathname === '/login' || request.nextUrl.pathname === '/signup')) {
const url = request.nextUrl.clone()
url.pathname = '/dashboard'
return NextResponse.redirect(url)
}
return supabaseResponse
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}الخطوة 7: بناء صفحات المصادقة
صفحة تسجيل الدخول
// src/app/login/page.tsx
'use client'
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
export default function LoginPage() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const router = useRouter()
const supabase = createClient()
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError(null)
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) {
setError(error.message)
setLoading(false)
return
}
router.push('/dashboard')
router.refresh()
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-xl shadow-lg">
<h2 className="text-3xl font-bold text-center text-gray-900">
تسجيل الدخول إلى لوحة المهام
</h2>
<form onSubmit={handleLogin} className="space-y-6">
{error && (
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
{error}
</div>
)}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
البريد الإلكتروني
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
كلمة المرور
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full flex justify-center py-2 px-4 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'جارٍ تسجيل الدخول...' : 'تسجيل الدخول'}
</button>
</form>
<p className="text-center text-sm text-gray-600">
ليس لديك حساب؟{' '}
<a href="/signup" className="text-blue-600 hover:underline">
إنشاء حساب
</a>
</p>
</div>
</div>
)
}صفحة التسجيل
// src/app/signup/page.tsx
'use client'
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
export default function SignupPage() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const router = useRouter()
const supabase = createClient()
const handleSignup = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError(null)
const { error } = await supabase.auth.signUp({
email,
password,
})
if (error) {
setError(error.message)
setLoading(false)
return
}
router.push('/dashboard')
router.refresh()
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-xl shadow-lg">
<h2 className="text-3xl font-bold text-center text-gray-900">
إنشاء حسابك
</h2>
<form onSubmit={handleSignup} className="space-y-6">
{error && (
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
{error}
</div>
)}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
البريد الإلكتروني
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
كلمة المرور (6 أحرف كحد أدنى)
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={6}
className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full flex justify-center py-2 px-4 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'جارٍ الإنشاء...' : 'إنشاء حساب'}
</button>
</form>
<p className="text-center text-sm text-gray-600">
لديك حساب بالفعل؟{' '}
<a href="/login" className="text-blue-600 hover:underline">
تسجيل الدخول
</a>
</p>
</div>
</div>
)
}الخطوة 8: بناء لوحة التحكم مع Server Components
تقوم لوحة التحكم بتحميل المهام من جانب الخادم لعرض أولي سريع، ثم تشترك في التغييرات الفورية من جانب العميل.
تخطيط لوحة التحكم
// src/app/dashboard/layout.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) redirect('/login')
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
<h1 className="text-xl font-bold text-gray-900">لوحة المهام</h1>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600">{user.email}</span>
<form action="/auth/signout" method="post">
<button
type="submit"
className="text-sm text-red-600 hover:underline"
>
تسجيل الخروج
</button>
</form>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 py-8">{children}</main>
</div>
)
}Server Component: تحميل المهام الأولية
// src/app/dashboard/page.tsx
import { createClient } from '@/lib/supabase/server'
import { TaskBoard } from './task-board'
export default async function DashboardPage() {
const supabase = await createClient()
const { data: tasks, error } = await supabase
.from('tasks')
.select('*')
.order('created_at', { ascending: false })
if (error) {
return <div className="text-red-600">فشل تحميل المهام: {error.message}</div>
}
return <TaskBoard initialTasks={tasks ?? []} />
}الخطوة 9: إنشاء مكوّن لوحة المهام الفوري
هذا هو قلب التطبيق — مكوّن عميل يعرض أعمدة Kanban ويشترك في التغييرات الفورية:
// src/app/dashboard/task-board.tsx
'use client'
import { createClient } from '@/lib/supabase/client'
import { useEffect, useState } from 'react'
type Task = {
id: string
title: string
description: string | null
status: 'todo' | 'in_progress' | 'done'
created_at: string
updated_at: string
user_id: string
}
const STATUS_COLUMNS = [
{ key: 'todo', label: 'للتنفيذ', color: 'bg-yellow-100 border-yellow-300' },
{ key: 'in_progress', label: 'قيد التنفيذ', color: 'bg-blue-100 border-blue-300' },
{ key: 'done', label: 'مكتمل', color: 'bg-green-100 border-green-300' },
] as const
export function TaskBoard({ initialTasks }: { initialTasks: Task[] }) {
const [tasks, setTasks] = useState<Task[]>(initialTasks)
const [newTitle, setNewTitle] = useState('')
const supabase = createClient()
// الاشتراك في التغييرات الفورية
useEffect(() => {
const channel = supabase
.channel('tasks:updates', { config: { private: true } })
.on('broadcast', { event: 'INSERT' }, (payload) => {
const newTask = payload.payload.record as Task
setTasks((prev) => [newTask, ...prev])
})
.on('broadcast', { event: 'UPDATE' }, (payload) => {
const updated = payload.payload.record as Task
setTasks((prev) =>
prev.map((t) => (t.id === updated.id ? updated : t))
)
})
.on('broadcast', { event: 'DELETE' }, (payload) => {
const deleted = payload.payload.old_record as Task
setTasks((prev) => prev.filter((t) => t.id !== deleted.id))
})
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [supabase])
const addTask = async (e: React.FormEvent) => {
e.preventDefault()
if (!newTitle.trim()) return
const { data: { user } } = await supabase.auth.getUser()
if (!user) return
const { error } = await supabase.from('tasks').insert({
title: newTitle.trim(),
user_id: user.id,
status: 'todo',
})
if (!error) setNewTitle('')
}
const updateStatus = async (taskId: string, status: Task['status']) => {
await supabase.from('tasks').update({ status }).eq('id', taskId)
}
const deleteTask = async (taskId: string) => {
await supabase.from('tasks').delete().eq('id', taskId)
}
return (
<div className="space-y-6">
{/* نموذج إضافة مهمة */}
<form onSubmit={addTask} className="flex gap-3">
<input
type="text"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="أضف مهمة جديدة..."
className="flex-1 rounded-lg border border-gray-300 px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium"
>
إضافة
</button>
</form>
{/* أعمدة Kanban */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{STATUS_COLUMNS.map((col) => (
<div key={col.key} className={`rounded-xl border-2 p-4 ${col.color}`}>
<h3 className="font-semibold text-lg mb-4">
{col.label}
<span className="ml-2 text-sm font-normal text-gray-500">
({tasks.filter((t) => t.status === col.key).length})
</span>
</h3>
<div className="space-y-3">
{tasks
.filter((t) => t.status === col.key)
.map((task) => (
<div
key={task.id}
className="bg-white rounded-lg p-4 shadow-sm border"
>
<h4 className="font-medium text-gray-900">{task.title}</h4>
{task.description && (
<p className="text-sm text-gray-500 mt-1">
{task.description}
</p>
)}
<div className="mt-3 flex gap-2 flex-wrap">
{col.key !== 'todo' && (
<button
onClick={() =>
updateStatus(
task.id,
col.key === 'done' ? 'in_progress' : 'todo'
)
}
className="text-xs px-2 py-1 bg-gray-100 rounded hover:bg-gray-200"
>
← رجوع
</button>
)}
{col.key !== 'done' && (
<button
onClick={() =>
updateStatus(
task.id,
col.key === 'todo' ? 'in_progress' : 'done'
)
}
className="text-xs px-2 py-1 bg-blue-100 rounded hover:bg-blue-200 text-blue-700"
>
تقدّم →
</button>
)}
<button
onClick={() => deleteTask(task.id)}
className="text-xs px-2 py-1 bg-red-100 rounded hover:bg-red-200 text-red-700"
>
حذف
</button>
</div>
</div>
))}
{tasks.filter((t) => t.status === col.key).length === 0 && (
<p className="text-sm text-gray-400 text-center py-4">
لا توجد مهام
</p>
)}
</div>
</div>
))}
</div>
</div>
)
}الخطوة 10: إضافة مسار تسجيل الخروج
// src/app/auth/signout/route.ts
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export async function POST() {
const supabase = await createClient()
await supabase.auth.signOut()
redirect('/login')
}الخطوة 11: معالج callback المصادقة
يرسل Supabase المستخدمين إلى عنوان callback بعد تأكيد البريد الإلكتروني. أضف هذا المسار:
// src/app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
return NextResponse.redirect(`${origin}/dashboard`)
}
}
return NextResponse.redirect(`${origin}/login?error=auth`)
}اختبار التطبيق
- شغّل خادم التطوير:
npm run dev- سجّل على
http://localhost:3000/signupببريد إلكتروني تجريبي - أنشئ مهامًا وانقلها بين الأعمدة
- افتح تبويبًا ثانيًا — ستظهر التغييرات فوريًا
- تحقق من RLS — سجّل الدخول بحساب مختلف للتأكد من عزل المهام
استكشاف الأخطاء وإصلاحها
المشاكل الشائعة وحلولها
"relation 'tasks' does not exist" — تأكد من تنفيذ SQL الخطوة 2 على مشروع Supabase الخاص بك.
الكوكيز لا تُحفظ — تحقق من أن الـ middleware مُهيّأ بنمط matcher الصحيح وأن @supabase/ssr مثبّت.
الأحداث الفورية لا تصل — تحقق من إنشاء trigger البث في الخطوة 3. راجع لوحة تحكم Supabase تحت Realtime > Inspector لمعرفة ما إذا كانت الأحداث تتدفق.
أخطاء "Unauthorized" — Row Level Security مفعّل. تأكد من أن المستخدم مصادق عليه قبل إجراء الاستعلامات. تحقق من أن سياسات RLS تربط auth.uid() بالعمود الصحيح.
فقدان الجلسة عند تحديث الصفحة — يجب أن يتعامل الـ middleware مع تحديث الرموز تلقائيًا. إذا فُقدت الجلسات، تحقق من أن setAll في عميل الخادم يكتب الكوكيز بشكل صحيح.
هيكل المشروع
src/
├── app/
│ ├── auth/
│ │ ├── callback/route.ts # Callback المصادقة
│ │ └── signout/route.ts # معالج تسجيل الخروج
│ ├── dashboard/
│ │ ├── layout.tsx # حماية المصادقة + العنوان
│ │ ├── page.tsx # Server Component (تحميل البيانات)
│ │ └── task-board.tsx # Client Component (فوري)
│ ├── login/page.tsx # نموذج تسجيل الدخول
│ └── signup/page.tsx # نموذج التسجيل
├── lib/
│ └── supabase/
│ ├── client.ts # عميل المتصفح
│ └── server.ts # عميل الخادم
└── middleware.ts # تحديث الجلسة + حراسة المسارات
الخطوات التالية
الآن بعد أن لديك لوحة مهام فورية تعمل، إليك طرق لتوسيعها:
- إضافة مزودي OAuth — فعّل تسجيل الدخول عبر Google أو GitHub أو Discord من إعدادات Supabase Auth
- تنفيذ السحب والإفلات — استخدم
@dnd-kit/coreلتفاعلات Kanban سلسة - إضافة Supabase Storage — اسمح للمستخدمين بإرفاق ملفات بالمهام
- النشر على Vercel — هيّئ متغيرات البيئة وانشر باستخدام
vercel deploy - إضافة الحضور — اعرض المستخدمين المتصلين حاليًا باستخدام Supabase Realtime Presence
الخلاصة
لقد بنيت لوحة مهام تعاونية full-stack فورية باستخدام Supabase و Next.js 15. الأنماط الأساسية التي تعلمتها هي:
- إعداد عميل مزدوج — عملاء Supabase منفصلين للمتصفح والخادم مع App Router
- إدارة الجلسات عبر Middleware — تحديث تلقائي للرموز مع كل طلب
- Row Level Security — التحكم في الوصول على مستوى قاعدة البيانات دون كتابة كود backend
- Triggers البث — النهج القابل للتوسع لإشعارات التغييرات الفورية
- Server Components + Client Components — تحميل البيانات من الخادم مع تفاعلية العميل
تنطبق هذه الأنماط على أي مشروع Supabase + Next.js، سواء كنت تبني تطبيق محادثة أو لوحة تحكم أو منتج SaaS. يتعامل Supabase مع البنية التحتية الثقيلة لتتمكن من التركيز على منطق تطبيقك.