Compare commits

...

7 Commits

Author SHA1 Message Date
2925676feb
Update __init__.py 2025-05-11 23:12:52 +10:00
7379629860
Update __init__.py 2025-05-11 23:02:50 +10:00
1ccfe78d25
Add files via upload 2025-05-11 22:46:53 +10:00
d3c8f0ab8e
Add files via upload 2025-05-11 22:35:11 +10:00
28746893fb
Rename FhirPad.png to fhirpad.png 2025-05-11 22:25:20 +10:00
0705518673
Update README.md 2025-05-11 22:25:01 +10:00
ac7710d2fb
Update README.md 2025-05-11 22:24:48 +10:00
11 changed files with 425 additions and 25 deletions

164
README.md
View File

@ -1,2 +1,162 @@
# FhirPad
A Fhir App based landing page ![FHIRPAD Logo](static/fhirpad.png)
FHIRPAD - FHIR App Platform
FHIRPAD is a web application built to facilitate the discovery and sharing of FHIR-based applications, enhancing healthcare interoperability. Users can register, browse, and manage FHIR apps, with features tailored for both general users and administrators.
Table of Contents
Features
Prerequisites
Installation
Usage
Admin Features
Contributing
License
Features
App Registration and Management: Users can submit, edit, and delete their FHIR apps with details like name, description, logo, launch URL, and more.
App Gallery: Browse and filter apps by categories, OS support, FHIR compatibility, pricing, and designed-for audiences.
Multiple Image Uploads: Upload multiple images for apps to showcase features.
OAuth Authentication: Log in via Google or GitHub, or use local credentials.
Admin Controls:
Manage categories for app filtering.
Edit or delete any app.
Edit user details (username, email, admin status, password reset).
Responsive Design: Works seamlessly on desktop and mobile devices.
Dark Mode: Toggle between light and dark themes.
External Links in New Tabs: Website and Try App buttons open in new windows for better usability.
Prerequisites
Docker and Docker Compose: For containerized deployment.
Python 3.11: If running locally without Docker.
SQLite: Used as the default database (can be swapped for another SQL database).
OAuth Credentials (optional): Google and GitHub client IDs and secrets for OAuth login.
Installation
Using Docker (Recommended)
Clone the Repository:
git clone <repository-url>
cd fhirpad
Set Up Environment Variables:
Copy the .env.example to .env and fill in your OAuth credentials (if using OAuth):cp .env.example .env
Edit .env:FHIRPAD_SECRET_KEY=your-secure-secret-key
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
FLASK_RUN_PORT=5009
Build and Run with Docker Compose:
docker-compose up --build
The app will be available at http://localhost:5009.
Initialize the Database:
Seed the database with initial data, including a default admin user (username: admin, password: admin123):docker-compose exec fhirpad python seed.py
Local Installation (Without Docker)
Clone the Repository:
git clone <repository-url>
cd fhirpad
Set Up a Virtual Environment:
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
Install Dependencies:
pip install -r requirements.txt
Set Up Environment Variables:
Copy .env.example to .env and configure as above.
Initialize the Database:
export FLASK_APP=app # On Windows: set FLASK_APP=app
flask db init
flask db migrate -m "Initial schema"
flask db upgrade
python seed.py
Run the Application:
flask run --host=0.0.0.0 --port=5009
Access the app at http://localhost:5009.
Usage
Access the App:
Open http://localhost:5009 in your browser.
Log In:
Use the default admin account: username admin, password admin123 (youll be prompted to change the password on first login).
Alternatively, register a new account or log in via Google/GitHub (if OAuth is configured).
Register an App:
Navigate to Register App from the navbar.
Fill in app details (name, description, launch URL, etc.) and upload a logo or app images.
Browse Apps:
Visit the Gallery page to filter and explore apps.
Click Website or Try App to visit external links (opens in a new tab).
Admin Actions (for admin users):
Access Manage Categories, Manage Apps, and Manage Users from the navbar.
Edit or delete categories, apps, or user accounts as needed.
Admin Features
Default Admin Account: Created during seeding (username: admin, password: admin123).
Manage Categories: Add or delete categories for app filtering.
Manage Apps: View, edit, or delete any app in the system.
Manage Users: Edit user details, toggle admin status, force password changes, or reset passwords.
Contributing
Fork the repository.
Create a new branch (git checkout -b feature/your-feature).
Make your changes and commit (git commit -am 'Add your feature').
Push to the branch (git push origin feature/your-feature).
Create a pull request.
Please ensure your code follows the projects style guidelines and includes appropriate tests.
License
This project is licensed under the MIT License. See the LICENSE file for details.

