diff --git a/.dockerignore b/.dockerignore
index cd2e6702..289e7d21 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,3 +1,5 @@
/build
/node_modules
-/.git
\ No newline at end of file
+/.git
+/.vscode
+/.github
\ No newline at end of file
diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml
new file mode 100644
index 00000000..af5fd9d6
--- /dev/null
+++ b/.github/workflows/push-docker.yml
@@ -0,0 +1,54 @@
+name: Build and Push Docker Image
+permissions:
+ contents: read
+ packages: write
+ id-token: write
+ pages: write
+
+on:
+ push:
+ branches:
+ - main
+ release:
+ types: [published]
+
+jobs:
+ build-and-push:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55
+
+ - name: Login to DockerHub
+ uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Extract metadata (tags, labels) for Docker
+ id: meta
+ uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
+ with:
+ images: ${{ vars.DOCKERHUB_REPO }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
+ with:
+ context: .
+ file: ./docker/Dockerfile
+ push: true
+ build-args: GITHUB_REF=${{ github.ref_name }}
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ platforms: linux/amd64,linux/arm64
+
+ - uses: shrink/actions-docker-extract@f97780187d59e88f06a98b166a7e700c0f400314
+ id: extract
+ with:
+ image: ${{ vars.DOCKERHUB_REPO }}
+ path: /usr/share/nginx/html/.
+ destination: dist
\ No newline at end of file
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 00000000..37d6eec4
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,35 @@
+# Use the official Node.js image as the base image
+FROM node:20
+
+# Set the working directory
+WORKDIR /app
+
+# Copy package.json and package-lock.json
+COPY package*.json ./
+
+# Install dependencies
+RUN npm install
+
+# Copy the rest of the application code
+COPY . .
+
+# Build the React app
+RUN npm run build
+
+# Use the official Nginx image to serve the React app
+FROM nginxinc/nginx-unprivileged:alpine
+
+# Copy only JS/CSS from the build output
+COPY --from=0 /app/build/yasgui.min.js /usr/share/nginx/html/
+COPY --from=0 /app/build/yasgui.min.css /usr/share/nginx/html/
+
+# Copy other static assets
+COPY docker/src/ /usr/share/nginx/html/
+
+USER root
+
+COPY docker/change-default-endpoint.sh /docker-entrypoint.d/
+RUN chmod +x /docker-entrypoint.d/change-default-endpoint.sh
+RUN chown 1001:0 -R /usr/share/nginx/html/
+
+USER 1001
\ No newline at end of file
diff --git a/docker/change-default-endpoint.sh b/docker/change-default-endpoint.sh
new file mode 100644
index 00000000..71fa5503
--- /dev/null
+++ b/docker/change-default-endpoint.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+# Set default value if the environment variable is not set
+: ${YASGUI_DEFAULT_ENDPOINT:="https://dbpedia.org/sparql"}
+
+# Replace the default endpoint in the bundle.js file
+echo "Switching YASGUI default endpoint to: $YASGUI_DEFAULT_ENDPOINT"
+sed -i "s|https://change.to.default.endpoint/sparql|$(printf '%s' "$YASGUI_DEFAULT_ENDPOINT" | sed 's/[&/\]/\\&/g')|g" /usr/share/nginx/html/*.js
+
+# Execute the command passed to the entrypoint
+exec "$@"
+
+# Execute the original Docker entrypoint script
+exec /docker-entrypoint.sh
\ No newline at end of file
diff --git a/docker/src/favicon.png b/docker/src/favicon.png
new file mode 100644
index 00000000..663009b9
Binary files /dev/null and b/docker/src/favicon.png differ
diff --git a/docker/src/index.html b/docker/src/index.html
new file mode 100644
index 00000000..8b188298
--- /dev/null
+++ b/docker/src/index.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Yasgui
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docker/src/logo.svg b/docker/src/logo.svg
new file mode 100644
index 00000000..62b2cc99
--- /dev/null
+++ b/docker/src/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docker/src/main.css b/docker/src/main.css
new file mode 100644
index 00000000..f1facb7f
--- /dev/null
+++ b/docker/src/main.css
@@ -0,0 +1,128 @@
+/* Global styles */
+body {
+ margin: 0;
+ padding: 0;
+ font-family:
+ -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
+ "Helvetica Neue", sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* YASGUI container */
+#yasgui {
+ margin-top: 0px;
+ height: calc(100vh - 60px);
+}
+
+/* Footer styles */
+#footer {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background-color: #f5f5f5;
+ padding: 8px 20px;
+ border-top: 1px solid #e0e0e0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ transition: transform 0.3s ease-in-out;
+}
+
+.footer-left {
+ position: absolute;
+ left: 20px;
+ display: flex;
+ align-items: center;
+}
+
+.footer-center {
+ text-align: center;
+}
+
+#logo {
+ height: 35px;
+ width: auto;
+ transition: opacity 0.2s;
+}
+
+#logo:hover {
+ opacity: 0.8;
+}
+
+#footer a {
+ color: #0066cc;
+ text-decoration: none;
+}
+
+#footer a:hover {
+ text-decoration: underline;
+}
+
+/* Footer toggle button */
+#footer-toggle {
+ display: none;
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background-color: #0066cc;
+ color: white;
+ border: none;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ cursor: pointer;
+ z-index: 999;
+ font-size: 20px;
+ transition: all 0.3s ease;
+}
+
+#footer-toggle:hover {
+ background-color: #0052a3;
+ transform: scale(1.1);
+}
+
+#footer-toggle:active {
+ transform: scale(0.95);
+}
+
+/* Responsive behavior for small screens */
+@media (max-width: 768px) {
+ #footer {
+ transform: translateY(100%);
+ }
+
+ #footer.expanded {
+ transform: translateY(0);
+ }
+
+ #footer-toggle {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ #footer-toggle.hidden {
+ opacity: 0;
+ pointer-events: none;
+ transform: scale(0);
+ }
+
+ .footer-left {
+ position: relative;
+ left: 0;
+ margin-bottom: 10px;
+ }
+
+ #footer {
+ flex-direction: column;
+ padding: 15px 20px;
+ }
+
+ .footer-center {
+ margin-top: 10px;
+ }
+}
diff --git a/docker/src/main.js b/docker/src/main.js
new file mode 100644
index 00000000..ec244d00
--- /dev/null
+++ b/docker/src/main.js
@@ -0,0 +1,74 @@
+// Footer toggle functionality for small screens
+(function () {
+ const footer = document.getElementById("footer");
+ const footerToggle = document.getElementById("footer-toggle");
+
+ if (!footer || !footerToggle) return;
+
+ // Toggle footer visibility
+ footerToggle.addEventListener("click", function () {
+ footer.classList.toggle("expanded");
+ footerToggle.classList.toggle("hidden");
+ });
+
+ // Close footer when clicking outside
+ document.addEventListener("click", function (event) {
+ const isClickInsideFooter = footer.contains(event.target);
+ const isClickOnToggle = footerToggle.contains(event.target);
+
+ // Only close if footer is expanded, click is outside, and not on toggle button
+ if (footer.classList.contains("expanded") && !isClickInsideFooter && !isClickOnToggle) {
+ footer.classList.remove("expanded");
+ footerToggle.classList.remove("hidden");
+ }
+ });
+})();
+
+// YASGUI is loaded via CDN in index.html and available as a global variable
+if (typeof Yasgui !== "undefined") {
+ const yasgui = new Yasgui(document.getElementById("yasgui"), {
+ // Set the SPARQL endpoint
+ requestConfig: {
+ endpoint: "https://change.to.default.endpoint/sparql",
+ },
+
+ // Allow resizing of the Yasqe editor
+ resizeable: true,
+
+ // Whether to autofocus on Yasqe on page load
+ autofocus: true,
+
+ // Use the default endpoint when a new tab is opened
+ copyEndpointOnNewTab: true,
+
+ // Configuring which endpoints appear in the endpoint catalogue list
+ endpointCatalogueOptions: {
+ getData: () => {
+ return [
+ //List of objects should contain the endpoint field
+ //Feel free to include any other fields (e.g. a description or icon
+ //that you'd like to use when rendering)
+ {
+ endpoint: "https://data-interop.era.europa.eu/api/sparql",
+ },
+ {
+ endpoint: "https://dbpedia.org/sparql",
+ },
+ {
+ endpoint: "https://query.wikidata.org",
+ },
+ // ...
+ ];
+ },
+ //Data object keys that are used for filtering. The 'endpoint' key already used by default
+ keys: [],
+ //Data argument contains a `value` property for the matched data object
+ //Source argument is the suggestion DOM element to append your rendered item to
+ renderItem: (data, source) => {
+ const contentDiv = document.createElement("div");
+ contentDiv.innerText = data.value.endpoint;
+ source.appendChild(contentDiv);
+ },
+ },
+ });
+}
diff --git a/docker/src/manifest.json b/docker/src/manifest.json
new file mode 100644
index 00000000..9c89c9f9
--- /dev/null
+++ b/docker/src/manifest.json
@@ -0,0 +1,15 @@
+{
+ "short_name": "Yasgui",
+ "name": "Yet Another SPARQL GUI",
+ "icons": [
+ {
+ "src": "favicon.png",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/docker/src/robots.txt b/docker/src/robots.txt
new file mode 100644
index 00000000..e9e57dc4
--- /dev/null
+++ b/docker/src/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow: