اذهب إلى المحتوى

Mustafa Suleiman

الأعضاء
  • المساهمات

    18818
  • تاريخ الانضمام

  • تاريخ آخر زيارة

  • عدد الأيام التي تصدر بها

    445

كل منشورات العضو Mustafa Suleiman

  1. عليك تعطيل خاصية once فالإعداد الافتراضي لـ once في مكتبة AOS هو true، بمعنى الأنيميشن يحدث مرة واحدة فقط عند أول ظهور للعنصر، وعند التمرير مرة أخرى لا يحدث شيء، قم بتعديل الكود للتالي: useEffect(() => { AOS.init({ duration: 1000, once: false, }); }, []); و duration: 1000 ستجعل الحركة أبطأ وسلسة أكثر.
  2. هل لديك هاتف به Telegram مُسجل دخوله بالفعل؟ سيصلك عليه رقم التأكيد، حاول استخدام إصدار الويندوز وليس الويب ثم اختيار رقم الهاتف ليصل رسالة عليه في حال ليس لديك هاتف به تيليجرام يعمل بالفعل
  3. ستحتاج إلى تحديث نماذج المشروع وتحديد العلاقات المناسبة حيث كل بطاقة مرتبطة بحدث واحد، وكل بطاقة يمكن أن تكون مرتبطة بزائر واحد وكل زائر مرتبط بحدث. ثم عليك إنشاء حدث جديد وبطاقةوحدد عدد الدعوات، ثم إنشاء زوار بعدد الدعوات، واربطهم بالحدث، ثم بطاقة لكل زائر واربطها بالحدث والزائر. بعد ذلك أنشئ رابط فريد لكل زائر وأرسله له، وعند دخول الزائر للرابط، تحقق من الحدث والبطاقة الخاصة به. وذلك حتى يتمكن المشرف من تسجيل الدخول من /admin/login للقيام بالتالي: ينشئ حدث جديد يسجل دخول للحدث ينشئ بطاقة ويحدد عدد الدعوات يعرض الدعوات وينسخ الروابط أو يرسلها بالبريد الزوار يستخدمون الروابط للتسجيل يحصلون على بطاقة PDF مع QR code عند الحضور، يتم مسح QR code لتأكيد الحضور أي عليك تحديث models.py إلى: from flask_sqlalchemy import SQLAlchemy from datetime import datetime import secrets db = SQLAlchemy() class Admin(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) email = db.Column(db.String(100), unique=True, nullable=False) password = db.Column(db.String(200), nullable=False) class EVENT(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) email = db.Column(db.String(100), nullable=False) password = db.Column(db.String(100), nullable=False) event_time = db.Column(db.DateTime, nullable=False) cards = db.relationship('CARD', backref='event', lazy=True) invitations = db.relationship('Invitation', backref='event', lazy=True) class CARD(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) image_filename = db.Column(db.String(100)) message = db.Column(db.Text) card_no = db.Column(db.Integer, nullable=False) # عدد الدعوات event_time = db.Column(db.DateTime) created_at = db.Column(db.DateTime, default=datetime.utcnow) event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False) invitations = db.relationship('Invitation', backref='card', lazy=True) class Invitation(db.Model): id = db.Column(db.Integer, primary_key=True) unique_token = db.Column(db.String(100), unique=True, nullable=False) is_used = db.Column(db.Boolean, default=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False) card_id = db.Column(db.Integer, db.ForeignKey('card.id'), nullable=False) visitor_id = db.Column(db.Integer, db.ForeignKey('visitor.id')) class Visitor(db.Model): id = db.Column(db.Integer, primary_key=True) full_name = db.Column(db.String(100), nullable=False) email = db.Column(db.String(100), nullable=False) phone = db.Column(db.String(14)) image_filename = db.Column(db.String(100)) qr_filename = db.Column(db.String(100)) qr_token = db.Column(db.String(100), unique=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) confirmed = db.Column(db.Boolean, default=False) attended = db.Column(db.Boolean, default=False) invitation = db.relationship('Invitation', backref='visitor', uselist=False) وتحديث ملف card.py لإضافة وظيفة إنشاء الروابط: from flask import ( session, Blueprint, flash, redirect, url_for, render_template, request, current_app, ) from helper import login_required_event from datetime import datetime import os import secrets from models import * bp = Blueprint("card", __name__, url_prefix="/event") @bp.route("/dashboard") @login_required_event def event_dashboard(): if "event_id" not in session: flash("يجب تسجيل الدخول أولاً", "warning") return redirect(url_for("auth.login")) event_id = session["event_id"] event = EVENT.query.get(event_id) cards = CARD.query.filter_by(event_id=event_id).all() return render_template("card/dashboard.html", event=event, cards=cards) @bp.route("/card_maker", methods=["POST", "GET"]) @login_required_event def card_maker(): if request.method == "POST": name = request.form["event_name"] message = request.form["message"] card_no = int(request.form["card_no"]) event_time = request.form["event_time"] photo = request.files["photo"] try: event_time = datetime.strptime(event_time, "%Y-%m-%d") except ValueError: flash("صيغة التاريخ غير صحيحة", "danger") return redirect(url_for("card.card_maker")) photo_filename = f"{datetime.now().timestamp()}_{photo.filename}" photo_filepath = os.path.join( current_app.config["UPLOAD_FOLDER"], photo_filename ) photo.save(photo_filepath) event_id = session["event_id"] card = CARD( name=name, image_filename=photo_filename, message=message, card_no=card_no, event_time=event_time, event_id=event_id ) db.session.add(card) db.session.commit() for i in range(card_no): invitation = Invitation( unique_token=secrets.token_urlsafe(32), event_id=event_id, card_id=card.id ) db.session.add(invitation) db.session.commit() flash(f"تم إنشاء البطاقة و {card_no} دعوة بنجاح!", "success") return redirect(url_for("card.card_show")) return render_template("card/card_maker.html") @bp.route("/card-show") @login_required_event def card_show(): event_id = session["event_id"] cards = CARD.query.filter_by(event_id=event_id).all() return render_template("card/card_show.html", cards=cards) @bp.route("/invitations/<int:card_id>") @login_required_event def show_invitations(card_id): card = CARD.query.get_or_404(card_id) if card.event_id != session["event_id"]: flash("ليس لديك صلاحية لعرض هذه الدعوات", "danger") return redirect(url_for("card.event_dashboard")) invitations = Invitation.query.filter_by(card_id=card_id).all() return render_template("card/invitations.html", card=card, invitations=invitations) @bp.route("/visitor-info") @login_required_event def visitor_info(): event_id = session["event_id"] visitors = db.session.query(Visitor).join( Invitation ).filter( Invitation.event_id == event_id, Invitation.visitor_id.isnot(None) ).all() return render_template("card/visitor_info.html", visitors=visitors) @bp.route("/qr-scanning") @login_required_event def qr_scanning(): return render_template("card/scanner.html") وتحديث ملف visitor.py لاستقبال الدعوات: from flask import ( Blueprint, redirect, url_for, render_template, request, flash, current_app, ) import os, qrcode, secrets from weasyprint import HTML from flask_mail import Mail, Message from models import * from datetime import datetime bp = Blueprint("visitor", __name__, url_prefix="/visitor") mail = Mail() @bp.route("/register/<token>", methods=["GET", "POST"]) def register(token): invitation = Invitation.query.filter_by(unique_token=token).first() if not invitation: flash("رابط الدعوة غير صالح", "danger") return redirect(url_for("index")) if invitation.is_used: flash("هذه الدعوة تم استخدامها مسبقاً", "warning") return redirect(url_for("index")) card = invitation.card event = invitation.event if request.method == "POST": full_name = request.form["full_name"] email = request.form["email"] phone = request.form["phone"] photo = request.files["photo"] visitor = Visitor( full_name=full_name, email=email, phone=phone ) if photo and photo.filename: photo_filename = f"{datetime.now().timestamp()}_{photo.filename}" photo_filepath = os.path.join( current_app.config["UPLOAD_FOLDER"], photo_filename ) photo.save(photo_filepath) visitor.image_filename = photo_filename qr_token = secrets.token_urlsafe(16) visitor.qr_token = qr_token db.session.add(visitor) db.session.commit() invitation.visitor_id = visitor.id invitation.is_used = True db.session.commit() confirm_url = url_for("visitor.confirm_attendance", token=qr_token, _external=True) qr_img = qrcode.make(confirm_url) qr_filename = f"qr_{visitor.id}.png" qr_path = os.path.join(current_app.config["UPLOAD_FOLDER"], qr_filename) qr_img.save(qr_path) visitor.qr_filename = qr_filename db.session.commit() pdf_path = generate_pdf(visitor, event, card, confirm_url) send_visitor_email(visitor, event, pdf_path) flash("تم التسجيل بنجاح! تحقق من بريدك الإلكتروني", "success") return redirect(url_for("index")) return render_template("visitor/register.html", event=event, card=card) @bp.route("/confirm_attendance/<token>") def confirm_attendance(token): visitor = Visitor.query.filter_by(qr_token=token).first() if visitor: visitor.attended = True db.session.commit() invitation = visitor.invitation event = invitation.event if invitation else None return render_template("visitor/confirmed.html", visitor=visitor, event=event) else: return "رمز غير صالح", 404 def generate_pdf(visitor, event, card, confirm_url): pdf_filename = f"invitation_{visitor.id}.pdf" pdf_path = os.path.join(current_app.config["UPLOAD_FOLDER"], pdf_filename) image_url = url_for( "static", filename=f"uploads/{visitor.image_filename}", _external=True ) if visitor.image_filename else None qr_url = url_for( "static", filename=f"uploads/{visitor.qr_filename}", _external=True ) if visitor.qr_filename else None card_image_url = url_for( "static", filename=f"uploads/{card.image_filename}", _external=True ) if card.image_filename else None html = render_template( "visitor/invitation_pdf.html", visitor=visitor, event=event, card=card, visitor_image=image_url, qr_image=qr_url, card_image=card_image_url, confirm_url=confirm_url ) base_url = request.host_url HTML(string=html, base_url=base_url).write_pdf(pdf_path) return pdf_path def send_visitor_email(visitor, event, pdf_path): msg = Message( f"دعوتك لحضور {event.name}", sender=current_app.config["MAIL_USERNAME"], recipients=[visitor.email] ) msg.body = f""" مرحباً {visitor.full_name}, يسعدنا دعوتك لحضور {event.name} في تاريخ {event.event_time.strftime('%Y-%m-%d')}. تجد بطاقة الدعوة مرفقة مع هذا البريد. مع أطيب التحيات """ with current_app.open_resource(pdf_path) as pdf: msg.attach(f"invitation_{visitor.id}.pdf", "application/pdf", pdf.read()) mail.send(msg) ثم إنشاء القوالب الجديدة templates/card/invitations.html {% extends "base.html" %} {% block title %}دعوات البطاقة{% endblock %} {% block content %} <div class="container mt-4"> <h2>دعوات البطاقة: {{ card.name }}</h2> <p>عدد الدعوات: {{ card.card_no }}</p> <div class="table-responsive"> <table class="table table-striped"> <thead> <tr> <th>#</th> <th>رابط الدعوة</th> <th>الحالة</th> <th>الزائر</th> <th>الإجراءات</th> </tr> </thead> <tbody> {% for invitation in invitations %} <tr> <td>{{ loop.index }}</td> <td> <input type="text" class="form-control form-control-sm" value="{{ url_for('visitor.register', token=invitation.unique_token, _external=True) }}" readonly> </td> <td> {% if invitation.is_used %} <span class="badge bg-success">مستخدمة</span> {% else %} <span class="badge bg-warning">غير مستخدمة</span> {% endif %} </td> <td> {% if invitation.visitor %} {{ invitation.visitor.full_name }} {% else %} - {% endif %} </td> <td> <button class="btn btn-sm btn-primary copy-btn" data-url="{{ url_for('visitor.register', token=invitation.unique_token, _external=True) }}"> نسخ الرابط </button> </td> </tr> {% endfor %} </tbody> </table> </div> <a href="{{ url_for('card.card_show') }}" class="btn btn-secondary">العودة</a> </div> <script> document.querySelectorAll('.copy-btn').forEach(btn => { btn.addEventListener('click', function() { const url = this.getAttribute('data-url'); navigator.clipboard.writeText(url).then(() => { this.textContent = 'تم النسخ!'; setTimeout(() => { this.textContent = 'نسخ الرابط'; }, 2000); }); }); }); </script> {% endblock %} و templates/visitor/register.html {% extends "base.html" %} {% block title %}التسجيل في الحدث{% endblock %} {% block content %} <div class="container mt-4"> <div class="row justify-content-center"> <div class="col-md-6"> <div class="card"> <div class="card-header bg-primary text-white"> <h4>التسجيل في {{ event.name }}</h4> </div> <div class="card-body"> <div class="text-center mb-4"> {% if card.image_filename %} <img src="{{ url_for('static', filename='uploads/' + card.image_filename) }}" alt="شعار الحدث" class="img-fluid" style="max-height: 200px;"> {% endif %} <h5 class="mt-3">{{ card.name }}</h5> <p>{{ card.message }}</p> <p><strong>التاريخ:</strong> {{ event.event_time.strftime('%Y-%m-%d') }}</p> </div> <form method="post" enctype="multipart/form-data"> <div class="mb-3"> <label for="full_name" class="form-label">الاسم الكامل</label> <input type="text" class="form-control" id="full_name" name="full_name" required> </div> <div class="mb-3"> <label for="email" class="form-label">البريد الإلكتروني</label> <input type="email" class="form-control" id="email" name="email" required> </div> <div class="mb-3"> <label for="phone" class="form-label">رقم الهاتف</label> <input type="tel" class="form-control" id="phone" name="phone" required> </div> <div class="mb-3"> <label for="photo" class="form-label">صورة شخصية (اختياري)</label> <input type="file" class="form-control" id="photo" name="photo" accept="image/*"> </div> <button type="submit" class="btn btn-primary w-100">تأكيد التسجيل</button> </form> </div> </div> </div> </div> </div> {% endblock %} و templates/visitor/invitation_pdf.html <!DOCTYPE html> <html lang="ar" dir="rtl"> <head> <meta charset="UTF-8"> <style> body { font-family: Arial, sans-serif; margin: 0; padding: 20px; text-align: center; background-color: #f5f5f5; } .invitation-card { background: white; border: 2px solid #gold; border-radius: 10px; padding: 30px; max-width: 600px; margin: 0 auto; box-shadow: 0 4px 6px rgba(0,0,0,0.1); } .header img { max-width: 150px; margin-bottom: 20px; } .event-name { font-size: 28px; color: #333; margin: 20px 0; } .message { font-size: 18px; color: #666; margin: 20px 0; line-height: 1.6; } .visitor-info { background: #f9f9f9; padding: 15px; border-radius: 5px; margin: 20px 0; text-align: right; } .qr-code { margin: 30px 0; } .qr-code img { width: 200px; height: 200px; } .instructions { text-align: right; margin-top: 30px; padding: 20px; background: #f0f0f0; border-radius: 5px; } .instructions h4 { color: #333; margin-bottom: 15px; } .instructions ol { text-align: right; padding-right: 20px; } .footer { margin-top: 30px; color: #888; font-size: 14px; } </style> </head> <body> <div class="invitation-card"> <div class="header"> {% if card_image %} <img src="{{ card_image }}" alt="شعار الحدث"> {% endif %} </div> <h1 class="event-name">{{ event.name }}</h1> <div class="message"> {{ card.message }} </div> <div class="visitor-info"> <h3>بيانات الضيف</h3> <p><strong>الاسم:</strong> {{ visitor.full_name }}</p> <p><strong>البريد الإلكتروني:</strong> {{ visitor.email }}</p> <p><strong>رقم الهاتف:</strong> {{ visitor.phone }}</p> <p><strong>تاريخ الحدث:</strong> {{ event.event_time.strftime('%Y-%m-%d %H:%M') }}</p> </div> <div class="qr-code"> <h3>رمز الدخول QR</h3> {% if qr_image %} <img src="{{ qr_image }}" alt="QR Code"> {% endif %} <p>يُرجى إبراز هذا الرمز عند الدخول</p> </div> <div class="instructions"> <h4>تعليمات مهمة:</h4> <ol> <li>يُرجى الحضور قبل الموعد بـ 15 دقيقة</li> <li>هذه الدعوة شخصية ولا يمكن تحويلها</li> <li>يُرجى إحضار هذه البطاقة أو إظهارها على الهاتف</li> <li>الدعوة صالحة لشخص واحد فقط</li> </ol> </div> <div class="footer"> <p>نتطلع لرؤيتكم في الحدث</p> <p>{{ event.name }} &copy; 2025</p> </div> </div> </body> </html> بعد ذلك تحديث templates/card/card_show.html {% extends "base.html" %} {% block title %}عرض البطاقات{% endblock %} {% block content %} <div class="container mt-4"> <h2>البطاقات المُنشأة</h2> <div class="row"> {% for card in cards %} <div class="col-md-4 mb-4"> <div class="card"> {% if card.image_filename %} <img src="{{ url_for('static', filename='uploads/' + card.image_filename) }}" class="card-img-top" alt="صورة البطاقة" style="height: 200px; object-fit: cover;"> {% endif %} <div class="card-body"> <h5 class="card-title">{{ card.name }}</h5> <p class="card-text">{{ card.message[:100] }}...</p> وتحديث templates/card/card_show.html {% extends "base.html" %} {% block title %}عرض البطاقات{% endblock %} {% block content %} <div class="container mt-4"> <h2>البطاقات المُنشأة</h2> <div class="row"> {% for card in cards %} <div class="col-md-4 mb-4"> <div class="card"> {% if card.image_filename %} <img src="{{ url_for('static', filename='uploads/' + card.image_filename) }}" class="card-img-top" alt="صورة البطاقة" style="height: 200px; object-fit: cover;"> {% endif %} <div class="card-body"> <h5 class="card-title">{{ card.name }}</h5> <p class="card-text">{{ card.message[:100] }}...</p> <ul class="list-unstyled"> <li><strong>عدد الدعوات:</strong> {{ card.card_no }}</li> <li><strong>التاريخ:</strong> {{ card.event_time.strftime('%Y-%m-%d') }}</li> <li><strong>الدعوات المستخدمة:</strong> {{ card.invitations|selectattr('is_used')|list|length }} من {{ card.card_no }} </li> </ul> <a href="{{ url_for('card.show_invitations', card_id=card.id) }}" class="btn btn-primary">عرض الدعوات</a> </div> </div> </div> {% endfor %} </div> <a href="{{ url_for('card.event_dashboard') }}" class="btn btn-secondary">العودة للوحة التحكم</a> </div> {% endblock %} كذلك تحديث templates/card/visitor_info.html {% extends "base.html" %} {% block title %}معلومات الزوار{% endblock %} {% block content %} <div class="container mt-4"> <h2>الزوار المسجلين</h2> <div class="table-responsive"> <table class="table table-striped"> <thead> <tr> <th>الصورة</th> <th>الاسم</th> <th>البريد الإلكتروني</th> <th>الهاتف</th> <th>تاريخ التسجيل</th> <th>الحالة</th> <th>الحضور</th> </tr> </thead> <tbody> {% for visitor in visitors %} <tr> <td> {% if visitor.image_filename %} <img src="{{ url_for('static', filename='uploads/' + visitor.image_filename) }}" alt="صورة الزائر" style="width: 50px; height: 50px; object-fit: cover; border-radius: 50%;"> {% else %} <span class="text-muted">لا توجد صورة</span> {% endif %} </td> <td>{{ visitor.full_name }}</td> <td>{{ visitor.email }}</td> <td>{{ visitor.phone }}</td> <td>{{ visitor.created_at.strftime('%Y-%m-%d %H:%M') }}</td> <td> {% if visitor.confirmed %} <span class="badge bg-success">مؤكد</span> {% else %} <span class="badge bg-warning">غير مؤكد</span> {% endif %} </td> <td> {% if visitor.attended %} <span class="badge bg-success">حضر</span> {% else %} <span class="badge bg-secondary">لم يحضر</span> {% endif %} </td> </tr> {% endfor %} </tbody> </table> </div> {% if not visitors %} <p class="text-center text-muted mt-4">لا يوجد زوار مسجلين حتى الآن</p> {% endif %} <a href="{{ url_for('card.event_dashboard') }}" class="btn btn-secondary mt-3">العودة للوحة التحكم</a> </div> {% endblock %} وتحديث templates/card/dashboard.html {% extends "base.html" %} {% block title %}لوحة التحكم{% endblock %} {% block content %} <div class="container mt-4"> <h1>لوحة تحكم الحدث: {{ event.name }}</h1> <div class="row mt-4"> <div class="col-md-3"> <div class="card text-center"> <div class="card-body"> <h5 class="card-title">البطاقات</h5> <p class="card-text display-4">{{ cards|length }}</p> <a href="{{ url_for('card.card_maker') }}" class="btn btn-primary">إنشاء بطاقة</a> </div> </div> </div> <div class="col-md-3"> <div class="card text-center"> <div class="card-body"> <h5 class="card-title">إجمالي الدعوات</h5> <p class="card-text display-4">{{ cards|sum(attribute='card_no') }}</p> <a href="{{ url_for('card.card_show') }}" class="btn btn-info">عرض البطاقات</a> </div> </div> </div> <div class="col-md-3"> <div class="card text-center"> <div class="card-body"> <h5 class="card-title">الزوار المسجلين</h5> <p class="card-text display-4">{{ event.visitors|length }}</p> <a href="{{ url_for('card.visitor_info') }}" class="btn btn-success">معلومات الزوار</a> </div> </div> </div> <div class="col-md-3"> <div class="card text-center"> <div class="card-body"> <h5 class="card-title">ماسح QR</h5> <i class="fas fa-qrcode display-4"></i> <br><br> <a href="{{ url_for('card.qr_scanning') }}" class="btn btn-warning">فتح الماسح</a> </div> </div> </div> </div> <div class="mt-4"> <h3>معلومات الحدث</h3> <ul class="list-group"> <li class="list-group-item"><strong>اسم الحدث:</strong> {{ event.name }}</li> <li class="list-group-item"><strong>البريد الإلكتروني:</strong> {{ event.email }}</li> <li class="list-group-item"><strong>تاريخ الحدث:</strong> {{ event.event_time.strftime('%Y-%m-%d') }}</li> </ul> </div> <div class="mt-4"> <a href="{{ url_for('auth.logout') }}" class="btn btn-danger">تسجيل الخروج</a> </div> </div> {% endblock %} وستحتاج إلى تحديث helper.py لإضافة دالة مساعدة from models import * from flask import redirect, url_for, g, current_app, session import functools, os def login_required_event(func): @functools.wraps(func) def decorator_func(*args, **kwargs): if 'event_id' not in session: return redirect(url_for("auth.login")) # تحميل بيانات الحدث g.event = EVENT.query.get(session['event_id']) if g.event is None: session.clear() return redirect(url_for("auth.login")) return func(*args, **kwargs) return decorator_func def login_required_admin(view): @functools.wraps(view) def wrapped_view(**kwargs): if g.admin is None: return redirect(url_for("admin_login")) return view(**kwargs) return wrapped_view وإضافة مسار API لإرسال الدعوات في card.py @bp.route("/send_invitations/<int:card_id>", methods=["POST"]) @login_required_event def send_invitations(card_id): card = CARD.query.get_or_404(card_id) if card.event_id != session["event_id"]: return {"error": "غير مصرح"}, 403 emails = request.json.get("emails", []) invitations = Invitation.query.filter_by( card_id=card_id, is_used=False ).limit(len(emails)).all() if len(invitations) < len(emails): return {"error": "عدد الإيميلات أكبر من الدعوات المتاحة"}, 400 for email, invitation in zip(emails, invitations): send_invitation_email(email, invitation, card) return {"success": True, "sent": len(emails)} def send_invitation_email(email, invitation, card): event = card.event invitation_url = url_for('visitor.register', token=invitation.unique_token, _external=True) msg = Message( f"دعوة لحضور {event.name}", sender=current_app.config["MAIL_USERNAME"], recipients=[email] ) msg.html = f""" <div dir="rtl" style="font-family: Arial, sans-serif;"> <h2>دعوة خاصة</h2> <p>يسرنا دعوتكم لحضور {event.name}</p> <p><strong>التاريخ:</strong> {event.event_time.strftime('%Y-%m-%d')}</p> <p>{card.message}</p> <p> <a href="{invitation_url}" style="background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;"> التسجيل وتأكيد الحضور </a> </p> <p>نتطلع لرؤيتكم في الحدث</p> </div> """ mail.send(msg) وإنشاء صفحة لإرسال الدعوات templates/card/send_invitations.html {% extends "base.html" %} {% block title %}إرسال الدعوات{% endblock %} {% block content %} <div class="container mt-4"> <h2>إرسال دعوات: {{ card.name }}</h2> <div class="card"> <div class="card-body"> <h5>إدخال عناوين البريد الإلكتروني</h5> <p>أدخل عنوان بريد إلكتروني واحد في كل سطر</p> <form id="sendInvitationsForm"> <div class="mb-3"> <textarea class="form-control" id="emails" rows="10" placeholder="example1@email.com&#10;example2@email.com&#10;example3@email.com"></textarea> </div> <div class="alert alert-info"> <strong>ملاحظة:</strong> لديك {{ available_invitations }} دعوة غير مستخدمة </div> <button type="submit" class="btn btn-primary">إرسال الدعوات</button> <a href="{{ url_for('card.show_invitations', card_id=card.id) }}" class="btn btn-secondary">إلغاء</a> </form> </div> </div> </div> <script> document.getElementById('sendInvitationsForm').addEventListener('submit', async function(e) { e.preventDefault(); const emailsText = document.getElementById('emails').value; const emails = emailsText.split('\n').filter(email => email.trim() !== ''); if (emails.length === 0) { alert('يرجى إدخال عنوان بريد إلكتروني واحد على الأقل'); return; } try { const response = await fetch('{{ url_for("card.send_invitations", card_id=card.id) }}', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ emails: emails }) }); const data = await response.json(); if (response.ok) { alert(`تم إرسال ${data.sent} دعوة بنجاح!`); window.location.href = '{{ url_for("card.show_invitations", card_id=card.id) }}'; } else { alert(`خطأ: ${data.error}`); } } catch (error) { alert('حدث خطأ في إرسال الدعوات'); } }); </script> {% endblock %} ثم عليك حذف قاعدة البيانات القديمة event.db وإنشاء جديدة: python create_superuser.py ثم تشغيل المشروع.
  4. الفكرة ليست في مكتبة أو إطار، بل في التوقيت والفترة التي صدرت بها React، فوقتها لم يكن هناك مكتبة أو إطار يوفر أداء ومميزات وتجربة جيدة للمطورين مثلها. ففي الفترة ما بين 2010 و 2012، كانت تطبيقات الويب تزداد تعقيدًا، وواجهت فيسبوك مشكلة حقيقية في إدارة واجهتها المعقدة، خاصة مع الإشعارات والرسائل التي تحتاج إلى تحديث مستمر، والطرق التقليدية مثل jQuery تؤدي إلى ما يسمى بـ spaghetti code أو كود معقد ومتشابك يصعب تطويره. وظهرت أطر عمل مختلفة ومنها AngularJS لكنها كانت تأتي مع تعقيدات خاصة بها، كالربط ثنائي الاتجاه للبيانات والذي جعل تتبع الأخطاء صعبًا في التطبيقات الكبيرة. فقدمت React فلسفة جديدة وبسيطة وهي تدفق البيانات أحادي الاتجاه، أي البيانات تتدفق من المكون الأب إلى المكون الابن، وذلك يجعل تتبع مصدر التغييرات والأخطاء أسهل بكثير، وحلت مشكلة حقيقية للمبرمجين في ذلك الوقت. والتعامل المباشر مع DOM المتصفح بطيء، وفكرة وجود نسخة افتراضية في الذاكرة ومقارنة التغييرات وتحديث الأجزاء الضرورية فقط كانت ثورية، مما أعطى تطبيقات رياكت دفعة أداء هائلة مقارنة بمنافسيها في ذلك الوقت. ثم إصدار React Native في 2015 وتلك كانت اللحظة التي غيرت كل شيء، فقبل React Native، كان على الشركات توظيف فريقين منفصلين لتطوير تطبيقات الموبايل، أي فريق لـ iOS باستخدام Swift وObjective-C وفريق لـ Android باستخدام Java وKotlin مؤخرًا. وجاءت React Native لتوظيف نفس مفاهيم رياكت لبناء تطبيقات موبايل حقيقية Native تعمل على النظامين. بالتالي لعدّة سنوات لحين صدور Vue.js وتطور Angular.js ظلت React هي المتربعة على عرش أفضل تقنية لتطوير واجهة المستخدم، وحظيت بدعم شركة فيسبوك المطورة لها، وتكون حولها نظام بيئي Ecosystem هائل.
  5. المُلخصات الخاصة بالدروس غير متاحة للحفاظ على محتوى الدورة من النشر، لكن يوجد موسوعة حسوب وبها تفصيل أو مُلخص لكل ما تريد في بايثون: https://wiki.hsoub.com/Python ويوجد مقالات ودروس خاصة ببايثون على أكاديمية حسوب. كذلك يوجد كتاب لتعلم لغة بايثون بالأكاديمية. أيضًا تستطيع البحث على جوجل عن Python Cheatsheet وستجد مُلخصات لكن بالإنجليزية. بالنسبة للأسئلة أرجو السؤال أسفل الدرس الذي تريد أسئلة حوله أو أسئلة عن الدروس الماضية حتى ذلك الدرس وسيتم توفيرها لك.
  6. أرجو توضيح ما هي الدورة المقصودة؟
  7. بتثبيت المكتبة من خلال الأمر: npm install react-scroll ثم استيرادها في المكون الذي تريده وليكن App.js أو مكون الشريط العلوي Navbar.js، عليك باستيراد المكونات الرئيسية من المكتبة Link و Element. حيث Link هو المكون الذي ستضغط عليه لبدء التنقل مثل رابط في القائمة، بينما Element هو المكون الذي يلتف حول القسم الذي تريد الانتقال إليه أي الوجهة. import { Link, Element } from 'react-scroll'; وكمثال بسيط: import React from 'react'; import { Link, Element } from 'react-scroll'; import './App.css'; function App() { return ( <div className="App"> <nav className="navbar"> <Link to="section1" smooth={true} duration={500} className="nav-link"> القسم الأول </Link> <Link to="section2" smooth={true} duration={500} className="nav-link"> القسم الثاني </Link> <Link to="section3" smooth={true} duration={500} className="nav-link"> القسم الثالث </Link> </nav> <Element name="section1" className="section"> <h1>القسم الأول</h1> </Element> <Element name="section2" className="section bg-grey"> <h1>القسم الثاني</h1> </Element> <Element name="section3" className="section"> <h1>القسم الثالث</h1> </Element> </div> ); } export default App;
  8. قم بتجربة تسجيل الدخول من خلال البرنامج الخاص بالحاسوب ثم تسجيل الدخول على الهاتف ومن المفترض أن يصلك رقم التأكيد على حساب تيليجرام الخاص بك أو رسالة على الهاتف: https://desktop.telegram.org/
  9. قم بتشغيل المشروع من خلال الأمر: npm run dev -- --host
  10. ليس من خلال الهاتف، بل توجه للحاسوب الخاص بك وقم بتشغيل المشروع، وسيظهر لك عنوان ip في منفذ الأوامر استخدمه لتصفح المشروع من خلال الهاتف، وأبقي الخادم يعمل على الحاسوب
  11. العنوان localhost الذي تستخدمه ذلك خاص بالخادم المحلي على حاسوبك أي من الجهاز الذي يعمل عليه الخادم الخاص بالمشروع، بينما لو أردت تصفحه من الهاتف، ستجد عنوان مختلف للوصول للمشروع عبر الشبكة لديك، ابحث في منفذ الأوامر لديك عن رقم IP يبدأ بـ 192 ثم قم بكتابته كما هو في المتصفح لديك في الهاتف لعرض المشروع
  12. هل تلك عملية تسجيل دخول لحساب قديم لك أم تسجيل حساب جديد؟ وما هو الإيميل الذي استخدمته هل هو gmail؟
  13. يوجد مشروع تخرج بالفعل، وبخصوص استيعاب محتوى الدورة، فستحتاج إلى المراجعة بلا شك، فلا تتوقع استيعاب كامل المفاهيم من المرة الأولى.
  14. لتحقيق استفادة عليك دراسة الدورة بالكامل في حال كنت تدرس البرمجة للمرة الأولى، أما لو كنت تريد دراسة مسارات معينة فتستطيع دراسة 4 مسارات بحد أدنى وقد قمت بذلك بالفعل. يمكنك التقدم للإختبار والحصول على الشهادة وسيتم إختبارك في تلك المسارات فقط، أرجو مُراسلة مركز المساعدة وتوفير روابط المشاريع التي قمت بها ورفعتها على حسابك في GitHub، وسيتم الرد عليك. لكن كما أخبرتك الأفضل دراسة الدورة بالكامل لتتعلم المهارات اللازمة لتصبح مطور بايثون.
  15. أي شيء تضعه خارج <Routes> كالـ <Navbar>سيظهر في جميع الصفحات، بالتالي يجب أن تضع مكون <Background /> داخل المسار الخاص بالصفحة الرئيسية مع مكون <Hero />، كذلك عليك تعديل المسار إلى / والذي يعني الصفحة الرئيسية: import { BrowserRouter, Routes, Route } from "react-router-dom"; return ( <> <div> <BrowserRouter> <Navbar /> <Routes> <Route path="/" element={ <> <Hero heroData={heroData[heroCount]} heroCount={heroCount} playStatus={playStatus} setHeroCount={setHeroCount} setPlayStatus={setPlayStatus} /> {/* هنا*/} <Background playStatus={playStatus} heroCount={heroCount} /> </> } /> </Routes> </BrowserRouter> </div> </> );
  16. من الطبيعي أن تتحرك الأيقونات من مكانها لأنها تأتي أسفل العنوان، والنص الخاص بالعنوان يستحوذ على المساحة المناسبة للنص، بالتالي لحل المشكلة عليك اقتطاع الجزء الباقي من النص لكي لا يظهر على سطر جديد من خلال خاصية white-space كالتالي: .prname { font-weight: bold; font-size: 1.1rem; margin-bottom: 0.5rem; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } بالنسبة لتحسين التنسيقات، فإليك تصميم أفضل: body { background-color: #f0f2f5; padding: 20px; } .items-shop { margin-top: 40px; display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; } .items-shop div:not(.last) { background-color: white; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); overflow: hidden; display: flex; flex-direction: column; } .items-shop div a { display: block; height: 200px; } .items-shop div img { width: 100%; height: 100%; object-fit: cover; } .items-shop div ul { list-style-type: none; padding: 1rem; margin: 0; font-size: 1rem; display: flex; flex-direction: column; flex-grow: 1; } .prname { font-weight: bold; font-size: 1.1rem; margin-bottom: 0.5rem; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pprice a { color: #007bff; font-weight: bold; text-decoration: none; } .items-shop hr { width: 100%; height: 1px; background-color: #eee; border: 0; margin: 0.5rem 0; } .paction { margin-top: auto; padding-top: 1rem; } .bottom-item { display: flex; justify-content: space-around; list-style: none; padding: 0; margin: 0; } .bottom-item li { display: flex; justify-content: center; align-items: center; height: 40px; width: 40px; background: linear-gradient(to right, rgb(51, 67, 148), rgb(68, 55, 140)); color: white; border-radius: 50%; cursor: pointer; transition: transform 0.2s; } .bottom-item li:hover { transform: scale(1.1); } وعليك إزالة الـ <br> والـ <hr> الزائدة من HTML لأنّ التنسيق السابق يُضيف هوامش من خلال margin.
  17. المشكلة في استخدامك لـ React Router أنت بحاجة إلى إعادة دراسة الأساسيات الخاصة به سواء على اليوتيوب أو من خلال دروس كالتالي: في ملف src\App.jsx يجب حذف مكون الـ Router طالما استخدمته بالفعل في src\Components\route.jsx فلا يصح استخدامه مرتين، وكذلك هناك نسختين من المكوّن Hero يتم عرضهم في نفس الوقت، حيث نسخة يتم عرضها يدوياً داخل App.jsx وتحصل على جميع الـ props الصحيحة. ونسخة ثانية يُنشئها ملف route.jsx لأنك وضعتها كعنصر المسار “/” ولا تصلها أي props، لذلك تكون heroData = undefined عليك حذف Hero من الراوتر، وضع بدلاً منه مكون رئيسي يمرر البيانات وهو App: import { createBrowserRouter, RouterProvider } from "react-router-dom"; import App from "../App"; import About from "./About/About"; const router = createBrowserRouter([ { path: "/", element: <App /> }, { path: "/about", element: <About /> }, ]); export default function AppRoutes() { return <RouterProvider router={router} />; } ثم في ملف src\main.jsx استخدم مكون AppRoutes لعرض التطبيق: import ReactDOM from "react-dom/client"; import AppRoutes from "./Components/route"; ReactDOM.createRoot(document.getElementById("root")).render(<AppRoutes />); أرفقت لك المشروع بعد التعديل. Apple.rar
  18. لا حاجة للتعمق في دورة علوم الحاسوب، فالهدف من الدورة هو شرح أساسيات المفاهيم الخاصة بالبرمجة وعلوم الحاسوب من أجل التأسيس في مجال البرمجة، أي ستحصل على نظرة عامة للمجال ويصبح لديك وعي حقيقي به، وبعدها تستطيع اختيار المجال الذي تريد التخصص به واستكمال مسيرتك البرمجية. أي أنّ الدورة لا توفر تخصص في مجال برمجي محدد أو شرح تقنيات بعينها، وشرط الحصول على وظيفة المذكور في وصف الدورة هو حقك بالطبع، وتستطيع المناقشة حوله من خلال مركز المساعدة فهو المختص بالأمور المالية. وبعد الإنتهاء من الدورة وإجتياز الإختبار سيتم توجهيك وإرشادك لمدة 6 أشهر بعد التخرج. ستجد هنا تفصل لفائدة دورة علوم الحاسوب ليتضح لك الأمر: وبعد تحديد التخصص البرمجة تستطيع التعمق كما تريد به وبذلك جهدك. وبخصوص توافر فرص عمل والذكاء الاصطناعي، فلا تشغل بالك بمثل تلك الأمور فلا فائدة تذكر منها غير حديث لا نتيجة مرجوة منه، الأمر مؤثر فقط من حيث المتطلبات حيث أصبح عليك الآن بذل جهد أكبر في الدراسة وتجنب الدراسة السطحية السريعة. اشغل وقتك بالدراسة وستجد الفرص بإذن الله، عدا ذلك مضيعة للوقت وهدر للطاقة، وذكر نفسك دائمًا هل تعلم شيء نافع أفضل عدم التعلم مُطلقًا؟ أي الأشخاص لديه فرصة؟
  19. من حيث السهولة والبساطة، فستضع شريط التنقل مباشرة Navbar.js في المكون الرئيسي App.js لكن وضع خارج تعريف المسارات <Routes>. import React from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import Navbar from './components/Navbar'; import './App.css'; function App() { return ( <BrowserRouter> {/* ضع شريط التنقل هنا، خارج الـ Routes */} <Navbar /> <main className="main-content"> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="/contact" element={<Contact />} /> </Routes> </main> </BrowserRouter> ); } export default App; وفي ملف components/Navbar.js استخدم مكون <Link> من react-router-dom للتنقل لتجنب إعادة تحميل الصفحة بالكامل. import React from 'react'; import { Link } from 'react-router-dom'; import './Navbar.css'; const Navbar = () => { return ( <nav className="navbar"> <div className="navbar-logo"> <Link to="/">شعار الموقع</Link> </div> <ul className="navbar-links"> <li> <Link to="/">الرئيسية</Link> </li> <li> <Link to="/about">عنّا</Link> </li> <li> <Link to="/contact">اتصل بنا</Link> </li> </ul> </nav> ); }; export default Navbar; ثم إضافة التنسيقات التالية في ملف Navbar.css وما نريده هو خاصية position: fixed لجعل الشريط ثابت، وz-index: 1000 للتأكد من أنه يظهر فوق كل المحتوى الآخر في الصفحة: .navbar { position: fixed; top: 0; left: 0; width: 100%; background-color: #333; color: white; padding: 1rem; display: flex; justify-content: space-between; align-items: center; z-index: 1000; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); } .navbar-logo a { color: white; text-decoration: none; font-size: 1.5rem; font-weight: bold; } .navbar-links { list-style: none; display: flex; margin: 0; padding: 0; } .navbar-links li { margin-left: 20px; } .navbar-links a { color: white; text-decoration: none; font-size: 1rem; } .navbar-links a:hover { color: #ddd; } وسيظهر مشكلة عند استخدام position: fixed، وهي خروج شريط التنقل من تدفق الصفحة الطبيعي، لأنّ المحتوى الآخر مثل Homeأو About سيبدأ من أعلى الشاشة، وسيكون مخفيًا خلف شريط التنقل. لذا ستحتاج إلى إضافة هامش علوي margin-top للحاوية الرئيسية للمحتوى. .main-content { margin-top: 80px; padding: 20px; } ولاحظ الهامش العلوي يجب أن يساوي ارتفاع شريط التنقل، فلو ارتفاع الشريط 60px، فاجعل الهامش 60px.
  20. من حيث الفائدة العملية فلا يوجد لها فائدة ملموسة، مجرد أيقونات وأرقام للدلالة على نشاطك ومدى تفاعلك بأقسام الأكاديمية، وكذلك في التعليقات أسفل الدروس. أي مجرد تشجيع من أجل التفاعل والمشاركة، فهو أمر مُحبب للمستخدمين على أي منصة، وستجد أمر مشابه في مواقع التواصل الإجتماعي.
  21. 401 يعني وصول غير مُصرح، أرى api.openweather بالتالي ستحتاج إلى تسجيل حساب والحصول على مفتاح API للتمكن من الإتصال، التسجيل من هنا: https://home.openweathermap.org/users/sign_up ثم الحصول على المفتاح من هنا: https://home.openweathermap.org/api_keys ثم تضمينه في الطلب الخاص بك وإليك مثال: https://api.openweathermap.org/data/3.0/onecall?lat=33.44&lon=-94.04&exclude=hourly,daily&appid={API key} لاحظ عليك استبدال API key بالمفتاح الخاص بك، والأفضل وضعه في ملف env. ثم استيراده منه كمتغير بيئة
  22. ملف env. في أي مشروع هو لإضافة المتغيرات البيئية الخاصة بالمشروع، وتلك المتغيرات بها معلومات حساسة لا نريد تضمينها في الكود، مثل رابط قاعدة البيانات ومفتاح الـ API للخدمات التي نستخدمها والكلمات السرية وأي معلومات لا تريد للغير الإطلاع عليها عند تصفح الكود. ويتم إضافة اسم الملف .env إلى ملف آخر اسمه .gitignore. والذي يمنع رفع الملف الذي يحتوي على الأسرار إلى GitHub، وبذلك لا يراها أي شخص آخر. كذلك تستطيع إنشاء ملف .env مختلف لكل بيئة عمل، فكل ما ستحتاجه هو تغيير قيمة المتغيرات التي به فقط، ففي بيئة التطوير سقوم باستخدام قاعدة بيانات محلية، وفي بيئة الإنتاج ستستخدم قاعدة البيانات الحقيقية على الخادم. بالنسبة لما في الملف لديك فهو VITE_APP_ID متغير بيئي خاصة بأداة التحزيم Vite، وهو مُعرف التطبيق لتكوين إعدادات التطبيق وللتمييز بين التطبيقات المختلفة وللاتصال بالخدمات الخارجية -مثل قواعد البيانات أو APIs.
  23. تلك مشكلة شائعة وليس أنت فقط من تعاني منها، أسبابها دراسة الدورة كما لو أنها فيلم بمعنى تشاهد بشكل سلبي دونّ استيعاب ما يحدث ولماذا قمنا به، وعند التطبيق تقوم بذلك مع الشرح وليس بمفردك، ظنًا منك أنك بذلك تقوم بالتطبيق على ما تعلمته، وأنت تقوم بذلك بالفعل لكن نسبة التركيز ليست مرتفعة وكذلك نسبة ما سيتثبت لديك أقل. فعند التطبيق بمفردك تقوم بالتركيز بشكل أكبر ويظهر لك نقاط الضعف وما أنت بحاجة إلى مراجعته والوقوف عليه، يجب تقسيم الدرس إلى حصص ثم التطبيق على كل حصة. كذلك يجب الإلتزام بعدد ساعات أسبوعي وعدم الإنقطاع لعدّة أيام خاصًة في الشهور الأولى، فأنت بحاجة إلى المداومة، وأيضًا المراجعة أمر ضروري والأفضل من خلال التطبيق العملي والذي من المفترض أن تخصص له 50% من وقتك. وستجد تفصيل هنا فالأمر بحاجة إلى تفصيل:
  24. هناك أساسيات علوم الحاسوب وهناك أساسيات البرمجة، الأخيرة تعني تعلم مفاهيم لغات البرمجة بشكل عام، أي المنطق البرمجي وكيفية تنفيذه من خلال المتغيرات وأنواع البيانات، بُنى التحكم وهي الجمل الشرطية وحلقات التكرار، الدوال وهياكل البيانات الأساسية وقواعد اللغة، وكيفية استقبال المدخلات من المستخدم وعرض نتائج. بالتالي تصبح قادر على كتابة أكواد برمجية تعمل وتحل مشاكل محددة وبسيطة. وذلك ما ستتعلمه في مسار أساسيات بايثون، لذا لا مشكلة في ذلك، لو أردت التأسيس بشكل مُتعمق أكثر ستحتاج إلى دراسة علوم الحاسوب، وذلك متاح في دورة علوم الحاسوب، في حال لا تستطيع الإشتراك بها يمكنك المتابعة في الدورة لا مشكلة وبها ما تحتاج. عامًة فهم أساسيات علوم الحاسوب هو ما يميز المبرمج العادي عن مهندس البرمجيات الذي يستطيع بناء أنظمة قوية لاستيعابه العميق وليس السطحي.
  25. الأمر سيتم من خلال CSS فقط عن طريق الـ animation والتحكم في الـ keyframes كالتالي: .animated-sale-button { transition: all 0.3s ease; background: linear-gradient(to right, #f72585, #b5179e, #7209b7, #f72585); background-size: 200% auto; animation: gradient-animation 3s linear infinite; } } @keyframes gradient-animation { 0% { background-position: 0% 50%; } 100% { background-position: 100% 50%; } } بالطبع افترضت أنّ اسم الكلاس الخاص بالزر هو animated-sale-button، ولاحظ أضفت background-size: 200% auto فذلك الجزء الأهم، لجعل صورة الخلفية أي التدرج اللوني أعرض بمرتين من الزر نفسه. ثم من خلال @keyframes gradient-animation نحدد ما تفعله الحركة، وهو تغيير موضع الخلفية background-position من 0% أي البداية إلى 100% وهي النهاية على المحور الأفقي، وبما أن حجم الخلفية 200%، فتحريكها بنسبة 100% سيُظهر الجزء المخفي من التدرج اللوني، وذلك يخلق وهم الحركة.
×
×
  • أضف...