View File

@ -1,19 +1,29 @@
import os import os
import hashlib import hashlib
from flask import Flask from flask import Flask, request
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager from flask_login import LoginManager
from flask_oauthlib.client import OAuth from flask_oauthlib.client import OAuth
from flask_migrate import Migrate
from config import Config from config import Config
from datetime import datetime from datetime import datetime
db = SQLAlchemy() db = SQLAlchemy()
login_manager = LoginManager() login_manager = LoginManager()
oauth = OAuth() oauth = OAuth()
migrate = Migrate()
def create_app(): def create_app():
app = Flask(__name__, instance_relative_config=True, static_folder='/app/static') app = Flask(__name__, instance_relative_config=True, static_folder='/app/static')
app.config.from_object(Config) app.config.from_object(Config)
# Remove SERVER_NAME to allow requests from www.sudo-fhir.au
app.config['PREFERRED_URL_SCHEME'] = 'https' # Ensure HTTPS URLs
# Handle X-Forwarded-Proto for HTTPS
@app.before_request
def before_request():
if request.headers.get('X-Forwarded-Proto') == 'https':
request.scheme = 'https'
try: try:
os.makedirs(app.instance_path, exist_ok=True) os.makedirs(app.instance_path, exist_ok=True)
@ -25,6 +35,7 @@ def create_app():
login_manager.init_app(app) login_manager.init_app(app)
login_manager.login_view = 'auth.login' login_manager.login_view = 'auth.login'
oauth.init_app(app) oauth.init_app(app)
migrate.init_app(app, db)
from app.models import User from app.models import User
@ -58,4 +69,4 @@ def create_app():
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
return app return app

126
app/privacy.tex Normal file
View File

