يسمح Discourse للمستخدمين برفع صور في المنشورات (وأماكن أخرى). في بعض الأحيان تكون هذه الصور كبيرة جدًا لعرضها، لذا يقوم Discourse بإنشاء نسخة مصغرة من الصورة وإضافتها إلى المنشور. عند النقر على هذه الصورة المصغرة، ستظهر طبقة تراكب أنيقة تحتوي على الصورة بالحجم الكامل - يُشار إلى هذا عادةً باسم “عرض الصور بتأثير Lightbox”.
يستخدم Discourse حاليًا مكتبة تسمى Magnific Popup للتعامل مع سلوك عرض الصور. هذا الموضوع يتناول تحديث عرض الصور في Discourse من خلال الانتقال من Magnific Popup إلى شيء يتماشى أكثر مع توقعات المستخدمين والمطورين اليوم.
السبب
مكتبة Magnific مكتبة رائعة، وعدد النجوم على صفحة المشروع يشير بشكل كبير إلى عدد المشكلات التي حلها للمستخدمين والمطورين على مر السنين.
كما كانت متقدمة على وقتها من حيث سهولة الاستخدام والعروض الميزات. يصبح هذا واضحًا عندما تفكر في أن جميع الوظائف التي تراها فيها اليوم كانت موجودة بالفعل في الإصدار 1.0، الذي صدر في عام 2014.
إذن، أين يتركنا ذلك؟ سأحافظ على الإيجاز. تم إنشاء Magnific Popup لعالم مختلف تمامًا حيث كان توافق المتصفحات متخلفًا عن مكانه اليوم. هذا يعني أربع نقاط.
- تعتمد على jQuery
- تحتوي على كود مخصص للمتصفحات القديمة
- تغير الجهاز المتوسط المستخدم للوصول إلى الويب كثيرًا منذ ذلك الحين
- تم تصميمه مع وضع شيء آخر غير التطبيقات ذات الصفحة الواحدة مثل Discourse كأولوية - وهي الصفحات الثابتة. هذا يثير مخاوف أداء محددة للتطبيقات ذات الصفحة الواحدة، سنتناولها لاحقًا.
نظرًا للحالة الحالية لمعايير الويب وكيفية القيام بكل أنواع السحر باستخدام JavaScript الخام، فمن المفهوم أن المشاريع الكبيرة مثل Discourse تتجه نحو الابتعاد عن الاعتماد على jQuery. لاحظ أن المناقشات حول jQuery خارجة عن الموضوع هنا، وهذا مجرد سياق في الغالب.
إذن، هذا هو السبب… أقل اعتمادًا على jQuery، وكود أقل للمتصفحات القديمة، وأداء أفضل بشكل عام، ودعم أفضل للجهاز المتوسط - الذي اختلف كثيرًا الآن.
الكيفية
لا يوجد نقص في الحلول المعروضة، وهناك نقاش حول عدم إعادة اختراع العجلة، لكن… لن نذهب إلى هناك ولن نطيل.
ربما أقصر طريقة لمعالجة هذه النقطة العالقة هي القول… بغض النظر عن مدى ملاءمة شيء جاهز… لن يناسب بنفس دقة شيء مصمم خصيصًا، ولن نزيد من ذلك.
هناك العديد من الأشياء التي يجب مراعاتها هنا، سواء كان ذلك فائضًا في الميزات مع المكتبات التي تغطي حالات استخدام متعددة مثل الصور وiframe والفيديو وما إلى ذلك، أو عدم الوضوح حول الترخيص.
معرفة حالات الاستخدام المحددة التي يحتاج Discourse لدعمها يجعل من الممكن تجنب الكود غير الضروري، مما يسمح بإضافة ميزات مستهدفة إضافية دون الحصول على الكثير من الحمل الزائد.
إذن، لدينا الآن خط أساس جيد جدًا. المتطلبات هي:
- لا jQuery
- التركيز فقط على الصور حاليًا
- دعم تحسينات جودة الحياة مثل السحب على الهاتف المحمول
- دمجها مع Discourse للسماح بتخصيص/تحسينات إضافية باستخدام أنظمة السمات/الإضافات الحالية.
ماذا
Discourse هو متجر Ember، لذا يجب أن يكون عرض الصور الجديد مكونًا في Ember للتعامل مع الترميز والبيانات. أيضًا، نظرًا لوجود توقع بأنه يجب أن يكون بإمكانك إعداد عرض الصور في أي مكان في التطبيق، فإن وجود خدمة عرض الصور منطقي جدًا. هذا سيمكن المطورين من:
- حقن الخدمة في أي مكون والربط بين طرق إعداد/تنظيف الخدمة مع دورة حياة المكون
- البحث عن الخدمة في أي مكان في التطبيق واستدعاء طرق إعداد/تنظيفها
بالإضافة إلى ذلك، كتحسين لجودة الحياة، يمكننا إضافة بعض التجريد وإنشاء دوال مساعدة يمكن استخدامها في السمات. المزيد عن ذلك لاحقًا.
بما أن أحد الأهداف هو السماح للسمات/الإضافات بتوسيع الوظائف، فإن خدمة عرض الصور ستبلغ عن تغييرات الحالة عبر appEvents. ستنشئ الأحداث التالية:
lightbox:openedlightbox:item-will-changelightbox:item-did-changelightbox:closed
سيتم تشغيل كل حدث مع نوع البيانات الذي يتوقعه المطورون عادةً. على سبيل المثال، سيحتوي حدث lightbox:opened على قائمة بجميع العناصر والعنصر الحالي الذي فتح عنده عرض الصور. المزيد عن ذلك لاحقًا.
بعد الانتهاء من ذلك، دعنا نتقدم.
خلال الأسابيع القليلة الماضية، كنت أعمل على مسودة PR تقدم Discourse Lightbox.
في لمحة سريعة، قد يبدو كبيرًا بعض الشيء، لذا دعنا نقسمه.
اختبار شيء يعتمد على الكثير من تفاعل المستخدم أمر شائك، ولهذا السبب تتضمن مجموعة اختبارات عرض الصور 63 اختبارًا مع 291 ادعاءً.
بعد الانتهاء من حجم الاختبارات، دعنا نقارن الحجم بسرعة مع Magnific Popup (كلاهما غير مصغر)
| Discourse Lightbox LOC | Magnific Popup LOC | الفرق | |
|---|---|---|---|
| JavaScript | 1197 | 1860 | (35%) |
| CSS | 813 | 351 | 131% |
| القوالب | 401 | 0 | - |
| المجموع | 2411 | 2211 | 9.1% |
إذن، يحتوي Discourse Lightbox على كود أكثر بنسبة 9% تقريبًا من Magnific. بالطبع، هذا جزء فقط من القصة لسببين.
- لم نأخذ في الاعتبار jQuery، الذي يعتمد عليه Magnific. jQuery 3.6 يحتوي تقريبًا على 11 ألف سطر من الكود.
- يضيف Discourse Lightbox ميزات أكثر مقارنة بـ Magnific.
دعنا نتناول هذه الميزات،
أي تقطع تراه في الفيديوهات أدناه يتعلق بالتسجيل، وليس ما سيعيشه المستخدمون. ستعمل جميع الرسوم المتحركة/التحولات بسرعة ثابتة 60 إطارًا في الثانية.
إذن، دون أي تأخير آخر،
التخطيط الأساسي
للمقارنة، إليك نفس الشيء في تنفيذ Magnific الحالي
بعض النقاط حول الفيديوهات أعلاه
-
سيستخدم عرض الصور الجديد الصورة كخلفية بدلاً من الخلفية الداكنة شبه الشفافة العامة. الصورة، بحكم التعريف، ستكمل نفسها. لاحظ أن هذا لا يضيف أي حمل شبكي. الصورة المستخدمة في الخلفية هي الصورة من جسم المنشور، مما يعني أنها ستكون مخزنة مؤقتًا بالفعل عندما يفتح المستخدمون عرض الصور.
-
يفضل Discourse Lightbox واجهة مستخدم ثابتة. بدلاً من إرفاق زر الإغلاق والعنوان وبيانات الصورة بالصورة، لديها مساحات مخصصة خاصة بها. هذا سيساعد في تقليل القفز عند التنقل بين صور بأبعاد مختلفة.
-
يمكن التنقل بين الصور باستخدام الأسهم في عرض الصور أو عبر اختصارات لوحة المفاتيح. → أو ↓ للتالي و ← أو ↑ للسابق. في الإعدادات اللغوية RTL، يتم عكس الاختصارات وفقًا لذلك.
التخطيط على الهاتف المحمول مشابه جدًا، باستثناء أن الأسهم غير معروضة لأنها تضيف إيماءات السحب للتنقل.
اسحب لليسار على الهاتف المحمول للتالي، واسحب لليمين للسابق. في الإعدادات اللغوية RTL، يتم عكس إيماءات السحب.
التكبير
يحتوي عرض الصور الجديد على زر تكبير مخصص يظهر عندما تكون الصورة التي تشاهدها أكبر من أبعاد نافذة العرض. عند النقر على زر التكبير، سيتم تكبير الصورة، وعند النقر على الزر مرة أخرى سيتم تصغير الصورة. بالإضافة إلى ذلك، هناك الآن اختصار لوحة مفاتيح للتكبير/التصغير Z. أخيرًا، إذا كانت الصورة قابلة للتكبير، فإن النقر عليها سيكون له نفس التأثير.
إليك فيديو يوضح جميع الطرق الثلاث
لاحظ أنه بينما لم يتم توضيح ذلك في الفيديو أعلاه، فإن المؤشر، عند تحريكه فوق صورة قابلة للتكبير، سيتغير ليعكس ذلك. سيتغير أيضًا إلى أيقونة zoom-out عند تحريكه فوق صورة مكبرة بالفعل.
يعمل التكبير بشكل مختلف في عرض الصور الجديد. على سطح المكتب، سيتبع القسم المكبر من الصورة المؤشر.
لا يوجد تحريك على الهاتف المحمول؛ يستخدم التمرير باللمس العادي للتحرك داخل صورة مكبرة.
التدوير
يضيف عرض الصور الجديد زر تدوير مخصص لتدوير الصورة بمقدار 90 درجة. التدوير لديه أيضًا اختصار لوحة مفاتيح. مفتاح R. إليك كيف يبدو ذلك
يمكن دمج التدوير والتكبير.
وضع ملء الشاشة
يضيف عرض الصور الجديد زر ملء الشاشة. الزر سيجعل نافذة المتصفح تدخل وضع ملء الشاشة. اختصار لوحة المفاتيح هو M
سيحافظ على تتبع حالته ويعود إلى الوضع العادي عند إيقاف تشغيل ملء الشاشة أو عند إغلاق عرض الصور.
التنزيل
يضيف عرض الصور الحالي رابط “تنزيل” أسفل الصورة. يفعل عرض الصور الجديد نفس الشيء لكنه يغير ذلك إلى أيقونة ويضيفها إلى تذييل عرض الصور بدلاً من ذلك. لا يزال يحترم نفس الأذونات. إذا…
prevent_anons_from_downloading_images
مفعلة، والمستخدم غير مسجل الدخول، فلن يتم عرض أيقونة التنزيل.
علامة تبويب جديدة
يضيف عرض الصور الحالي رابط “الأصلية” أسفل الصورة يفتحها في علامة تبويب جديدة. يفعل عرض الصور الجديد نفس الشيء لكنه يضيفه كأيقونة في رأس عرض الصور. سيحترم أيضًا نفس الإذن المحدد لأيقونة التنزيل.
عنوان الصورة
يركز عرض الصور الجديد بشكل رئيسي على الصورة. يتم تقصير عناوين الصور إلى سطر واحد افتراضيًا لكنها تدعم التوسيع. إليك مثال على كيف يبدو ذلك
هناك اختصار لوحة مفاتيح لتوسيع/طي العنوان T، ولن يتم عرض العنوان عند تكبير/تدوير الصورة.
شريط العرض المتتابع
ميزة جديدة متاحة هي عرض جميع الصور في المعرض في شريط عرض متتابع. سيعتمد التخطيط على شاشة الجهاز؛ يمكن أن يكون أفقيًا أو عموديًا، وإليك كيف يبدو ذلك.
هناك اختصار لوحة مفاتيح لتبديل شريط العرض المتتابع. A
وهكذا يبدو على الهاتف المحمول
السحب لأسفل على الهاتف المحمول سيقوم بتبديل شريط العرض المتتابع تشغيلًا/إيقافًا.
الإغلاق
ما زال Esc يعمل كما قبل على سطح المكتب لإغلاق عرض الصور. هناك الآن إيماءة سحب إضافية على الهاتف المحمول، ويمكنك السحب لأعلى لإغلاق عرض الصور.
إمكانية الوصول
بeyond الأساسيات مثل تسميات الأزرار، يضيف عرض الصور الجديد عنصر إعلان لقارئ الشاشة خارج الشاشة. عند التنقل إلى صورة داخل عرض الصور، سيقوم بقراءة الفهرس والعنوان بناءً على التنسيق التالي.
image %{current} of %{total}: %{title}
إليك مثال قصير على ذلك
يزيل عرض الصور الجديد أيضًا جميع الأزرار غير الضرورية التي لا تخدم أي غرض لقارئات الشاشة عبر aria-hidden.
تذكر أن إمكانية الوصول هي مهمة مستمرة، وهذا ليس مكتملًا بأي حال من الأحوال. هناك دائمًا شيء لتحسينه، لكنني توقفت هنا للحفاظ على البساطة للإصدار 1.
هذا يغطي جميع ميزات عرض الصور الجديد.
دعنا نتحول إلى بعض لغة المطورين.
مستمعي الأحداث
يضيف عرض الصور الحالي مستمعي أحداث النقر لكل صورة عرض صور فردية في المنشورات المطبوخة. هذا يعني أن المنشور الذي يحتوي على 20 صورة سيكون لديه 20 مستمع حدث للنقر لعرض الصور.
يستفيد عرض الصور الجديد من تفويض الأحداث ويضيف مستمع حدث واحد فقط للمنشور نفسه.
إليك عداد مستخدم مجهول في نافذة متصفح خفية لعدد مستمعي الأحداث لعرض الصور الحالي في منشور يحتوي على 20 صورة بعد فرض جمع القمامة
وهذا هو نفس المنشور مع عرض الصور الجديد
علاوة على ذلك، يضيف التنقل داخل عرض الصور حاليًا مستمعي أحداث، والتي تنتهي بأن تكون يتيمة وبالتالي تتداخل مع جمع القمامة. إليك رسم بياني لـ
- تحميل صفحة موضوع بمنشور واحد يحتوي على 20 صورة.
- فتح عرض الصور والتنقل عبر جميع الصور الـ 20 ثلاث مرات متتالية.
- إغلاق عرض الصور
- فرض جمع القمامة للمتصفح
عرض الصور الحالي:
عرض الصور الجديد:
أما مستمعي الأحداث على عروض الصور نفسها، فلا يبدو أنها يتم تنظيفها حاليًا في Magnific (تذكر، لم يتم بناؤها للتطبيقات ذات الصفحة الواحدة).
ملاحظة سريعة حول الاختبارات أعلاه. هي بدائية جدًا، والأرقام غير مقصود أن تكون “علمية”، الهدف هنا هو تحديد الاتجاه وليس الأرقام الدقيقة.
ملاحظات للمطورين
دعنا نتحدث عن إعداد وتنظيف عروض الصور مع Discourse Lightbox الجديد.
إعداد وتنظيف عروض الصور
لدى المطورين خياران.
-
حقن خدمة عرض الصور في مكون عبر
import { inject as service } from "@ember/service"; //... @service lightboxيمكنك بعد ذلك استدعاء
this.lightbox.setupLightboxes({ container: yourContainer // عقدة DOM selector: ".css-selector" // محدد سلسلة للعناصر التي تريد عرضها })ثم في أي وقت تريد التنظيف، فقط استدعاء
this.lightbox.cleanupLightboxes()هذا كل شيء.
-
إذا كنت لا تريد حقن خدمة عرض الصور، يمكنك استيراد
setupLightboxesوcleanupLightboxesهكذاimport { cleanupLightboxes, setupLightboxes, } from "discourse/lib/lightbox";الباقي هو نفسه حقن الخدمة. ستقوم هاتان الدالتان بالبحث عن الخدمة لك. لذا
setupLightboxes({ container: yourContainer // عقدة DOM selector: ".css-selector" // محدد سلسلة للعناصر التي تريد عرضها }) //.... cleanupLightboxes()
لاحظ أن كلا من الاستدعاء المباشر من الخدمة أو عبر الدوال المساعدة سيقبل أيضًا قائمة العقد للتوافق مع الإصدارات السابقة، لكن هذا غير موصى به.
ملاحظة أخيرة حول هذا هي أنه يمكنك أيضًا جعل عنصر غير صورة يعمل كمحفز لفتح عرض الصور الذي قمت بإعداده. على سبيل المثال
<div class="my-container">
<img class="my-selector" src="foo">
<img class="my-selector" src="bar">
....
<button>Open Lightbox</button>
</div>
سأفعل شيئًا مثل هذا لإعداد عروض صور أساسية على div أعلاه.
import {
cleanupLightboxes,
setupLightboxes,
} from "discourse/lib/lightbox";
//...
setupLightboxes({
container: document.querySelector(".my-container"),
selector: ".my-selector"
})
لجعل الزر يفتح عرض الصور، كل ما تحتاجه هو إضافة data-lightbox-trigger هكذا
<button data-lightbox-trigger>Open Lightbox</button>
الباقي يتم تلقائيًا.
أخيرًا، في أي وقت تريد التنظيف، استدعاء
cleanupLightboxes()
التنظيف ليس حاسمًا حقًا لأن خدمة عرض الصور ستنظف تلقائيًا عند تشغيل حدث dom:clean في التطبيق (عند انتقالات المسار)
الاستماع إلى أحداث عرض الصور
سيطلق عرض الصور الجديد أحداثًا، كما ناقشنا سابقًا. هذه الأحداث هي:
lightbox:openedlightbox:item-will-changelightbox:item-did-changelightbox:closed
lightbox:opened
سيتم تشغيل هذا الحدث عند فتح عرض الصور ويحتوي على كائنين.
items: هذا مصفوفة لجميع الصور في عرض الصور الحالي. كل منها سيكون كائنًا.currentItem: هذا هو الكائن للعنصر الحالي الذي فتح عنده عرض الصور.
يبدو كائن العنصر هكذا:
{
"fullsizeURL": "https://d11a6trkgmumsb.cloudfront.net/original/4X/2/3/c/23c5746c48803deac5c105081dba20555187c3ff.jpeg",
"smallURL": "https://d11a6trkgmumsb.cloudfront.net/optimized/4X/2/3/c/23c5746c48803deac5c105081dba20555187c3ff_2_600x750.jpeg",
"downloadURL": "/uploads/short-url/56rKTvkvmL6c2C8OFQtMo3D8sIn.jpeg?dl=1",
"title": "Close-up Photo of a Black Microphone on Stand",
"fileDetails": "1200×1500 88.9 KB",
"dominantColor": "793C6D",
"aspectRatio": "400 / 500",
"index": 0,
"cssVars": "--dominant-color: #793C6D;--aspect-ratio: 400 / 500;--small-url: url(https://d11a6trkgmumsb.cloudfront.net/optimized/4X/2/3/c/23c5746c48803deac5c105081dba20555187c3ff_2_600x750.jpeg);",
"isLoaded": true,
"hasLoadingError": false,
"width": 1200,
"height": 1500,
"canZoom": true
}
lightbox:item-will-change
يتم تشغيل هذا الحدث مباشرة قبل تغيير العنصر الحالي في عرض الصور. سيكون لديه currentItem (الذي على وشك التغيير)
lightbox:item-did-change
يتم تشغيل هذا الحدث مباشرة بعد تغيير العنصر في عرض الصور وانتهى تحميله، وسيكون لديه currentItem كحجة.
lightbox:closed
يتم تشغيل هذا الحدث مباشرة بعد إغلاق عرض الصور ولا يحتوي على أي حجج.
مع الأحداث أعلاه. يمكن لمكون سمة نظري بسهولة إضافة تحليلات لعرض الصور هكذا
api.onAppEvent('lightbox:opened', ({items, currentItem}) => {
console.log({items});
console.log({currentItem});
// your analytics code here
});
أو أفكار أخرى مماثلة.
CSS
يستخدم عرض الصور الجديد اصطلاح تسمية BEM لفئات HTML. إليك قائمة كاملة بالمحددات التي يمكنك استخدامها
html.has-lightbox {
// css لعنصر HTML عندما تكون عروض الصور مفتوحة
}
.d-lightbox {
&--is-visible {
// العنصر الرئيسي لعرض الصور
}
&__content {
// غلاف المحتوى الداخلي لعرض الصور
}
}
.d-lightbox {
&__content__header {
// رأس عرض الصور
}
}
.d-lightbox {
&__content__body {
// جسم عرض الصور (يحتوي على الصورة الرئيسية)
&__backdrop {
// الخلفية لعرض الصور
}
&__main-image {
// الصورة الرئيسية لعرض الصور
}
&__error-message {
// رسالة الخطأ لعرض الصور
}
&__previous-button,
&__next-button {
// أزرار السابق/التالي الرئيسية لعرض الصور
}
}
}
.d-lightbox {
&__content__footer {
// تذييل عرض الصور
&__main-title {
// عنوان الصورة لعرض الصور
&__item-file-details {
// تفاصيل ملف الصورة لعرض الصور مثل "1000x582 183KB"
}
}
}
}
.d-lightbox {
&__content__carousel {
// حاوية شريط العرض المتتابع لعرض الصور
&__previous-button,
&__next-button {
// أزرار السابق/التالي لشريط العرض المتتابع لعرض الصور
}
}
}
.d-lightbox {
&__content__carousel {
&__carousel-items {
// حاوية عناصر شريط العرض المتتابع لعرض الصور
&__item,
&__item--is-current {
// عنصر شريط العرض المتتابع لعرض الصور
}
&__item--is-current {
// العنصر الحالي لشريط العرض المتتابع لعرض الصور
}
}
}
}
.d-lightbox {
&--is-vertical &__content__carousel {
// أنماط شريط العرض المتتابع العمودي لعرض الصور
}
}
.d-lightbox {
&--is-horizontal &__content__carousel {
// أنماط شريط العرض المتتابع الأفقي لعرض الصور
}
}
.d-lightbox {
.btn-flat {
// أنماط لجميع أزرار عرض الصور
}
}
.d-lightbox {
&__content {
&__focus-trap,
&__screen-reader-announcer {
// فخ التركيز ومعلن قارئ الشاشة لعرض الصور. هذه خارج الشاشة
}
}
}
/* أنماط الحالة */
// شريط العرض المتتابع
.d-lightbox {
&--has-carousel {
// أنماط عرض الصور عند فتح شريط العرض المتتابع
}
}
// عنوان موسع
.d-lightbox {
&--has-expanded-title {
// أنماط عرض الصور عند توسيع العنوان
}
}
// تكبير
.d-lightbox {
&--can-zoom {
// أنماط عرض الصور عند إمكانية تكبير الصورة
}
&--is-zoomed {
// أنماط عرض الصور عند تكبير الصورة
}
}
// تدوير
.d-lightbox {
&--is-rotated {
// أنماط عرض الصور عند تدوير الصورة
}
}
// ملء الشاشة
.d-lightbox {
&--is-fullscreen {
// أنماط عرض الصور عند ملء الشاشة للصورة
}
}
الآن بعد أن غطينا ماذا، يمكننا أخيرًا الانتقال إلى…
متى
حاليًا، جاهز للمراجعة، والتي يجب أن تحدث أولاً. بعد اجتياز المراجعة، سيكون متاحًا للمواقع التي تقوم بالتحديث. يضيف PR إعداد موقع مؤقت جديد أثناء انتقالنا من Magnific Popup إلى Discourse Lightbox. اسم الإعداد هو:
enable_experimental_lightbox
إذا كان الإعداد معطلًا، فلن يكون لـ PR أي تأثير، وسيستمر كل شيء في العمل كما قبل مع Magnific Popup.
سيستبدل Discourse Lightbox Magnific Popup في المنشورات المطبوخة ورسائل الدردشة ومكونات رفع الصور عند تفعيل الإعداد.
خارطة الطريق
- مراجعة PR
- دمج PR
- نافذة ملاحظات عامة (1-2 أسبوع)
- PR لإزالة Magnific Popup من الأساس وإزالة إعداد الموقع التجريبي.
- TBD: أهداف ممتدة مثل مكون سمة يستكشف تخطيطات مختلفة (يجب أن يكون مباشرًا لأن عرض الصور الجديد يستخدم CSS Grid للتخطيط)
الشكر والتقدير
- تم رعاية هذا العمل بسخاء من قبل CDCK

- حب كبير
موجه إلى Dmytro Semenov، مبتكر Magnific Popup، لابتكار شيء كان متقدمًا على وقته بكثير. - الصور المستخدمة في العروض التوضيحية أعلاه مقدمة من Irina Iriser @pexels