@ -0,0 +1,126 @@
% Preamble setup for a comprehensive LaTeX document
\documentclass[a4paper,12pt]{article}
% Including essential packages for formatting and structure
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{lmodern}
\usepackage{geometry}
\usepackage{titlesec}
\usepackage{hyperref}
\usepackage{fancyhdr}
\usepackage{lastpage}
% Setting page geometry for proper margins
\geometry{margin=1in}
% Customizing section titles
\titleformat{\section}{\Large\bfseries}{\thesection}{1em}{}
\titleformat{\subsection}{\large\bfseries}{\thesubsection}{1em}{}
% Setting up the header and footer
\pagestyle{fancy}
\fancyhf{}
\fancyhead[L]{FHIRPAD Privacy Policy}
\fancyfoot[R]{Page \thepage\ of \pageref{LastPage}}
% Defining hyperlink style
\hypersetup{
colorlinks=true,
linkcolor=blue,
urlcolor=blue,
pdftitle={FHIRPAD Privacy Policy},
pdfauthor={FHIRPAD Team},
pdfsubject={Privacy Policy},
pdfkeywords={privacy, FHIRPAD, data protection}
}
% Starting the document
\begin{document}
% Title and date
\begin{center}
{\LARGE\textbf{FHIRPAD Privacy Policy}}\\[0.5cm]
{\normalsize Last updated: May 11, 2025}
\end{center}
% Introduction section
\section{Introduction}
Welcome to FHIRPAD, a platform dedicated to discovering and sharing FHIR-based applications to enhance healthcare interoperability. We are committed to protecting your privacy and ensuring the security of your personal information. This Privacy Policy outlines how we collect, use, disclose, and safeguard your information when you use our website and services.
% Information Collection section
\section{Information We Collect}
We may collect the following types of information:
\subsection{Personal Information}
\begin{itemize}
\item \textbf{Account Information}: When you register, we collect your username, email address, and password (hashed for security).
\item \textbf{OAuth Information}: If you log in via Google or GitHub, we collect your email address, username, and OAuth ID.
\end{itemize}
\subsection{User-Submitted Content}
\begin{itemize}
\item \textbf{App Submissions}: When you submit an app, we collect app details such as name, description, developer name, contact email, logo, launch URL, website, and app images.
\end{itemize}
\subsection{Usage Data}
\begin{itemize}
\item \textbf{Log Data}: We collect information such as your IP address, browser type, pages visited, and timestamps when you access our site.
\item \textbf{Cookies}: We use cookies to store theme preferences (light/dark mode) and session information.
\end{itemize}
% Use of Information section
\section{How We Use Your Information}
We use the collected information for the following purposes:
\begin{itemize}
\item To provide and maintain our services, including user authentication and app management.
\item To display user-submitted apps in the gallery and detail pages.
\item To communicate with you, including responding to inquiries via email (e.g., \href{mailto:support@fhirpad.com}{support@fhirpad.com}).
\item To improve our website functionality and user experience.
\item To monitor usage and ensure the security of our platform.
\end{itemize}
% Sharing of Information section
\section{Sharing of Information}
We do not sell or trade your personal information. We may share your information in the following cases:
\begin{itemize}
\item \textbf{User-Submitted Apps}: App details (e.g., name, developer, contact email) are publicly displayed on the gallery and app detail pages.
\item \textbf{Service Providers}: We may share data with third-party services (e.g., Google OAuth, GitHub OAuth) to facilitate login functionality.
\item \textbf{Legal Requirements}: We may disclose information if required by law or to protect the rights, property, or safety of FHIRPAD and its users.
\end{itemize}
% Data Security section
\section{Data Security}
We implement reasonable security measures to protect your information, including:
\begin{itemize}
\item Password hashing using Werkzeug's secure methods.
\item Secure session management with Flask-Login.
\item Use of HTTPS for data transmission (if configured in production).
\end{itemize}
However, no method of transmission over the internet or electronic storage is 100\% secure, and we cannot guarantee absolute security.
% Your Rights section
\section{Your Rights}
You have the following rights regarding your personal information:
\begin{itemize}
\item \textbf{Access and Update}: You can access and update your account information via your profile.
\item \textbf{Delete}: You can delete your apps via the "My Listings" page. To delete your account, contact \href{mailto:support@fhirpad.com}{support@fhirpad.com}.
\item \textbf{Cookies}: You can manage cookie preferences via your browser settings.
\end{itemize}
% Third-Party Links section
\section{Third-Party Links}
FHIRPAD contains links to external websites (e.g., via "Website" and "Try App" buttons). We are not responsible for the privacy practices or content of these third-party sites. These links open in a new tab/window for your convenience.
% Changes to Privacy Policy section
\section{Changes to This Privacy Policy}
We may update this Privacy Policy from time to time. The updated version will be posted on this page with the "Last updated" date. We encourage you to review this policy periodically.
% Contact Us section
\section{Contact Us}
If you have any questions about this Privacy Policy, please contact us at:
\begin{itemize}
\item Email: \href{mailto:support@fhirpad.com}{support@fhirpad.com}
\end{itemize}
\end{document}

View File

@ -40,6 +40,14 @@ def landing():
categories = Category.query.all() categories = Category.query.all()
return render_template('landing.html', featured_apps=featured_apps, categories=categories) return render_template('landing.html', featured_apps=featured_apps, categories=categories)
@gallery_bp.route('/about')
def about():
return render_template('about.html')
@gallery_bp.route('/disclaimer')
def disclaimer():
return render_template('disclaimer.html')
@gallery_bp.route('/gallery', methods=['GET', 'POST']) @gallery_bp.route('/gallery', methods=['GET', 'POST'])
def gallery(): def gallery():
form = GalleryFilterForm() form = GalleryFilterForm()

60
app/templates/about.html Normal file
View File

@ -0,0 +1,60 @@
{% extends "base.html" %}
{% block title %}About FHIRPAD{% endblock %}
{% block content %}
<div class="px-4 py-5 my-5 text-center">
<div class="mb-4">
<img class="d-block mx-auto" src="{{ url_for('static', filename='fhirpad.png') }}" alt="FHIRPAD" width="256" height="256">
</div>
<h1 class="display-5 fw-bold text-body-emphasis">About FHIRPAD</h1>
<div class="col-lg-6 mx-auto">
<p class="lead mb-2">Discover and share FHIR-based applications to enhance healthcare interoperability.</p>
<p class="text-muted">Streamline Your FHIR Workflow</p>
</div>
</div>
<div class="container">
<div class="row g-4">
<!-- App Management -->
<div class="col-md-4">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="card-title">App Management</h5>
<p class="card-text">Register, edit, and manage your FHIR applications.</p>
<div class="d-grid gap-2">
<a href="{{ url_for('gallery.register') }}" class="btn btn-primary">Register App</a>
<a href="{{ url_for('gallery.my_listings') }}" class="btn btn-outline-secondary">Manage Your Apps</a>
</div>
</div>
</div>
</div>
<!-- Admin Controls -->
<div class="col-md-4">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="card-title">Admin Controls</h5>
<p class="card-text">Manage categories, apps, and users (admin-only).</p>
<div class="d-grid gap-2">
<a href="{{ url_for('gallery.manage_categories') }}" class="btn btn-outline-secondary">Manage Categories</a>
<a href="{{ url_for('gallery.admin_apps') }}" class="btn btn-outline-secondary">Manage Apps</a>
<a href="{{ url_for('gallery.admin_users') }}" class="btn btn-outline-secondary">Manage Users</a>
</div>
</div>
</div>
</div>
<!-- User Features -->
<div class="col-md-4">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="card-title">User Features</h5>
<p class="card-text">Explore apps and personalize your experience.</p>
<div class="d-grid gap-2">
<a href="{{ url_for('gallery.gallery') }}" class="btn btn-outline-secondary">Browse Apps</a>
<a href="{{ url_for('auth.login') }}" class="btn btn-outline-secondary">Login</a>
<a href="{{ url_for('auth.register') }}" class="btn btn-outline-secondary">Register</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -15,11 +15,13 @@
</h5> </h5>
<div class="d-flex justify-content-center gap-2 mb-4"> <div class="d-flex justify-content-center gap-2 mb-4">
{% if app.website %} {% if app.website %}
<a href="{{ app.website }}" class="btn btn-primary" aria-label="Visit {{ app.name }} website">Website</a> <a href="{{ app.website }}" class="btn btn-primary" target="_blank" rel="noopener noreferrer" aria-label="Visit {{ app.name }} website">Website</a>
{% else %} {% else %}
<button class="btn btn-primary" disabled>Website</button> <button class="btn btn-primary" disabled>Website</button>
{% endif %} {% endif %}
<a href="{{ app.launch_url }}" class="btn btn-outline-primary" aria-label="Try {{ app.name }} with sandbox data">Try App</a> {% if app.launch_url %}
<a href="{{ app.launch_url }}" class="btn btn-outline-primary" target="_blank" rel="noopener noreferrer" aria-label="Try {{ app.name }} with sandbox data">Try App</a>
{% endif %}
</div> </div>
<hr class="my-4"> <hr class="my-4">
<div class="row g-4"> <div class="row g-4">
@ -62,7 +64,7 @@
<hr class="small-hr"> <hr class="small-hr">
<p> <p>
{% if app.website %} {% if app.website %}
<a href="{{ app.website }}" class="text-decoration-none">{{ app.website }}</a> <a href="{{ app.website }}" class="text-decoration-none" target="_blank" rel="noopener noreferrer">{{ app.website }}</a>
{% else %} {% else %}
Not specified. Not specified.
{% endif %} {% endif %}
@ -72,13 +74,13 @@
<section class="mb-3"> <section class="mb-3">
<h5>Security and Privacy Policy</h5> <h5>Security and Privacy Policy</h5>
<hr class="small-hr"> <hr class="small-hr">
<p><a href="{{ app.website }}/security-and-privacy" class="text-decoration-none">{{ app.website }}/security-and-privacy</a></p> <p><a href="{{ app.website }}/security-and-privacy" class="text-decoration-none" target="_blank" rel="noopener noreferrer">{{ app.website }}/security-and-privacy</a></p>
</section> </section>
{% endif %} {% endif %}
<section class="mb-3"> <section class="mb-3">
<h5>Launch URL</h5> <h5>Launch URL</h5>
<hr class="small-hr"> <hr class="small-hr">
<p><a href="{{ app.launch_url }}" class="text-decoration-none">{{ app.launch_url }}</a></p> <p><a href="{{ app.launch_url }}" class="text-decoration-none" target="_blank" rel="noopener noreferrer">{{ app.launch_url }}</a></p>
</section> </section>
<section class="mb-3"> <section class="mb-3">
<div class="row g-2"> <div class="row g-2">
@ -95,7 +97,7 @@
</div> </div>
<div class="col-6"> <div class="col-6">
<h5>FHIR Compatibility</h5> <h5>FHIR Compatibility</h5>
<hr class="small-hr"> <.ttf class="small-hr">
<p> <p>
{% if app.fhir_compatibility %} {% if app.fhir_compatibility %}
<a href="{{ url_for('gallery.gallery', fhir_support=app.fhir_compatibility_id) }}" class="text-decoration-none">{{ app.fhir_compatibility.name }}</a> <a href="{{ url_for('gallery.gallery', fhir_support=app.fhir_compatibility_id) }}" class="text-decoration-none">{{ app.fhir_compatibility.name }}</a>
@ -204,6 +206,7 @@
border-color: #4dabf7; border-color: #4dabf7;
} }
.btn-outline-primary:hover { .btn-outline-primary:hover {
THESE BUTTONS ARE CURRENTLY DISABLED ON THE DETAIL PAGE
background-color: #007bff; background-color: #007bff;
color: white; color: white;
} }

View File

@ -375,13 +375,13 @@
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div class="footer-left"> <div class="footer-left">
<a href="https://www.hl7.org/fhir/" target="_blank" rel="noreferrer" aria-label="Visit FHIR website">FHIR</a> <a href="https://www.hl7.org/fhir/" target="_blank" rel="noreferrer" aria-label="Visit FHIR website">FHIR</a>
<a href="/about" aria-label="Learn about FHIRPAD">What is FHIRPAD</a> <a href="{{ url_for('gallery.about') }}" aria-label="Learn about FHIRPAD">What is FHIRPAD</a>
<a href="mailto:support@fhirpad.com" aria-label="Contact FHIRPAD support">Contact Us</a> <a href="mailto:support@fhirpad.com" aria-label="Contact FHIRPAD support">Contact Us</a>
</div> </div>
<div class="footer-right"> <div class="footer-right">
<a href="/documents/privacy.pdf" aria-label="View Privacy Policy">Privacy</a> <a href="{{ url_for('static', filename='documents/privacy.pdf') }}" aria-label="View Privacy Policy">Privacy</a>
<a href="/documents/terms.pdf" aria-label="View Terms of Use">Terms of Use</a> <a href="/documents/terms.pdf" aria-label="View Terms of Use">Terms of Use</a>
<a href="/documents/disclaimer.pdf" aria-label="View Disclaimer">Disclaimer</a> <a href="{{ url_for('gallery.disclaimer') }}" aria-label="View Disclaimer">Disclaimer</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}Disclaimer{% endblock %}
{% block content %}
<div class="container mt-4">
<h1>Disclaimer</h1>
<p>Last updated: May 11, 2025</p>
<h2>General Information</h2>
<p>The information provided by FHIRPAD on this website is for general informational purposes only. All information on the site is provided in good faith; however, we make no representation or warranty of any kind, express or implied, regarding the accuracy, adequacy, validity, reliability, availability, or completeness of any information on the site.</p>
<h2>External Links Disclaimer</h2>
<p>The site may contain links to external websites (e.g., via "Website" and "Try App" buttons) that are not provided or maintained by or in any way affiliated with FHIRPAD. Please note that FHIRPAD does not guarantee the accuracy, relevance, timeliness, or completeness of any information on these external websites. Clicking on these links will open them in a new tab/window for your convenience.</p>
<h2>Professional Advice Disclaimer</h2>
<p>The content on FHIRPAD is not intended to be a substitute for professional medical advice, diagnosis, or treatment. Always seek the advice of your physician or other qualified health provider with any questions you may have regarding a medical condition. Never disregard professional medical advice or delay in seeking it because of something you have read on this website.</p>
<h2>Limitation of Liability</h2>
<p>Under no circumstance shall FHIRPAD have any liability to you for any loss or damage of any kind incurred as a result of the use of the site or reliance on any information provided on the site. Your use of the site and your reliance on any information on the site is solely at your own risk.</p>
<h2>User-Submitted Content</h2>
<p>FHIRPAD allows users to submit and manage FHIR-based applications. The content, accuracy, and functionality of these applications are the responsibility of the submitting users. FHIRPAD does not endorse or guarantee the quality, safety, or legality of any user-submitted apps.</p>
<p>If you have any questions about this disclaimer, please contact us at <a href="mailto:support@fhirpad.com">support@fhirpad.com</a>.</p>
</div>
{% endblock %}

View File

@ -134,10 +134,10 @@
<p class="card-text"><small class="text-muted">By {{ app.developer }}</small></p> <p class="card-text"><small class="text-muted">By {{ app.developer }}</small></p>
<div class="d-flex flex-wrap gap-2 mb-2"> <div class="d-flex flex-wrap gap-2 mb-2">
{% if app.website %} {% if app.website %}
<a href="{{ app.website }}" class="btn btn-primary btn-sm" aria-label="Visit {{ app.name }} website">Website</a> <a href="{{ app.website }}" class="btn btn-primary btn-sm" target="_blank" rel="noopener noreferrer" aria-label="Visit {{ app.name }} website">Website</a>
{% endif %} {% endif %}
{% if app.launch_url %} {% if app.launch_url %}
<a href="{{ app.launch_url }}" class="btn btn-outline-primary btn-sm" aria-label="Try {{ app.name }} with sandbox data">Try App</a> <a href="{{ app.launch_url }}" class="btn btn-outline-primary btn-sm" target="_blank" rel="noopener noreferrer" aria-label="Try {{ app.name }} with sandbox data">Try App</a>
{% endif %} {% endif %}
</div> </div>
<a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-primary mt-auto">View Details</a> <a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-primary mt-auto">View Details</a>

View File

@ -1,16 +1,23 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}FHIRPAD Home{% endblock %} {% block title %}FHIRPAD Home{% endblock %}
{% block content %} {% block content %}
<div class="container mt-4"> <div class="px-4 py-5 my-5 text-center">
<div class="text-center mb-5"> <div class="mb-4">
<img src="{{ url_for('static', filename='fhirpad.png') }}" alt="FHIRPAD" height="60" class="mb-3" onerror="this.style.display='none';"> <img class="d-block mx-auto" src="{{ url_for('static', filename='fhirpad.png') }}" alt="FHIRPAD" width="256" height="256">
<h1>Welcome to FHIRPAD</h1>
<p class="lead">Discover and share FHIR-based applications to enhance healthcare interoperability.</p>
<a href="{{ url_for('gallery.register') }}" class="btn btn-primary btn-lg">Submit Your App</a>
<a href="{{ url_for('gallery.gallery') }}" class="btn btn-outline-primary btn-lg">Browse Apps</a>
</div> </div>
<h1 class="display-5 fw-bold text-body-emphasis">Welcome to FHIRPAD</h1>
<div class="col-lg-6 mx-auto">
<p class="lead mb-2">Discover and share FHIR-based applications to enhance healthcare interoperability.</p>
<p class="text-muted">Streamline Your FHIR Workflow</p>
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
<a href="{{ url_for('gallery.register') }}" class="btn btn-primary btn-lg px-4 me-sm-3">Submit Your App</a>
<a href="{{ url_for('gallery.gallery') }}" class="btn btn-outline-primary btn-lg px-4">Browse Apps</a>
</div>
</div>
</div>
<!-- Featured Apps --> <!-- Featured Apps -->
<div class="container">
<h2 class="mb-4">Featured Apps</h2> <h2 class="mb-4">Featured Apps</h2>
{% if featured_apps %} {% if featured_apps %}
<div class="row"> <div class="row">
@ -28,10 +35,10 @@
<p class="card-text"><small class="text-muted">By {{ app.developer }}</small></p> <p class="card-text"><small class="text-muted">By {{ app.developer }}</small></p>
<div class="d-flex flex-wrap gap-2 mb-2"> <div class="d-flex flex-wrap gap-2 mb-2">
{% if app.website %} {% if app.website %}
<a href="{{ app.website }}" class="btn btn-primary btn-sm" aria-label="Visit {{ app.name }} website">Website</a> <a href="{{ app.website }}" class="btn btn-primary btn-sm" target="_blank" rel="noopener noreferrer" aria-label="Visit {{ app.name }} website">Website</a>
{% endif %} {% endif %}
{% if app.launch_url %} {% if app.launch_url %}
<a href="{{ app.launch_url }}" class="btn btn-outline-primary btn-sm" aria-label="Try {{ app.name }} with sandbox data">Try App</a> <a href="{{ app.launch_url }}" class="btn btn-outline-primary btn-sm" target="_blank" rel="noopener noreferrer" aria-label="Try {{ app.name }} with sandbox data">Try App</a>
{% endif %} {% endif %}
</div> </div>
<a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-primary mt-auto">View Details</a> <a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-primary mt-auto">View Details</a>

View File

Before

Width:  |  Height:  |  Size: 819 KiB

After

Width:  |  Height:  |  Size: 819 KiB