Merge dev into main for v03.09.01 release

Merges all dev branch work including:
- Accessibility toolbar (6 toggleable options)
- Complete module overrides with showtitle (24 modules)
- IcoMoon to Font Awesome 7 compatibility layer
- Sidebar accordion (open desktop, collapsed mobile)
- TOC scoped to article body, multi-level heading support
- Bootstrap collapse for mobile menu
- Search module full-width in header
- Blog equal-height cards
- Footer padding and dynamic floating control offsets
- Auto dev mode when Joomla debug enabled
- mod_login count() null fix
- Main menu link color fixes
- Back-to-top FA icon and anchor

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 17:40:15 -05:00
175 changed files with 8153 additions and 5891 deletions

18
.github/CODEOWNERS vendored
View File

@@ -8,20 +8,8 @@
# Combined with branch protection (require PR reviews), this prevents # Combined with branch protection (require PR reviews), this prevents
# unauthorized modifications to workflows, configs, and governance files. # unauthorized modifications to workflows, configs, and governance files.
# ── Synced workflows (managed by MokoStandards — do not edit manually) ──── # ── Workflows (synced from MokoStandards — must not be manually edited) ──
/.github/workflows/deploy-dev.yml @jmiller-moko /.github/workflows/ @jmiller-moko
/.github/workflows/deploy-demo.yml @jmiller-moko
/.github/workflows/deploy-rs.yml @jmiller-moko
/.github/workflows/auto-release.yml @jmiller-moko
/.github/workflows/auto-dev-issue.yml @jmiller-moko
/.github/workflows/auto-assign.yml @jmiller-moko
/.github/workflows/sync-version-on-merge.yml @jmiller-moko
/.github/workflows/enterprise-firewall-setup.yml @jmiller-moko
/.github/workflows/repository-cleanup.yml @jmiller-moko
/.github/workflows/standards-compliance.yml @jmiller-moko
/.github/workflows/codeql-analysis.yml @jmiller-moko
/.github/workflows/repo_health.yml @jmiller-moko
# Custom workflows in .github/workflows/ not listed above are repo-owned.
# ── GitHub configuration ───────────────────────────────────────────────── # ── GitHub configuration ─────────────────────────────────────────────────
/.github/ISSUE_TEMPLATE/ @jmiller-moko /.github/ISSUE_TEMPLATE/ @jmiller-moko
@@ -35,7 +23,7 @@
/composer.json @jmiller-moko /composer.json @jmiller-moko
/phpstan.neon @jmiller-moko /phpstan.neon @jmiller-moko
/Makefile @jmiller-moko /Makefile @jmiller-moko
/.ftpignore @jmiller-moko /.ftp_ignore @jmiller-moko
/.gitignore @jmiller-moko /.gitignore @jmiller-moko
/.gitattributes @jmiller-moko /.gitattributes @jmiller-moko
/.editorconfig @jmiller-moko /.editorconfig @jmiller-moko

View File

@@ -9,22 +9,14 @@
# INGROUP: MokoStandards.Automation # INGROUP: MokoStandards.Automation
# REPO: https://github.com/mokoconsulting-tech/MokoStandards # REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/auto-dev-issue.yml.template # PATH: /templates/workflows/shared/auto-dev-issue.yml.template
# VERSION: 04.05.13 # VERSION: 04.05.00
# BRIEF: Auto-create tracking issue with sub-issues for dev/rc branch workflow # BRIEF: Auto-create tracking issue when a dev/** or rc/** branch is pushed
# NOTE: Synced via bulk-repo-sync to .github/workflows/auto-dev-issue.yml in all governed repos. # NOTE: Synced via bulk-repo-sync to .github/workflows/auto-dev-issue.yml in all governed repos.
name: Dev/RC Branch Issue name: Auto Dev Branch Issue
on: on:
# Auto-create on RC branch creation
create: create:
# Manual trigger for dev branches
workflow_dispatch:
inputs:
branch:
description: 'Branch name (e.g., dev/my-feature or dev/04.06)'
required: true
type: string
env: env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@@ -38,20 +30,15 @@ jobs:
name: Create version tracking issue name: Create version tracking issue
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: >- if: >-
(github.event_name == 'workflow_dispatch') || github.event.ref_type == 'branch' &&
(github.event.ref_type == 'branch' && startsWith(github.event.ref, 'rc/')) (startsWith(github.event.ref, 'dev/') || startsWith(github.event.ref, 'rc/'))
steps: steps:
- name: Create tracking issue and sub-issues - name: Create tracking issue
env: env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: | run: |
# For manual dispatch, use input; for auto, use event ref
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
BRANCH="${{ inputs.branch }}"
else
BRANCH="${{ github.event.ref }}" BRANCH="${{ github.event.ref }}"
fi
REPO="${{ github.repository }}" REPO="${{ github.repository }}"
ACTOR="${{ github.actor }}" ACTOR="${{ github.actor }}"
NOW=$(date -u '+%Y-%m-%d %H:%M UTC') NOW=$(date -u '+%Y-%m-%d %H:%M UTC')
@@ -71,122 +58,45 @@ jobs:
TITLE="${TITLE_PREFIX}(${VERSION}): ${BRANCH_TYPE} tracking for ${BRANCH}" TITLE="${TITLE_PREFIX}(${VERSION}): ${BRANCH_TYPE} tracking for ${BRANCH}"
BODY="## ${BRANCH_TYPE} Branch Created
| Field | Value |
|-------|-------|
| **Branch** | \`${BRANCH}\` |
| **Version** | \`${VERSION}\` |
| **Type** | ${BRANCH_TYPE} |
| **Created by** | @${ACTOR} |
| **Created at** | ${NOW} |
| **Repository** | \`${REPO}\` |
## Checklist
- [ ] Feature development complete
- [ ] Tests passing
- [ ] README.md version bumped to \`${VERSION}\`
- [ ] CHANGELOG.md updated
- [ ] PR created targeting \`main\`
- [ ] Code reviewed and approved
- [ ] Merged to \`main\`
---
*Auto-created by [auto-dev-issue.yml](.github/workflows/auto-dev-issue.yml) on branch creation.*"
# Dedent heredoc
BODY=$(echo "$BODY" | sed 's/^ //')
# Check for existing issue with same title prefix # Check for existing issue with same title prefix
EXISTING=$(gh api "repos/${REPO}/issues?state=open&per_page=10" \ EXISTING=$(gh api "repos/${REPO}/issues?state=open&per_page=5" \
--jq ".[] | select(.title | startswith(\"${TITLE_PREFIX}(${VERSION})\")) | .number" 2>/dev/null | head -1) --jq ".[] | select(.title | startswith(\"${TITLE_PREFIX}(${VERSION})\")) | .number" 2>/dev/null | head -1)
if [ -n "$EXISTING" ]; then if [ -n "$EXISTING" ]; then
echo " Issue #${EXISTING} already exists for ${VERSION}" >> $GITHUB_STEP_SUMMARY echo " Issue #${EXISTING} already exists for ${VERSION}" >> $GITHUB_STEP_SUMMARY
exit 0
fi
# ── Define sub-issues for the dev workflow ────────────────────────
if [[ "$BRANCH" == rc/* ]]; then
SUB_ISSUES=(
"RC Testing|Verify all features work on rc branch|type: test,release-candidate"
"Regression Testing|Run full regression suite before merge to main|type: test,release-candidate"
"Version Bump|Bump version in README.md and all headers|type: version,release-candidate"
"Changelog Update|Update CHANGELOG.md with release notes|documentation,release-candidate"
"Merge to Main|Create PR from rc branch to main|type: release,needs-review"
)
else else
SUB_ISSUES=( ISSUE_URL=$(gh issue create \
"Development|Implement feature/fix on dev branch|type: feature,status: in-progress"
"Unit Testing|Write and pass unit tests|type: test,status: pending"
"Code Review|Request and complete code review|needs-review,status: pending"
"Version Bump|Bump version in README.md and all headers|type: version,status: pending"
"Changelog Update|Update CHANGELOG.md with release notes|documentation,status: pending"
"Create RC Branch|Promote dev to rc branch for final testing|type: release,status: pending"
"Merge to Main|Create PR from rc/dev to main|type: release,needs-review,status: pending"
)
fi
# ── Create sub-issues first ───────────────────────────────────────
SUB_LIST=""
SUB_NUMBERS=""
for SUB in "${SUB_ISSUES[@]}"; do
IFS='|' read -r SUB_TITLE SUB_DESC SUB_LABELS <<< "$SUB"
SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}"
SUB_BODY=$(printf '### %s\n\n%s\n\n| Field | Value |\n|-------|-------|\n| **Parent Branch** | `%s` |\n| **Version** | `%s` |\n\n---\n*Sub-issue of the %s tracking issue for `%s`.*' \
"$SUB_TITLE" "$SUB_DESC" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$BRANCH")
SUB_URL=$(gh issue create \
--repo "$REPO" \
--title "$SUB_FULL_TITLE" \
--body "$SUB_BODY" \
--label "${SUB_LABELS}" \
--assignee "jmiller-moko" 2>&1)
SUB_NUM=$(echo "$SUB_URL" | grep -oE '[0-9]+$')
if [ -n "$SUB_NUM" ]; then
SUB_LIST="${SUB_LIST}\n- [ ] ${SUB_TITLE} (#${SUB_NUM})"
SUB_NUMBERS="${SUB_NUMBERS} #${SUB_NUM}"
fi
sleep 0.3
done
# ── Create parent tracking issue ──────────────────────────────────
PARENT_BODY=$(printf '## %s Branch Created\n\n| Field | Value |\n|-------|-------|\n| **Branch** | `%s` |\n| **Version** | `%s` |\n| **Type** | %s |\n| **Created by** | @%s |\n| **Created at** | %s |\n| **Repository** | `%s` |\n\n## Workflow Sub-Issues\n\n%b\n\n---\n*Auto-created by [auto-dev-issue.yml](.github/workflows/auto-dev-issue.yml) on branch creation.*' \
"$BRANCH_TYPE" "$BRANCH" "$VERSION" "$BRANCH_TYPE" "$ACTOR" "$NOW" "$REPO" "$SUB_LIST")
PARENT_URL=$(gh issue create \
--repo "$REPO" \ --repo "$REPO" \
--title "$TITLE" \ --title "$TITLE" \
--body "$PARENT_BODY" \ --body "$BODY" \
--label "${LABEL_TYPE},version" \ --label "${LABEL_TYPE},version" \
--assignee "jmiller-moko" 2>&1) --assignee "jmiller-moko" 2>&1)
echo "✅ Created tracking issue: ${ISSUE_URL}" >> $GITHUB_STEP_SUMMARY
PARENT_NUM=$(echo "$PARENT_URL" | grep -oE '[0-9]+$')
# ── Link sub-issues back to parent ────────────────────────────────
if [ -n "$PARENT_NUM" ]; then
for SUB in "${SUB_ISSUES[@]}"; do
IFS='|' read -r SUB_TITLE _ _ <<< "$SUB"
SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}"
SUB_NUM=$(gh api "repos/${REPO}/issues?state=open&per_page=20" \
--jq ".[] | select(.title == \"${SUB_FULL_TITLE}\") | .number" 2>/dev/null | head -1)
if [ -n "$SUB_NUM" ]; then
gh api "repos/${REPO}/issues/${SUB_NUM}" -X PATCH \
-f body="$(gh api "repos/${REPO}/issues/${SUB_NUM}" --jq '.body' 2>/dev/null)
> **Parent Issue:** #${PARENT_NUM}" --silent 2>/dev/null || true
fi fi
sleep 0.2
done
fi
# ── RC: Create or update draft release ────────────────────────────
if [[ "$BRANCH" == rc/* ]]; then
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
RELEASE_TAG="v${MAJOR}"
DRAFT_EXISTS=$(gh release view "$RELEASE_TAG" --json isDraft -q .isDraft 2>/dev/null || true)
if [ -z "$DRAFT_EXISTS" ]; then
# No release exists — create draft
gh release create "$RELEASE_TAG" \
--title "v${MAJOR} (RC: ${VERSION})" \
--notes "## Release Candidate ${VERSION}\n\nRC branch: \`${BRANCH}\`\nTracking issue: ${PARENT_URL}" \
--draft \
--target main 2>/dev/null || true
echo "Draft release created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
elif [ "$DRAFT_EXISTS" = "true" ]; then
# Draft exists — update title
gh release edit "$RELEASE_TAG" \
--title "v${MAJOR} (RC: ${VERSION})" --draft 2>/dev/null || true
echo "Draft release updated: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
else
# Release exists and is published — set back to draft for RC
gh release edit "$RELEASE_TAG" \
--title "v${MAJOR} (RC: ${VERSION})" --draft 2>/dev/null || true
echo "Release ${RELEASE_TAG} set to draft for RC" >> $GITHUB_STEP_SUMMARY
fi
fi
# ── Summary ───────────────────────────────────────────────────────
echo "## Dev Workflow Issues Created" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Item | Issue |" >> $GITHUB_STEP_SUMMARY
echo "|------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| **Parent** | ${PARENT_URL} |" >> $GITHUB_STEP_SUMMARY
echo "| **Sub-issues** |${SUB_NUMBERS} |" >> $GITHUB_STEP_SUMMARY

View File

@@ -6,31 +6,29 @@
# DEFGROUP: GitHub.Workflow # DEFGROUP: GitHub.Workflow
# INGROUP: MokoStandards.Release # INGROUP: MokoStandards.Release
# REPO: https://github.com/mokoconsulting-tech/MokoStandards # REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/joomla/auto-release.yml.template # PATH: /templates/workflows/shared/auto-release.yml.template
# VERSION: 04.05.13 # VERSION: 04.05.00
# BRIEF: Joomla build & release — ZIP package, update.xml, SHA-256 checksum # BRIEF: Unified build & release pipeline — version branch, platform version, badges, tag, release
# #
# +========================================================================+ # ╔════════════════════════════════════════════════════════════════════════╗
# | BUILD & RELEASE PIPELINE (JOOMLA) | # BUILD & RELEASE PIPELINE
# +========================================================================+ # ╠════════════════════════════════════════════════════════════════════════╣
# | | #
# | Triggers on push to main (skips bot commits + [skip ci]): | # Triggers on push to main (skips bot commits + [skip ci]):
# | | #
# | Every push: | # Every push:
# | 1. Read version from README.md | # 1. Read version from README.md
# | 3. Set platform version (Joomla <version>) | # 3. Set platform version (Dolibarr $this->version, Joomla <version>)║
# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files | # 4. Update [VERSION: XX.YY.ZZ] badges in markdown files
# | 5. Write update.xml (Joomla update server XML) | # 5. Write update.txt / update.xml
# | 6. Create git tag vXX.YY.ZZ | # 6. Create git tag vXX.YY.ZZ
# | 7a. Patch: update existing GitHub Release for this minor | # 7a. Patch: update existing GitHub Release for this minor
# | 8. Build ZIP, upload asset, write SHA-256 to update.xml | #
# | | # ║ Minor releases only (patch == 00):
# | Every version change: archives main -> version/XX.YY branch | # 2. Create/update version/XX.YY branch (patches update in-place)
# | Patch 00 = development (no release). First release = patch 01. | # 7b. Create new GitHub Release
# | First release only (patch == 01): | #
# | 7b. Create new GitHub Release | # ╚════════════════════════════════════════════════════════════════════════╝
# | |
# +========================================================================+
name: Build & Release name: Build & Release
@@ -72,13 +70,13 @@ jobs:
cd /tmp/mokostandards cd /tmp/mokostandards
composer install --no-dev --no-interaction --quiet composer install --no-dev --no-interaction --quiet
# -- STEP 1: Read version ----------------------------------------------- # ── STEP 1: Read version ───────────────────────────────────────────
- name: "Step 1: Read version from README.md" - name: "Step 1: Read version from README.md"
id: version id: version
run: | run: |
VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null) VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null)
if [ -z "$VERSION" ]; then if [ -z "$VERSION" ]; then
echo "No VERSION in README.md — skipping release" echo "⏭️ No VERSION in README.md — skipping release"
echo "skip=true" >> "$GITHUB_OUTPUT" echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0 exit 0
fi fi
@@ -86,34 +84,24 @@ jobs:
MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}') MINOR=$(echo "$VERSION" | awk -F. '{printf "%s.%s", $1, $2}')
PATCH=$(echo "$VERSION" | awk -F. '{print $3}') PATCH=$(echo "$VERSION" | awk -F. '{print $3}')
MAJOR=$(echo "$VERSION" | awk -F. '{print $1}')
MINOR_NUM=$(echo "$VERSION" | awk -F. '{print $2}')
echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
echo "branch=version/${MINOR}" >> "$GITHUB_OUTPUT" echo "branch=version/${MINOR}" >> "$GITHUB_OUTPUT"
echo "minor=$MINOR" >> "$GITHUB_OUTPUT" echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
if [ "$PATCH" = "00" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "is_minor=false" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (patch 00 = development — skipping release)"
else
echo "skip=false" >> "$GITHUB_OUTPUT" echo "skip=false" >> "$GITHUB_OUTPUT"
if [ "$PATCH" = "01" ]; then if [ "$PATCH" = "00" ]; then
echo "is_minor=true" >> "$GITHUB_OUTPUT" echo "is_minor=true" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (first release — full pipeline)" echo "Version: $VERSION (minor release — full pipeline)"
else else
echo "is_minor=false" >> "$GITHUB_OUTPUT" echo "is_minor=false" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (patch — platform version + badges only)" echo "Version: $VERSION (patch — platform version + badges only)"
fi
fi fi
- name: Check if already released - name: Check if already released
if: steps.version.outputs.skip != 'true' if: steps.version.outputs.skip != 'true'
id: check id: check
run: | run: |
TAG="${{ steps.version.outputs.release_tag }}" TAG="${{ steps.version.outputs.tag }}"
BRANCH="${{ steps.version.outputs.branch }}" BRANCH="${{ steps.version.outputs.branch }}"
TAG_EXISTS=false TAG_EXISTS=false
@@ -131,109 +119,102 @@ jobs:
echo "already_released=false" >> "$GITHUB_OUTPUT" echo "already_released=false" >> "$GITHUB_OUTPUT"
fi fi
# -- SANITY CHECKS ------------------------------------------------------- # ── SANITY CHECKS ────────────────────────────────────────────────────
- name: "Sanity: Pre-release validation" - name: "Sanity: Platform-specific validation"
if: >- if: >-
steps.version.outputs.skip != 'true' && steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true' steps.check.outputs.already_released != 'true'
run: | run: |
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null)
ERRORS=0 ERRORS=0
echo "## Pre-Release Sanity Checks (Joomla)" >> $GITHUB_STEP_SUMMARY echo "## 🔍 Pre-Release Sanity Checks" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Platform: \`${PLATFORM}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
# -- Version drift check (must pass before release) --------
README_VER=$(grep -oP 'VERSION:\s*\K[\d.]+' README.md 2>/dev/null | head -1)
if [ "$README_VER" != "$VERSION" ]; then
echo "- Version drift: README says \`${README_VER}\` but releasing \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Version consistent: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
# Check CHANGELOG version matches
CL_VER=$(grep -oP 'VERSION:\s*\K[\d.]+' CHANGELOG.md 2>/dev/null | head -1)
if [ -n "$CL_VER" ] && [ "$CL_VER" != "$VERSION" ]; then
echo "- CHANGELOG drift: \`${CL_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi
# Check composer.json version if present
if [ -f "composer.json" ]; then
COMP_VER=$(grep -oP '"version"\s*:\s*"\K[^"]+' composer.json 2>/dev/null | head -1)
if [ -n "$COMP_VER" ] && [ "$COMP_VER" != "$VERSION" ]; then
echo "- composer.json drift: \`${COMP_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi
fi
# Common checks # Common checks
if [ ! -f "LICENSE" ]; then if [ ! -f "LICENSE" ]; then
echo "- Missing LICENSE file" >> $GITHUB_STEP_SUMMARY echo " Missing LICENSE file" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1)) ERRORS=$((ERRORS+1))
else else
echo "- LICENSE present" >> $GITHUB_STEP_SUMMARY echo " LICENSE" >> $GITHUB_STEP_SUMMARY
fi fi
if [ ! -d "src" ] && [ ! -d "htdocs" ]; then if [ ! -d "src" ]; then
echo "- Warning: No src/ or htdocs/ directory" >> $GITHUB_STEP_SUMMARY echo "⚠️ No src/ directory" >> $GITHUB_STEP_SUMMARY
else else
echo "- Source directory present" >> $GITHUB_STEP_SUMMARY echo "✅ src/ directory" >> $GITHUB_STEP_SUMMARY
fi fi
# -- Joomla: manifest version drift -------- # Dolibarr-specific checks
if [ "$PLATFORM" = "crm-module" ]; then
MOD_FILE=$(find src htdocs -path "*/core/modules/mod*.class.php" -print -quit 2>/dev/null)
if [ -z "$MOD_FILE" ]; then
echo "❌ No module descriptor (src/core/modules/mod*.class.php)" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "✅ Module descriptor: \`${MOD_FILE}\`" >> $GITHUB_STEP_SUMMARY
# Check module number
NUMERO=$(grep -oP '\$this->numero\s*=\s*\K\d+' "$MOD_FILE" 2>/dev/null || echo "0")
if [ "$NUMERO" = "0" ] || [ -z "$NUMERO" ]; then
echo "❌ Module number (\$this->numero) is 0 or not set" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "✅ Module number: ${NUMERO}" >> $GITHUB_STEP_SUMMARY
fi
# Check url_last_version exists
if grep -q 'url_last_version' "$MOD_FILE" 2>/dev/null; then
echo "✅ url_last_version is set" >> $GITHUB_STEP_SUMMARY
else
echo "⚠️ url_last_version not set — update checks won't work" >> $GITHUB_STEP_SUMMARY
fi
fi
fi
# Joomla-specific checks
if [ "$PLATFORM" = "waas-component" ]; then
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -n "$MANIFEST" ]; then
XML_VER=$(grep -oP '<version>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
fi
# -- Joomla: XML manifest existence --------
if [ -z "$MANIFEST" ]; then if [ -z "$MANIFEST" ]; then
echo "- No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY echo " No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1)) ERRORS=$((ERRORS+1))
else else
echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY echo " Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# -- Joomla: extension type check -------- # Check extension type
TYPE=$(grep -oP '<extension[^>]+type="\K[^"]+' "$MANIFEST" 2>/dev/null) TYPE=$(grep -oP '<extension[^>]+type="\K[^"]+' "$MANIFEST" 2>/dev/null)
echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY echo " Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
fi
fi fi
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -gt 0 ]; then if [ "$ERRORS" -gt 0 ]; then
echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY echo "**${ERRORS} error(s) — release may be incomplete**" >> $GITHUB_STEP_SUMMARY
else else
echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY echo "**All sanity checks passed**" >> $GITHUB_STEP_SUMMARY
fi fi
# -- STEP 2: Create or update version/XX.YY archive branch --------------- # ── STEP 2: Create or update version/XX.YY branch ──────────────────
# Always runs — every version change on main archives to version/XX.YY - name: "Step 2: Version branch"
- name: "Step 2: Version archive branch" if: >-
if: steps.check.outputs.already_released != 'true' steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true'
run: | run: |
BRANCH="${{ steps.version.outputs.branch }}" BRANCH="${{ steps.version.outputs.branch }}"
IS_MINOR="${{ steps.version.outputs.is_minor }}" IS_MINOR="${{ steps.version.outputs.is_minor }}"
PATCH="${{ steps.version.outputs.version }}" if [ "$IS_MINOR" = "true" ]; then
PATCH_NUM=$(echo "$PATCH" | awk -F. '{print $3}')
# Check if branch exists
if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then
git push origin HEAD:"$BRANCH" --force
echo "Updated archive branch: ${BRANCH} (patch ${PATCH_NUM})" >> $GITHUB_STEP_SUMMARY
else
git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH" git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
git push origin "$BRANCH" --force git push origin "$BRANCH" --force
echo "Created archive branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY echo "🌿 Created branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
else
git push origin HEAD:"$BRANCH" --force
echo "📝 Updated branch: ${BRANCH} (patch)" >> $GITHUB_STEP_SUMMARY
fi fi
# -- STEP 3: Set platform version ---------------------------------------- # ── STEP 3: Set platform version ───────────────────────────────────
- name: "Step 3: Set platform version" - name: "Step 3: Set platform version"
if: >- if: >-
steps.version.outputs.skip != 'true' && steps.version.outputs.skip != 'true' &&
@@ -243,7 +224,7 @@ jobs:
php /tmp/mokostandards/api/cli/version_set_platform.php \ php /tmp/mokostandards/api/cli/version_set_platform.php \
--path . --version "$VERSION" --branch main --path . --version "$VERSION" --branch main
# -- STEP 4: Update version badges ---------------------------------------- # ── STEP 4: Update version badges ──────────────────────────────────
- name: "Step 4: Update version badges" - name: "Step 4: Update version badges"
if: >- if: >-
steps.version.outputs.skip != 'true' && steps.version.outputs.skip != 'true' &&
@@ -256,22 +237,27 @@ jobs:
fi fi
done done
# -- STEP 5: Write update.xml (Joomla update server) --------------------- # ── STEP 5: Write update files (Dolibarr: update.txt / Joomla: update.xml)
- name: "Step 5: Write update.xml" - name: "Step 5: Write update files"
if: >- if: >-
steps.version.outputs.skip != 'true' && steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true' steps.check.outputs.already_released != 'true'
run: | run: |
PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null)
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
REPO="${{ github.repository }}" REPO="${{ github.repository }}"
# -- Parse extension metadata from XML manifest ---------------- if [ "$PLATFORM" = "crm-module" ]; then
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) printf '%s' "$VERSION" > update.txt
if [ -z "$MANIFEST" ]; then echo "📦 update.txt: ${VERSION}" >> $GITHUB_STEP_SUMMARY
echo "Warning: No Joomla XML manifest found — skipping update.xml" >> $GITHUB_STEP_SUMMARY
exit 0
fi fi
if [ "$PLATFORM" = "waas-component" ]; then
# ── Parse extension metadata from XML manifest ──────────────
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "⚠️ No Joomla XML manifest found — skipping update.xml" >> $GITHUB_STEP_SUMMARY
else
EXT_NAME=$(grep -oP '<name>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}") EXT_NAME=$(grep -oP '<name>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}")
EXT_TYPE=$(grep -oP '<extension[^>]+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component") EXT_TYPE=$(grep -oP '<extension[^>]+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component")
EXT_ELEMENT=$(grep -oP '<element>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "") EXT_ELEMENT=$(grep -oP '<element>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "")
@@ -299,7 +285,7 @@ jobs:
FOLDER_TAG="<folder>${EXT_FOLDER}</folder>" FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"
fi fi
# Build targetplatform (fallback to Joomla 5 if not in manifest) # Build targetplatform (fallback to Joomla 5+6 if not in manifest)
if [ -z "$TARGET_PLATFORM" ]; then if [ -z "$TARGET_PLATFORM" ]; then
TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/") TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")
fi fi
@@ -313,7 +299,7 @@ jobs:
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip" DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip"
INFO_URL="https://github.com/${REPO}/releases/tag/v${VERSION}" INFO_URL="https://github.com/${REPO}/releases/tag/v${VERSION}"
# -- Write update.xml (stable release) -------------------------- # ── Write update.xml (stable release) ───────────────────────
{ {
printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>' printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
printf '%s\n' '<updates>' printf '%s\n' '<updates>'
@@ -340,16 +326,18 @@ jobs:
printf '%s\n' '</updates>' printf '%s\n' '</updates>'
} > update.xml } > update.xml
echo "update.xml: ${VERSION} (stable) — ${EXT_TYPE}/${EXT_ELEMENT}" >> $GITHUB_STEP_SUMMARY echo "📦 update.xml: ${VERSION} (stable) — ${EXT_TYPE}/${EXT_ELEMENT}" >> $GITHUB_STEP_SUMMARY
fi
fi
# -- Commit all changes --------------------------------------------------- # ── Commit all changes ─────────────────────────────────────────────
- name: Commit release changes - name: Commit release changes
if: >- if: >-
steps.version.outputs.skip != 'true' && steps.version.outputs.skip != 'true' &&
steps.check.outputs.already_released != 'true' steps.check.outputs.already_released != 'true'
run: | run: |
if git diff --quiet && git diff --cached --quiet; then if git diff --quiet && git diff --cached --quiet; then
echo "No changes to commit" echo " No changes to commit"
exit 0 exit 0
fi fi
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
@@ -360,25 +348,18 @@ jobs:
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>" --author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>"
git push git push
# -- STEP 6: Create tag --------------------------------------------------- # ── STEP 6: Create tag ─────────────────────────────────────────────
- name: "Step 6: Create git tag" - name: "Step 6: Create git tag"
if: >- if: >-
steps.version.outputs.skip != 'true' && steps.version.outputs.skip != 'true' &&
steps.check.outputs.tag_exists != 'true' && steps.check.outputs.tag_exists != 'true'
steps.version.outputs.is_minor == 'true'
run: | run: |
RELEASE_TAG="${{ steps.version.outputs.release_tag }}" TAG="${{ steps.version.outputs.tag }}"
# Only create the major release tag if it doesn't exist yet git tag "$TAG"
if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then git push origin "$TAG"
git tag "$RELEASE_TAG" echo "🏷️ Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
git push origin "$RELEASE_TAG"
echo "Tag created: ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
else
echo "Tag ${RELEASE_TAG} already exists" >> $GITHUB_STEP_SUMMARY
fi
echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
# -- STEP 7: Create or update GitHub Release ------------------------------ # ── STEP 7: Create or update GitHub Release ──────────────────────────
- name: "Step 7: GitHub Release" - name: "Step 7: GitHub Release"
if: >- if: >-
steps.version.outputs.skip != 'true' && steps.version.outputs.skip != 'true' &&
@@ -387,129 +368,67 @@ jobs:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: | run: |
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}" TAG="${{ steps.version.outputs.tag }}"
BRANCH="${{ steps.version.outputs.branch }}" BRANCH="${{ steps.version.outputs.branch }}"
MAJOR="${{ steps.version.outputs.major }}" IS_MINOR="${{ steps.version.outputs.is_minor }}"
# Derive the minor version base (XX.YY.00)
MINOR_BASE=$(echo "$VERSION" | sed 's/\.[0-9]*$/.00/')
MINOR_TAG="v${MINOR_BASE}"
NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) NOTES=$(php /tmp/mokostandards/api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}" [ -z "$NOTES" ] && NOTES="Release ${VERSION}"
echo "$NOTES" > /tmp/release_notes.md echo "$NOTES" > /tmp/release_notes.md
# Check if the major release already exists if [ "$IS_MINOR" = "true" ]; then
EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true) # Minor release: create new GitHub Release
gh release create "$TAG" \
if [ -z "$EXISTING" ]; then --title "${VERSION}" \
# First release for this major
gh release create "$RELEASE_TAG" \
--title "v${MAJOR} (latest: ${VERSION})" \
--notes-file /tmp/release_notes.md \ --notes-file /tmp/release_notes.md \
--target "$BRANCH" --target "$BRANCH"
echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY echo "🚀 Release created: ${VERSION}" >> $GITHUB_STEP_SUMMARY
else else
# Append version notes to existing major release # Patch release: update the existing minor release with new tag
CURRENT_NOTES=$(gh release view "$RELEASE_TAG" --json body -q .body 2>/dev/null || true) # Find the latest release for this minor version
EXISTING=$(gh release view "$MINOR_TAG" --json tagName -q .tagName 2>/dev/null || true)
if [ -n "$EXISTING" ]; then
# Update existing release body with patch info
CURRENT_NOTES=$(gh release view "$MINOR_TAG" --json body -q .body 2>/dev/null || true)
{ {
echo "$CURRENT_NOTES" echo "$CURRENT_NOTES"
echo "" echo ""
echo "---" echo "---"
echo "### ${VERSION}" echo "### Patch ${VERSION}"
echo "" echo ""
cat /tmp/release_notes.md cat /tmp/release_notes.md
} > /tmp/updated_notes.md } > /tmp/updated_notes.md
gh release edit "$RELEASE_TAG" \ gh release edit "$MINOR_TAG" \
--title "v${MAJOR} (latest: ${VERSION})" \ --title "${MINOR_BASE} (latest: ${VERSION})" \
--notes-file /tmp/updated_notes.md --notes-file /tmp/updated_notes.md
echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY echo "📝 Release updated: ${MINOR_BASE} → patch ${VERSION}" >> $GITHUB_STEP_SUMMARY
fi
# -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
# Every patch builds an install-ready ZIP and uploads it to the minor release.
# Result: one Release per minor version with a ZIP for each patch.
- name: "Step 8: Build Joomla package and update checksum"
if: >-
steps.version.outputs.skip != 'true'
env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: |
VERSION="${{ steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
REPO="${{ github.repository }}"
# All ZIPs upload to the major release tag (vXX)
gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || {
echo "No release ${RELEASE_TAG} found — skipping ZIP upload"
exit 0
}
# Find extension element name from manifest
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
[ -z "$MANIFEST" ] && exit 0
EXT_ELEMENT=$(grep -oP '<element>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml)
PACKAGE_NAME="${EXT_ELEMENT}-${VERSION}.zip"
# -- Build install-ready ZIP from src/ ----------------------------
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ — skipping package"; exit 0; }
cd "$SOURCE_DIR"
zip -r "/tmp/${PACKAGE_NAME}" . -x '*.git*' '*.DS_Store' 'Thumbs.db' '*.log'
cd ..
FILESIZE=$(stat -c%s "/tmp/${PACKAGE_NAME}" 2>/dev/null || stat -f%z "/tmp/${PACKAGE_NAME}" 2>/dev/null || echo "unknown")
# -- Calculate SHA-256 -------------------------------------------
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
# -- Upload ZIP to the minor release tag -------------------------
gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" --clobber 2>/dev/null || {
echo "Could not upload with --clobber, retrying..."
gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" 2>/dev/null || true
}
# -- Update update.xml with SHA-256 for latest patch -------------
if [ -f "update.xml" ]; then
if grep -q '<sha256>' update.xml; then
sed -i "s|<sha256>.*</sha256>|<sha256>sha256:${SHA256}</sha256>|" update.xml
else else
sed -i "s|</downloads>|</downloads>\n <sha256>sha256:${SHA256}</sha256>|" update.xml # No existing minor release found — create one for this patch
gh release create "$TAG" \
--title "${VERSION}" \
--notes-file /tmp/release_notes.md
echo "🚀 Release created: ${VERSION} (no minor release found)" >> $GITHUB_STEP_SUMMARY
fi
fi fi
# Also update the download URL to point to this patch's ZIP # ── Summary ────────────────────────────────────────────────────────
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
sed -i "s|<downloadurl[^>]*>[^<]*</downloadurl>|<downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>|" update.xml
git add update.xml
git commit -m "chore(release): SHA-256 + download URL for ${VERSION} [skip ci]" \
--author="github-actions[bot] <github-actions[bot]@users.noreply.github.com>" || true
git push || true
fi
echo "### Joomla Package" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${PACKAGE_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Size | ${FILESIZE} bytes |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | \`${RELEASE_TAG}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Download | [${PACKAGE_NAME}](https://github.com/${REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}) |" >> $GITHUB_STEP_SUMMARY
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary - name: Pipeline Summary
if: always() if: always()
run: | run: |
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY echo "## ⏭️ Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else else
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (Joomla)" >> $GITHUB_STEP_SUMMARY echo "## Build & Release Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY echo "|------|--------|" >> $GITHUB_STEP_SUMMARY

View File

@@ -22,7 +22,7 @@
# INGROUP: MokoStandards.Deploy # INGROUP: MokoStandards.Deploy
# REPO: https://github.com/mokoconsulting-tech/MokoStandards # REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/deploy-demo.yml.template # PATH: /templates/workflows/shared/deploy-demo.yml.template
# VERSION: 04.05.13 # VERSION: 04.05.00
# BRIEF: SFTP deployment workflow for demo server — synced to all governed repos # BRIEF: SFTP deployment workflow for demo server — synced to all governed repos
# NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-demo.yml in all governed repos. # NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-demo.yml in all governed repos.
# Port is resolved in order: DEMO_FTP_PORT variable → :port suffix in DEMO_FTP_HOST → 22. # Port is resolved in order: DEMO_FTP_PORT variable → :port suffix in DEMO_FTP_HOST → 22.
@@ -296,12 +296,6 @@ jobs:
HOST="$HOST_RAW" HOST="$HOST_RAW"
PORT="$PORT_VAR" PORT="$PORT_VAR"
if [ -z "$HOST" ]; then
echo "⏭️ DEMO_FTP_HOST not configured — skipping demo deployment."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Priority 1 — explicit DEMO_FTP_PORT variable # Priority 1 — explicit DEMO_FTP_PORT variable
if [ -n "$PORT" ]; then if [ -n "$PORT" ]; then
echo " Using explicit DEMO_FTP_PORT=${PORT}" echo " Using explicit DEMO_FTP_PORT=${PORT}"
@@ -323,7 +317,7 @@ jobs:
echo "SFTP target: ${HOST}:${PORT}" echo "SFTP target: ${HOST}:${PORT}"
- name: Build remote path - name: Build remote path
if: steps.source.outputs.skip == 'false' && steps.conn.outputs.skip != 'true' if: steps.source.outputs.skip == 'false'
id: remote id: remote
env: env:
DEMO_FTP_PATH: ${{ vars.DEMO_FTP_PATH }} DEMO_FTP_PATH: ${{ vars.DEMO_FTP_PATH }}
@@ -332,9 +326,10 @@ jobs:
BASE="$DEMO_FTP_PATH" BASE="$DEMO_FTP_PATH"
if [ -z "$BASE" ]; then if [ -z "$BASE" ]; then
echo "⏭️ DEMO_FTP_PATH not configured — skipping demo deployment." echo " DEMO_FTP_PATH is not set."
echo "skip=true" >> "$GITHUB_OUTPUT" echo " Configure it as an org-level variable (Settings → Variables) and"
exit 0 echo " ensure this repository has been granted access to it."
exit 1
fi fi
# DEMO_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo. # DEMO_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo.
@@ -645,7 +640,7 @@ jobs:
rm -f /tmp/deploy_key /tmp/sftp-config.json rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Create or update failure issue - name: Create or update failure issue
if: failure() && steps.remote.outputs.skip != 'true' && steps.conn.outputs.skip != 'true' if: failure() && steps.remote.outputs.skip != 'true'
env: env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: | run: |

View File

@@ -22,7 +22,7 @@
# INGROUP: MokoStandards.Deploy # INGROUP: MokoStandards.Deploy
# REPO: https://github.com/mokoconsulting-tech/MokoStandards # REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/deploy-dev.yml.template # PATH: /templates/workflows/shared/deploy-dev.yml.template
# VERSION: 04.05.13 # VERSION: 04.05.00
# BRIEF: SFTP deployment workflow for development server — synced to all governed repos # BRIEF: SFTP deployment workflow for development server — synced to all governed repos
# NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-dev.yml in all governed repos. # NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-dev.yml in all governed repos.
# Port is resolved in order: DEV_FTP_PORT variable → :port suffix in DEV_FTP_HOST → 22. # Port is resolved in order: DEV_FTP_PORT variable → :port suffix in DEV_FTP_HOST → 22.

View File

@@ -22,7 +22,7 @@
# INGROUP: MokoStandards.Deploy # INGROUP: MokoStandards.Deploy
# REPO: https://github.com/mokoconsulting-tech/MokoStandards # REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/deploy-rs.yml.template # PATH: /templates/workflows/shared/deploy-rs.yml.template
# VERSION: 04.05.13 # VERSION: 04.05.00
# BRIEF: SFTP deployment workflow for release staging server — synced to all governed repos # BRIEF: SFTP deployment workflow for release staging server — synced to all governed repos
# NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-rs.yml in all governed repos. # NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-rs.yml in all governed repos.
# Port is resolved in order: RS_FTP_PORT variable → :port suffix in RS_FTP_HOST → 22. # Port is resolved in order: RS_FTP_PORT variable → :port suffix in RS_FTP_HOST → 22.
@@ -296,12 +296,6 @@ jobs:
HOST="$HOST_RAW" HOST="$HOST_RAW"
PORT="$PORT_VAR" PORT="$PORT_VAR"
if [ -z "$HOST" ]; then
echo "⏭️ RS_FTP_HOST not configured — skipping RS deployment."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Priority 1 — explicit RS_FTP_PORT variable # Priority 1 — explicit RS_FTP_PORT variable
if [ -n "$PORT" ]; then if [ -n "$PORT" ]; then
echo " Using explicit RS_FTP_PORT=${PORT}" echo " Using explicit RS_FTP_PORT=${PORT}"
@@ -323,7 +317,7 @@ jobs:
echo "SFTP target: ${HOST}:${PORT}" echo "SFTP target: ${HOST}:${PORT}"
- name: Build remote path - name: Build remote path
if: steps.source.outputs.skip == 'false' && steps.conn.outputs.skip != 'true' if: steps.source.outputs.skip == 'false'
id: remote id: remote
env: env:
RS_FTP_PATH: ${{ vars.RS_FTP_PATH }} RS_FTP_PATH: ${{ vars.RS_FTP_PATH }}
@@ -332,9 +326,10 @@ jobs:
BASE="$RS_FTP_PATH" BASE="$RS_FTP_PATH"
if [ -z "$BASE" ]; then if [ -z "$BASE" ]; then
echo "⏭️ RS_FTP_PATH not configured — skipping RS deployment." echo " RS_FTP_PATH is not set."
echo "skip=true" >> "$GITHUB_OUTPUT" echo " Configure it as an org-level variable (Settings → Variables) and"
exit 0 echo " ensure this repository has been granted access to it."
exit 1
fi fi
# RS_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo. # RS_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo.
@@ -572,7 +567,7 @@ jobs:
rm -f /tmp/deploy_key /tmp/sftp-config.json rm -f /tmp/deploy_key /tmp/sftp-config.json
- name: Create or update failure issue - name: Create or update failure issue
if: failure() && steps.remote.outputs.skip != 'true' && steps.conn.outputs.skip != 'true' if: failure() && steps.remote.outputs.skip != 'true'
env: env:
GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }} GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
run: | run: |

View File

@@ -22,7 +22,7 @@
# INGROUP: MokoStandards.Firewall # INGROUP: MokoStandards.Firewall
# REPO: https://github.com/mokoconsulting-tech/MokoStandards # REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/enterprise-firewall-setup.yml.template # PATH: /templates/workflows/shared/enterprise-firewall-setup.yml.template
# VERSION: 04.05.13 # VERSION: 04.05.00
# BRIEF: Enterprise firewall configuration — generates outbound allow-rules including SFTP deployment server # BRIEF: Enterprise firewall configuration — generates outbound allow-rules including SFTP deployment server
# NOTE: Reads DEV_FTP_HOST / DEV_FTP_PORT variables to include SFTP egress rules alongside HTTPS rules. # NOTE: Reads DEV_FTP_HOST / DEV_FTP_PORT variables to include SFTP egress rules alongside HTTPS rules.

View File

@@ -9,7 +9,7 @@
# INGROUP: MokoStandards.Maintenance # INGROUP: MokoStandards.Maintenance
# REPO: https://github.com/mokoconsulting-tech/MokoStandards # REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/repository-cleanup.yml.template # PATH: /templates/workflows/shared/repository-cleanup.yml.template
# VERSION: 04.05.13 # VERSION: 04.05.00
# BRIEF: Recurring repository maintenance — labels, branches, workflows, logs, doc indexes # BRIEF: Recurring repository maintenance — labels, branches, workflows, logs, doc indexes
# NOTE: Synced via bulk-repo-sync to .github/workflows/repository-cleanup.yml in all governed repos. # NOTE: Synced via bulk-repo-sync to .github/workflows/repository-cleanup.yml in all governed repos.
# Runs on the 1st and 15th of each month at 6:00 AM UTC, and on manual dispatch. # Runs on the 1st and 15th of each month at 6:00 AM UTC, and on manual dispatch.

View File

@@ -9,7 +9,7 @@
# INGROUP: MokoStandards.Automation # INGROUP: MokoStandards.Automation
# REPO: https://github.com/mokoconsulting-tech/MokoStandards # REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/sync-version-on-merge.yml.template # PATH: /templates/workflows/shared/sync-version-on-merge.yml.template
# VERSION: 04.05.13 # VERSION: 04.05.00
# BRIEF: Auto-bump patch version on every push to main and propagate to all file headers # BRIEF: Auto-bump patch version on every push to main and propagate to all file headers
# NOTE: Synced via bulk-repo-sync to .github/workflows/sync-version-on-merge.yml in all governed repos. # NOTE: Synced via bulk-repo-sync to .github/workflows/sync-version-on-merge.yml in all governed repos.
# README.md is the single source of truth for the repository version. # README.md is the single source of truth for the repository version.

8
.gitignore vendored
View File

@@ -198,9 +198,5 @@ venv/
*.coverage *.coverage
hypothesis/ hypothesis/
src/media/css/theme/dark.custom.css
# ============================================================ src/media/css/theme/light.custom.css
# Cassiopeia custom theme overrides
# ============================================================
/src/media/css/theme/dark.custom.css
/src/media/css/theme/light.custom.css

View File

@@ -12,13 +12,48 @@
BRIEF: Changelog file documenting version history of MokoCassiopeia BRIEF: Changelog file documenting version history of MokoCassiopeia
--> -->
# Changelog — MokoCassiopeia (VERSION: 03.08.03) # Changelog — MokoCassiopeia (VERSION: 03.09.02)
All notable changes to the MokoCassiopeia Joomla template are documented in this file. All notable changes to the MokoCassiopeia Joomla template are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] - 2026-04-02
### Added
- **Favicon configuration** — New "Favicon" tab in template config; upload a PNG and all favicon sizes are auto-generated via PHP GD (ICO, Apple Touch Icon 180px, Android Chrome 192/512px, site.webmanifest)
- **Module overrides** — 11 new `default.php` layout overrides for Joomla core modules: `mod_custom`, `mod_articles_latest`, `mod_articles_popular`, `mod_articles_news`, `mod_articles_category`, `mod_breadcrumbs`, `mod_footer`, `mod_login`, `mod_finder`, `mod_tags_popular`, `mod_tags_similar`, `mod_related_items`
- **Module title support** — All module overrides respect `$module->showtitle`, `header_tag`, `header_class`, and `moduleclass_sfx` parameters
- **Module CSS** — BEM-scoped styles for module titles, article lists, tag badges, search forms, login forms, breadcrumbs, and footer content
- **Hero card variables** — Full variable-driven hero system: `--hero-card-bg`, `--hero-card-color`, `--hero-card-overlay`, `--hero-card-border-radius`, `--hero-card-padding-x/y`, `--hero-card-max-width`, plus `--hero-alt-card-*` for secondary variant
- **Hero mobile breakpoint** — Photo background hidden on mobile (≤767.98px), hero card becomes full-bleed (100dvh, no border-radius)
- **CSS fallback values** — 1365 `var()` calls in template.css now include inline fallback values
- **Card border-radius** — `.card` now has `.25rem` fallback on `var(--card-border-radius)`
- **Usage section in README** — Added missing "Usage" section required by MokoStandards
### Changed
- **Button backgrounds** — `--btn-bg: transparent` changed to `var(--body-bg)` in dark and light themes
- **Offcanvas close button** — `.offcanvas-header .btn-close` now gets `background-color` from `--offcanvas-bg`
- **Custom template sync** — Both `dark.custom.css` and `light.custom.css` now contain all variables from their standard counterparts (was missing 223 variables)
- **Overlay layer** — Added `--hero-overlay-bg-position` and `--hero-overlay-bg-size` variables
- **Legacy CSS cleanup** — Removed vendor prefixes (`-webkit-box`, `-ms-flexbox`) from `.overlay` rules, replaced with modern flexbox
### Removed
- **FILE INFORMATION headers** — Stripped DEFGROUP/INGROUP/PATH/VERSION/BRIEF metadata from all PHP, CSS, JS, INI, and HTML files (kept in XML and README per policy)
- **Mobile overrides** — Deleted 26 `mobile.php` layout files and their empty parent directories
- **Joomla-specific gitignore entries** — Removed ~700 lines of Joomla CMS core paths from `.gitignore` (not applicable to a template repository)
### Fixed
- **CI: composer install** — Workflow `standards-compliance.yml` now conditionally runs `composer install` only when `composer.json` exists
- **CI: YAML syntax** — Fixed invalid YAML in `auto-update-sha.yml` caused by multiline commit message in run block
---
## [03.09.02] - 2026-03-26 ## [03.09.02] - 2026-03-26
### Added - Hero Variant System & Block Color System ### Added - Hero Variant System & Block Color System
@@ -37,13 +72,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
#### Files Modified #### Files Modified
- `src/media/css/template.css` — hero variant rules, block color `:nth-child()` rules, named override rules - `src/media/css/template.css` — hero variant rules, block color `:nth-child()` rules, named override rules
- `src/templates/light.custom.css` — hero and block color variables (light mode) - `src/media/css/theme/light.standard.css` — hero and block color variables (light standard)
- `src/templates/dark.custom.css` — hero and block color variables (dark mode) - `src/media/css/theme/dark.standard.css` — hero and block color variables (dark standard)
- `docs/CSS_VARIABLES.md` — full variable reference for both systems - `src/templates/light.custom.css` — hero and block color variables (light custom starter)
- `src/templates/dark.custom.css` — hero and block color variables (dark custom starter)
- `src/templateDetails.xml` — Theme Preview tab, hero/block note fields, scriptfile registration, version bump to 03.09.02
- `src/language/en-GB/tpl_mokocassiopeia.ini` — language strings for new admin fields (British English)
- `src/language/en-US/tpl_mokocassiopeia.ini` — language strings for new admin fields (American English)
- `docs/CSS_VARIABLES.md` — full variable reference for both systems, sync script documentation
- `CHANGELOG.md` — this entry - `CHANGELOG.md` — this entry
#### Files Added #### Files Added
- `src/templates/theme-test.html` — Bootstrap-style test page showing all CSS variables and new features - `src/templates/theme-test.html` — Bootstrap-style test page with branded showcase, CSS variable swatches, hero demos, block color demos, and color test image
- `src/script.php` — Joomla install/update lifecycle script (runs CSS variable sync on upgrade, checks PHP/Joomla minimum versions)
- `src/sync_custom_vars.php` — CLI/library utility that detects missing CSS variables in user custom palettes and injects them
- `src/templates/brand-showcase.html` — Interactive color system gradients with hover pixel sampler, Bootstrap component showcase
#### Variable Audit
- All 20 hero/block variables confirmed present in all 4 theme files (light/dark standard + custom)
- No duplicate variable declarations found across any theme file
- `--gutter-x` references in template.css are self-scoped to grid containers (standard Bootstrap 5 behavior, not a `:root` variable)
--- ---

View File

@@ -146,6 +146,20 @@ The template includes a dark mode toggle. Test it by:
--- ---
## Usage
Once installed and set as the default site template, MokoCassiopeia works out of the box with Joomla's standard content and module system. Key usage points:
- **Template Options** — Configure via **System → Site Templates → MokoCassiopeia** (theme colours, layout, analytics, favicon, drawers)
- **Custom Colour Schemes** — Copy `templates/mokocassiopeia/templates/light.custom.css` or `dark.custom.css` to `media/templates/site/mokocassiopeia/css/theme/` and select "Custom" in the Theme tab
- **Custom CSS/JS** — Create `media/templates/site/mokocassiopeia/css/user.css` or `js/user.js` for site-specific overrides that survive template updates
- **Module Overrides** — The template includes overrides for common Joomla modules with consistent title rendering, Bootstrap 5 styling, and Font Awesome 7 icons
- **Dark Mode** — Enabled by default with a floating toggle button; respects system preference and persists via localStorage
See [Configuration](#-configuration) below for detailed parameter reference.
---
## ⚙️ Configuration ## ⚙️ Configuration
### Global Parameters ### Global Parameters

View File

@@ -10,7 +10,7 @@
INGROUP: MokoCassiopeia.Documentation INGROUP: MokoCassiopeia.Documentation
REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
FILE: docs/CSS_VARIABLES.md FILE: docs/CSS_VARIABLES.md
VERSION: 03.06.03 VERSION: 03.09.02
BRIEF: Complete CSS variable reference for MokoCassiopeia template BRIEF: Complete CSS variable reference for MokoCassiopeia template
--> -->
@@ -60,6 +60,12 @@ To create custom color schemes:
4. **Note**: Custom files are gitignored and won't be committed to the repository 4. **Note**: Custom files are gitignored and won't be committed to the repository
5. **On upgrade**: When the template is updated, `script.php` automatically runs `sync_custom_vars.php` to detect any new variables added to the starter templates and inject them into your existing custom palette files. Your existing values are never overwritten — only genuinely new variables are added. You can also run this manually:
```bash
php templates/mokocassiopeia/sync_custom_vars.php --dry-run # preview what would be added
php templates/mokocassiopeia/sync_custom_vars.php # apply missing variables
```
--- ---
## Primary Brand Colors ## Primary Brand Colors
@@ -1440,9 +1446,9 @@ These ensure optimal readability for links within alert boxes.
* Repository: [https://github.com/mokoconsulting-tech/MokoCassiopeia](https://github.com/mokoconsulting-tech/MokoCassiopeia) * Repository: [https://github.com/mokoconsulting-tech/MokoCassiopeia](https://github.com/mokoconsulting-tech/MokoCassiopeia)
* Path: /docs/CSS_VARIABLES.md * Path: /docs/CSS_VARIABLES.md
* Owner: Moko Consulting * Owner: Moko Consulting
* Version: 03.06.03 * Version: 03.09.02
* Status: Active * Status: Active
* Effective Date: 2026-01-30 * Effective Date: 2026-03-26
* Classification: Public Open Source Documentation * Classification: Public Open Source Documentation
## Revision History ## Revision History

View File

@@ -1,19 +1,9 @@
<?php <?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> /* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project. This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: Joomla.Template.Site
INGROUP: MokoCassiopeia
REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
PATH: ./templates/mokocassiopeia/component.php
VERSION: 03.06.02
BRIEF: Main template index file for MokoCassiopeia rendering site layout
*/ */
@@ -54,15 +44,6 @@ $sitename = htmlspecialchars($sitenameR, ENT_QUOTES, 'UTF-8');
$menu = $app->getMenu()->getActive(); $menu = $app->getMenu()->getActive();
$pageclass = $menu !== null ? $menu->getParams()->get('pageclass_sfx', '') : ''; $pageclass = $menu !== null ? $menu->getParams()->get('pageclass_sfx', '') : '';
// Respect “Site Name in Page Titles” (0:none, 1:before, 2:after)
$mode = (int) $app->get('sitename_pagetitles', 0);
$pageTitle = trim($this->getTitle());
$final = $pageTitle !== ''
? ($mode === 1 ? $sitenameR . ' - ' . $pageTitle
: ($mode === 2 ? $pageTitle . ' - ' . $sitenameR : $pageTitle))
: $sitenameR;
$this->setTitle($final);
// Template/Media path // Template/Media path
$templatePath = 'media/templates/site/mokocassiopeia'; $templatePath = 'media/templates/site/mokocassiopeia';

View File

@@ -1,19 +1,9 @@
<?php <?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> /* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project. This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: Joomla.Template.Site
INGROUP: MokoCassiopeia
REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
PATH: ./templates/mokocassiopeia/custom.php
VERSION: 03.06.02
BRIEF: MokoCassiopeia with user-defined overrides
*/ */
function console_log($output, $with_script_tags = true) { function console_log($output, $with_script_tags = true) {

View File

@@ -1,19 +1,9 @@
<?php <?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> /* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project. This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
DEFGROUP: Joomla.Template.Site
INGROUP: MokoCassiopeia
REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
PATH: ./templates/mokocassiopeia/error.php
VERSION: 03.06.02
BRIEF: Error page template file for MokoCassiopeia
*/ */
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -428,7 +418,7 @@ $wa->useScript('user.js'); // js/user.js
<?php if ($this->params->get('backTop') == 1) : ?> <?php if ($this->params->get('backTop') == 1) : ?>
<a href="#top" id="back-top" class="back-to-top-link" aria-label="<?php echo Text::_('TPL_MOKOCASSIOPEIA_BACKTOTOP'); ?>"> <a href="#top" id="back-top" class="back-to-top-link" aria-label="<?php echo Text::_('TPL_MOKOCASSIOPEIA_BACKTOTOP'); ?>">
<span class="icon-arrow-up icon-fw" aria-hidden="true"></span> <span class="fa-solid fa-arrow-up" aria-hidden="true"></span>
</a> </a>
<?php endif; ?> <?php endif; ?>

173
src/helper/favicon.php Normal file
View File

@@ -0,0 +1,173 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Favicon generator — creates ICO, Apple Touch Icon, and Android icons
* from a single source PNG uploaded via the template config.
*/
defined('_JEXEC') or die;
class MokoFaviconHelper
{
/**
* Sizes to generate: filename => [width, height, format].
* ICO embeds 16×16 and 32×32 internally.
*/
private const SIZES = [
'apple-touch-icon.png' => [180, 180, 'png'],
'favicon-32x32.png' => [32, 32, 'png'],
'favicon-16x16.png' => [16, 16, 'png'],
'android-chrome-192x192.png' => [192, 192, 'png'],
'android-chrome-512x512.png' => [512, 512, 'png'],
];
/**
* Generate all favicon files from a source PNG if they don't already exist
* or if the source has been modified since last generation.
*
* @param string $sourcePath Absolute path to the source PNG.
* @param string $outputDir Absolute path to the output directory.
*
* @return bool True if generation succeeded or files are up to date.
*/
public static function generate(string $sourcePath, string $outputDir): bool
{
if (!is_file($sourcePath) || !extension_loaded('gd')) {
return false;
}
if (!is_dir($outputDir)) {
mkdir($outputDir, 0755, true);
}
$sourceTime = filemtime($sourcePath);
$stampFile = $outputDir . '/.favicon_generated';
// Skip if already up to date
if (is_file($stampFile) && filemtime($stampFile) >= $sourceTime) {
return true;
}
$source = imagecreatefrompng($sourcePath);
if (!$source) {
return false;
}
imagealphablending($source, false);
imagesavealpha($source, true);
$srcW = imagesx($source);
$srcH = imagesy($source);
// Generate PNG sizes
foreach (self::SIZES as $filename => [$w, $h]) {
$resized = imagecreatetruecolor($w, $h);
imagealphablending($resized, false);
imagesavealpha($resized, true);
$transparent = imagecolorallocatealpha($resized, 0, 0, 0, 127);
imagefill($resized, 0, 0, $transparent);
imagecopyresampled($resized, $source, 0, 0, 0, 0, $w, $h, $srcW, $srcH);
imagepng($resized, $outputDir . '/' . $filename, 9);
imagedestroy($resized);
}
// Generate ICO (contains 16×16 and 32×32)
self::generateIco($source, $srcW, $srcH, $outputDir . '/favicon.ico');
// Generate site.webmanifest
self::generateManifest($outputDir);
imagedestroy($source);
// Write timestamp stamp
file_put_contents($stampFile, date('c'));
return true;
}
/**
* Build a minimal ICO file containing 16×16 and 32×32 PNG entries.
*/
private static function generateIco(\GdImage $source, int $srcW, int $srcH, string $outPath): void
{
$entries = [];
foreach ([16, 32] as $size) {
$resized = imagecreatetruecolor($size, $size);
imagealphablending($resized, false);
imagesavealpha($resized, true);
$transparent = imagecolorallocatealpha($resized, 0, 0, 0, 127);
imagefill($resized, 0, 0, $transparent);
imagecopyresampled($resized, $source, 0, 0, 0, 0, $size, $size, $srcW, $srcH);
ob_start();
imagepng($resized, null, 9);
$pngData = ob_get_clean();
imagedestroy($resized);
$entries[] = ['size' => $size, 'data' => $pngData];
}
// ICO header: 2 bytes reserved, 2 bytes type (1=ICO), 2 bytes count
$count = count($entries);
$ico = pack('vvv', 0, 1, $count);
// Calculate offset: header (6) + directory entries (16 each)
$offset = 6 + ($count * 16);
$imageData = '';
foreach ($entries as $entry) {
$size = $entry['size'] >= 256 ? 0 : $entry['size'];
$dataLen = strlen($entry['data']);
// ICONDIRENTRY: width, height, colors, reserved, planes, bpp, size, offset
$ico .= pack('CCCCvvVV', $size, $size, 0, 0, 1, 32, $dataLen, $offset);
$imageData .= $entry['data'];
$offset += $dataLen;
}
file_put_contents($outPath, $ico . $imageData);
}
/**
* Write a site.webmanifest for Android/PWA icon discovery.
*/
private static function generateManifest(string $outputDir): void
{
$manifest = [
'icons' => [
['src' => 'android-chrome-192x192.png', 'sizes' => '192x192', 'type' => 'image/png'],
['src' => 'android-chrome-512x512.png', 'sizes' => '512x512', 'type' => 'image/png'],
],
];
file_put_contents(
$outputDir . '/site.webmanifest',
json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
);
}
/**
* Return the <link> tags to inject into <head>.
*
* @param string $basePath URL path to the favicon directory (relative to site root).
*
* @return string HTML link tags.
*/
public static function getHeadTags(string $basePath): string
{
$basePath = rtrim($basePath, '/');
return '<link rel="apple-touch-icon" sizes="180x180" href="' . $basePath . '/apple-touch-icon.png">' . "\n"
. '<link rel="icon" type="image/png" sizes="32x32" href="' . $basePath . '/favicon-32x32.png">' . "\n"
. '<link rel="icon" type="image/png" sizes="16x16" href="' . $basePath . '/favicon-16x16.png">' . "\n"
. '<link rel="manifest" href="' . $basePath . '/site.webmanifest">' . "\n"
. '<link rel="shortcut icon" href="' . $basePath . '/favicon.ico">' . "\n";
}
}

162
src/helper/minify.php Normal file
View File

@@ -0,0 +1,162 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* CSS/JS minifier — generates .min files from source when dev mode is off,
* deletes them when dev mode is on.
*/
defined('_JEXEC') or die;
class MokoMinifyHelper
{
/**
* Files to minify: source path relative to template media root.
* The .min variant is derived automatically (template.css → template.min.css).
*/
private const CSS_FILES = [
'css/template.css',
'css/theme/light.standard.css',
'css/theme/dark.standard.css',
'css/theme/light.custom.css',
'css/theme/dark.custom.css',
];
private const JS_FILES = [
'js/template.js',
];
/**
* When dev mode is ON: delete all .min files.
* When dev mode is OFF: regenerate .min files if source is newer.
*
* @param string $mediaRoot Absolute path to the template media directory.
* @param bool $devMode Whether development mode is enabled.
*/
public static function sync(string $mediaRoot, bool $devMode): void
{
$mediaRoot = rtrim($mediaRoot, '/\\');
foreach (self::CSS_FILES as $relPath) {
$source = $mediaRoot . '/' . $relPath;
$min = self::minPath($source);
if ($devMode) {
self::deleteIfExists($min);
} else {
self::buildIfStale($source, $min, 'css');
}
}
foreach (self::JS_FILES as $relPath) {
$source = $mediaRoot . '/' . $relPath;
$min = self::minPath($source);
if ($devMode) {
self::deleteIfExists($min);
} else {
self::buildIfStale($source, $min, 'js');
}
}
}
/**
* Derive the .min path from a source path.
* template.css → template.min.css
*/
private static function minPath(string $path): string
{
$info = pathinfo($path);
return $info['dirname'] . '/' . $info['filename'] . '.min.' . $info['extension'];
}
/**
* Delete a file if it exists.
*/
private static function deleteIfExists(string $path): void
{
if (is_file($path)) {
@unlink($path);
}
}
/**
* Build the minified file if the source is newer or the min file is missing.
*/
private static function buildIfStale(string $source, string $min, string $type): void
{
if (!is_file($source)) {
return;
}
// Skip if min file exists and is newer than source
if (is_file($min) && filemtime($min) >= filemtime($source)) {
return;
}
$content = file_get_contents($source);
if ($content === false) {
return;
}
$minified = ($type === 'css')
? self::minifyCss($content)
: self::minifyJs($content);
file_put_contents($min, $minified);
}
/**
* Minify CSS by stripping comments, excess whitespace, and unnecessary characters.
*/
private static function minifyCss(string $css): string
{
// Remove comments (but keep IE hacks like /*\*/)
$css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
// Remove whitespace around { } : ; , > + ~
$css = preg_replace('/\s*([{}:;,>+~])\s*/', '$1', $css);
// Remove remaining newlines and tabs
$css = preg_replace('/\s+/', ' ', $css);
// Remove spaces around selectors
$css = str_replace(['{ ', ' {', '; ', ' ;'], ['{', '{', ';', ';'], $css);
// Remove trailing semicolons before closing braces
$css = str_replace(';}', '}', $css);
// Remove leading/trailing whitespace
return trim($css);
}
/**
* Minify JS by stripping single-line comments, multi-line comments,
* and collapsing whitespace. Preserves string literals.
*/
private static function minifyJs(string $js): string
{
// Remove multi-line comments
$js = preg_replace('!/\*.*?\*/!s', '', $js);
// Remove single-line comments (but not URLs like http://)
$js = preg_replace('!(?<=^|[\s;{}()\[\]])//[^\n]*!m', '', $js);
// Collapse whitespace
$js = preg_replace('/\s+/', ' ', $js);
// Remove spaces around operators and punctuation
$js = preg_replace('/\s*([{}();,=+\-*\/<>!&|?:])\s*/', '$1', $js);
// Restore necessary spaces (after keywords)
$js = preg_replace('/(var|let|const|return|typeof|instanceof|new|delete|throw|case|in|of)([^\s;})><=!&|?:,])/', '$1 $2', $js);
return trim($js);
}
}

View File

@@ -1 +0,0 @@
<!DOCTYPE html><title></title>

View File

@@ -1,112 +0,0 @@
<?php
/**
* @package Community Builder
* @subpackage com_comprofiler
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for Community Builder login view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
$return = $this->return ?? '';
$showRegisterLink = $this->showRegisterLink ?? true;
$showLostPasswordLink = $this->showLostPasswordLink ?? true;
?>
<div class="cb-login-responsive cb-component">
<div class="cb-login__container">
<div class="cb-login__header">
<h1 class="cb-login__title">
<?php echo Text::_('COM_COMPROFILER_LOGIN'); ?>
</h1>
</div>
<form action="<?php echo Route::_('index.php?option=com_comprofiler&view=login'); ?>"
method="post"
class="cb-login__form"
aria-label="<?php echo Text::_('COM_COMPROFILER_LOGIN_FORM'); ?>">
<div class="cb-login__field">
<label for="cb-username" class="cb-login__label">
<?php echo Text::_('COM_COMPROFILER_USERNAME'); ?>
<span class="cb-login__required" aria-label="<?php echo Text::_('COM_COMPROFILER_REQUIRED'); ?>">*</span>
</label>
<input type="text"
id="cb-username"
name="username"
class="cb-login__input"
required
aria-required="true"
autocomplete="username"
placeholder="<?php echo Text::_('COM_COMPROFILER_USERNAME'); ?>">
</div>
<div class="cb-login__field">
<label for="cb-password" class="cb-login__label">
<?php echo Text::_('COM_COMPROFILER_PASSWORD'); ?>
<span class="cb-login__required" aria-label="<?php echo Text::_('COM_COMPROFILER_REQUIRED'); ?>">*</span>
</label>
<input type="password"
id="cb-password"
name="passwd"
class="cb-login__input"
required
aria-required="true"
autocomplete="current-password"
placeholder="<?php echo Text::_('COM_COMPROFILER_PASSWORD'); ?>">
</div>
<?php if ($this->showRememberMe ?? true) : ?>
<div class="cb-login__remember">
<div class="form-check">
<input type="checkbox"
id="cb-remember"
name="remember"
class="form-check-input cb-login__remember-checkbox"
value="yes">
<label for="cb-remember" class="form-check-label cb-login__remember-label">
<?php echo Text::_('COM_COMPROFILER_REMEMBER_ME'); ?>
</label>
</div>
</div>
<?php endif; ?>
<div class="cb-login__actions">
<button type="submit" class="cb-login__btn cb-login__btn--submit btn btn-primary">
<span class="icon-lock" aria-hidden="true"></span>
<?php echo Text::_('COM_COMPROFILER_LOGIN'); ?>
</button>
</div>
<input type="hidden" name="task" value="login">
<input type="hidden" name="return" value="<?php echo htmlspecialchars($return, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo $this->token ?? ''; ?>
</form>
<div class="cb-login__links">
<?php if ($showRegisterLink) : ?>
<div class="cb-login__link">
<a href="<?php echo Route::_('index.php?option=com_comprofiler&view=registers'); ?>" class="cb-login__link-item">
<span class="icon-user-plus" aria-hidden="true"></span>
<?php echo Text::_('COM_COMPROFILER_REGISTER_NEW_ACCOUNT'); ?>
</a>
</div>
<?php endif; ?>
<?php if ($showLostPasswordLink) : ?>
<div class="cb-login__link">
<a href="<?php echo Route::_('index.php?option=com_comprofiler&view=lostpassword'); ?>" class="cb-login__link-item">
<span class="icon-question" aria-hidden="true"></span>
<?php echo Text::_('COM_COMPROFILER_FORGOT_PASSWORD'); ?>
</a>
</div>
<?php endif; ?>
</div>
</div>
</div>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><title></title>

View File

@@ -1,136 +0,0 @@
<?php
/**
* @package Community Builder
* @subpackage com_comprofiler
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for Community Builder registration view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
// Get form and fields
$form = $this->form ?? null;
$tabs = $this->tabs ?? null;
?>
<div class="cb-register-responsive cb-component">
<div class="cb-register__header">
<h1 class="cb-register__title">
<?php echo Text::_('COM_COMPROFILER_REGISTER'); ?>
</h1>
<?php if ($this->introduction ?? null) : ?>
<div class="cb-register__intro">
<?php echo $this->introduction; ?>
</div>
<?php endif; ?>
</div>
<?php if ($form) : ?>
<form action="<?php echo $this->action ?? ''; ?>"
method="post"
class="cb-register__form"
enctype="multipart/form-data"
aria-label="<?php echo Text::_('COM_COMPROFILER_REGISTRATION_FORM'); ?>">
<?php if ($tabs) : ?>
<?php foreach ($tabs as $tab) : ?>
<?php if (isset($tab->fields) && !empty($tab->fields)) : ?>
<fieldset class="cb-register__fieldset">
<?php if ($tab->title) : ?>
<legend class="cb-register__legend">
<?php echo htmlspecialchars($tab->title, ENT_QUOTES, 'UTF-8'); ?>
</legend>
<?php endif; ?>
<?php if ($tab->description) : ?>
<div class="cb-register__tab-description">
<?php echo $tab->description; ?>
</div>
<?php endif; ?>
<div class="cb-register__fields">
<?php foreach ($tab->fields as $field) : ?>
<div class="cb-register__field<?php echo $field->required ? ' cb-register__field--required' : ''; ?>">
<label for="<?php echo htmlspecialchars($field->name, ENT_QUOTES, 'UTF-8'); ?>"
class="cb-register__label">
<?php echo htmlspecialchars($field->title, ENT_QUOTES, 'UTF-8'); ?>
<?php if ($field->required) : ?>
<span class="cb-register__required" aria-label="<?php echo Text::_('COM_COMPROFILER_REQUIRED'); ?>">*</span>
<?php endif; ?>
</label>
<?php if ($field->description) : ?>
<div class="cb-register__field-description" id="<?php echo htmlspecialchars($field->name, ENT_QUOTES, 'UTF-8'); ?>-desc">
<?php echo $field->description; ?>
</div>
<?php endif; ?>
<div class="cb-register__input-wrapper">
<?php echo $field->input; ?>
</div>
<?php if (isset($field->error) && $field->error) : ?>
<div class="cb-register__error" role="alert">
<?php echo $field->error; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</fieldset>
<?php endif; ?>
<?php endforeach; ?>
<?php endif; ?>
<?php if ($this->showCaptcha ?? false) : ?>
<div class="cb-register__captcha">
<?php echo $this->captcha; ?>
</div>
<?php endif; ?>
<?php if ($this->showTerms ?? false) : ?>
<div class="cb-register__terms">
<div class="form-check">
<input type="checkbox"
id="cb-terms"
name="agreedToTerms"
class="form-check-input cb-register__terms-checkbox"
required
aria-required="true"
aria-describedby="cb-terms-text">
<label for="cb-terms" class="form-check-label cb-register__terms-label" id="cb-terms-text">
<?php echo Text::_('COM_COMPROFILER_AGREE_TO_TERMS'); ?>
<a href="<?php echo $this->termsUrl ?? ''; ?>" target="_blank" rel="noopener noreferrer">
<?php echo Text::_('COM_COMPROFILER_TERMS_CONDITIONS'); ?>
</a>
</label>
</div>
</div>
<?php endif; ?>
<div class="cb-register__actions">
<button type="submit" class="cb-register__btn cb-register__btn--submit btn btn-primary">
<span class="icon-check" aria-hidden="true"></span>
<?php echo Text::_('COM_COMPROFILER_REGISTER_SUBMIT'); ?>
</button>
<a href="<?php echo $this->loginUrl ?? ''; ?>" class="cb-register__btn cb-register__btn--cancel btn btn-secondary">
<span class="icon-cancel" aria-hidden="true"></span>
<?php echo Text::_('JCANCEL'); ?>
</a>
</div>
<?php echo $this->token ?? ''; ?>
</form>
<?php else : ?>
<div class="alert alert-warning" role="alert">
<?php echo Text::_('COM_COMPROFILER_REGISTRATION_NOT_AVAILABLE'); ?>
</div>
<?php endif; ?>
</div>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><title></title>

View File

@@ -1,100 +0,0 @@
<?php
/**
* @package Community Builder
* @subpackage com_comprofiler
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for Community Builder user profile view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
// Get user object
$user = $this->user ?? null;
$tabs = $this->tabs ?? null;
?>
<div class="cb-profile-responsive cb-component">
<?php if ($user) : ?>
<div class="cb-profile__header">
<?php if ($user->getField('avatar', null, 'html', 'none', 'profile')) : ?>
<div class="cb-profile__avatar">
<?php echo $user->getField('avatar', null, 'html', 'none', 'profile'); ?>
</div>
<?php endif; ?>
<div class="cb-profile__header-info">
<h1 class="cb-profile__name">
<?php echo htmlspecialchars($user->getField('formatname', null, 'html', 'none', 'profile'), ENT_QUOTES, 'UTF-8'); ?>
</h1>
<?php if ($user->getField('onlinestatus', null, 'html', 'none', 'profile')) : ?>
<div class="cb-profile__status">
<?php echo $user->getField('onlinestatus', null, 'html', 'none', 'profile'); ?>
</div>
<?php endif; ?>
</div>
</div>
<?php if ($tabs) : ?>
<div class="cb-profile__tabs">
<ul class="cb-profile__tabs-nav" role="tablist" aria-label="<?php echo Text::_('COM_COMPROFILER_PROFILE_TABS'); ?>">
<?php foreach ($tabs as $tab) : ?>
<?php if (isset($tab->fields) && !empty($tab->fields)) : ?>
<li class="cb-profile__tab-item" role="presentation">
<a href="#<?php echo htmlspecialchars($tab->id, ENT_QUOTES, 'UTF-8'); ?>"
class="cb-profile__tab-link"
role="tab"
aria-controls="<?php echo htmlspecialchars($tab->id, ENT_QUOTES, 'UTF-8'); ?>"
aria-selected="false">
<?php echo htmlspecialchars($tab->title, ENT_QUOTES, 'UTF-8'); ?>
</a>
</li>
<?php endif; ?>
<?php endforeach; ?>
</ul>
<div class="cb-profile__tabs-content">
<?php foreach ($tabs as $tab) : ?>
<?php if (isset($tab->fields) && !empty($tab->fields)) : ?>
<div id="<?php echo htmlspecialchars($tab->id, ENT_QUOTES, 'UTF-8'); ?>"
class="cb-profile__tab-pane"
role="tabpanel"
aria-labelledby="<?php echo htmlspecialchars($tab->id, ENT_QUOTES, 'UTF-8'); ?>-tab">
<?php if ($tab->description) : ?>
<div class="cb-profile__tab-description">
<?php echo $tab->description; ?>
</div>
<?php endif; ?>
<div class="cb-profile__fields">
<?php foreach ($tab->fields as $field) : ?>
<?php if ($field->value) : ?>
<div class="cb-profile__field">
<div class="cb-profile__field-label">
<?php echo htmlspecialchars($field->title, ENT_QUOTES, 'UTF-8'); ?>
</div>
<div class="cb-profile__field-value">
<?php echo $field->value; ?>
</div>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php else : ?>
<div class="alert alert-warning" role="alert">
<?php echo Text::_('COM_COMPROFILER_USER_NOT_FOUND'); ?>
</div>
<?php endif; ?>
</div>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><title></title>

View File

@@ -1,123 +0,0 @@
<?php
/**
* @package Community Builder
* @subpackage com_comprofiler
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for Community Builder users list view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
// Get users list
$users = $this->users ?? [];
$pagination = $this->pagination ?? null;
$search = $this->search ?? '';
$listid = $this->listid ?? 0;
?>
<div class="cb-userslist-responsive cb-component">
<div class="cb-userslist__header">
<h1 class="cb-userslist__title">
<?php echo Text::_('COM_COMPROFILER_USERLIST'); ?>
</h1>
<?php if ($this->showSearch ?? true) : ?>
<div class="cb-userslist__search">
<form action="<?php echo Route::_('index.php?option=com_comprofiler&view=userslist&listid=' . $listid); ?>"
method="post"
class="cb-userslist__search-form"
role="search"
aria-label="<?php echo Text::_('COM_COMPROFILER_SEARCH_USERS'); ?>">
<div class="cb-userslist__search-wrapper">
<label for="cb-userslist-search" class="visually-hidden">
<?php echo Text::_('COM_COMPROFILER_SEARCH'); ?>
</label>
<input type="search"
id="cb-userslist-search"
name="search"
class="cb-userslist__search-input"
placeholder="<?php echo Text::_('COM_COMPROFILER_SEARCH_USERS'); ?>"
value="<?php echo htmlspecialchars($search, ENT_QUOTES, 'UTF-8'); ?>"
aria-label="<?php echo Text::_('COM_COMPROFILER_SEARCH'); ?>">
<button type="submit"
class="cb-userslist__search-btn btn btn-primary"
aria-label="<?php echo Text::_('COM_COMPROFILER_SEARCH'); ?>">
<span class="icon-search" aria-hidden="true"></span>
<span class="cb-userslist__search-text"><?php echo Text::_('COM_COMPROFILER_SEARCH'); ?></span>
</button>
</div>
</form>
</div>
<?php endif; ?>
</div>
<?php if (!empty($users)) : ?>
<div class="cb-userslist__grid">
<?php foreach ($users as $user) : ?>
<div class="cb-userslist__user-card">
<?php if ($user->getField('avatar', null, 'html', 'none', 'list')) : ?>
<div class="cb-userslist__avatar">
<a href="<?php echo $user->getField('canvas', null, 'html', 'none', 'profile'); ?>"
aria-label="<?php echo Text::sprintf('COM_COMPROFILER_VIEW_PROFILE', htmlspecialchars($user->getField('formatname', null, 'html', 'none', 'list'), ENT_QUOTES, 'UTF-8')); ?>">
<?php echo $user->getField('avatar', null, 'html', 'none', 'list'); ?>
</a>
</div>
<?php endif; ?>
<div class="cb-userslist__user-info">
<h3 class="cb-userslist__username">
<a href="<?php echo $user->getField('canvas', null, 'html', 'none', 'profile'); ?>">
<?php echo htmlspecialchars($user->getField('formatname', null, 'html', 'none', 'list'), ENT_QUOTES, 'UTF-8'); ?>
</a>
</h3>
<?php if ($user->getField('onlinestatus', null, 'html', 'none', 'list')) : ?>
<div class="cb-userslist__status">
<?php echo $user->getField('onlinestatus', null, 'html', 'none', 'list'); ?>
</div>
<?php endif; ?>
<?php if (isset($user->fields) && !empty($user->fields)) : ?>
<div class="cb-userslist__fields">
<?php foreach ($user->fields as $field) : ?>
<?php if ($field->value) : ?>
<div class="cb-userslist__field">
<span class="cb-userslist__field-label"><?php echo htmlspecialchars($field->title, ENT_QUOTES, 'UTF-8'); ?>:</span>
<span class="cb-userslist__field-value"><?php echo $field->value; ?></span>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="cb-userslist__actions">
<a href="<?php echo $user->getField('canvas', null, 'html', 'none', 'profile'); ?>"
class="cb-userslist__btn btn btn-primary btn-sm">
<span class="icon-user" aria-hidden="true"></span>
<?php echo Text::_('COM_COMPROFILER_VIEW_PROFILE'); ?>
</a>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if ($pagination) : ?>
<div class="cb-userslist__pagination">
<?php echo $pagination->getPagesLinks(); ?>
</div>
<?php endif; ?>
<?php else : ?>
<div class="alert alert-info" role="alert">
<?php echo Text::_('COM_COMPROFILER_NO_USERS_FOUND'); ?>
</div>
<?php endif; ?>
</div>

View File

@@ -1 +1,76 @@
<!DOCTYPE html><title></title> <!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -1,17 +1,10 @@
<?php <?php
/** /**
* @package Joomla.Site * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @subpackage Templates.MokoCassiopeia
* *
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> * This file is part of a Moko Consulting project.
* @license GNU General Public License version 3 or later; see LICENSE.txt
* *
* FILE INFORMATION * SPDX-License-Identifier: GPL-3.0-or-later
* DEFGROUP: Joomla.Template.Site
* INGROUP: MokoCassiopeia
* PATH: ./templates/mokocassiopeia/html/com_content/article/toc-left.php
* VERSION: 03.06.02
* BRIEF: Article layout with table of contents on the left side using Bootstrap TOC
*/ */
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -19,6 +12,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Associations; use Joomla\CMS\Language\Associations;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper; use Joomla\CMS\Layout\LayoutHelper;
// Load Bootstrap TOC assets // Load Bootstrap TOC assets
@@ -43,13 +37,13 @@ $assocParam = (Associations::isEnabled() && $params->get('show_associations'));
<div class="col-lg-3 col-md-4 order-md-1 mb-4"> <div class="col-lg-3 col-md-4 order-md-1 mb-4">
<div class="sticky-top toc-wrapper" style="top: 20px;"> <div class="sticky-top toc-wrapper" style="top: 20px;">
<nav id="toc" data-toggle="toc" class="toc-container"> <nav id="toc" data-toggle="toc" class="toc-container">
<h5 class="toc-title"><?php echo HTMLHelper::_('string.truncate', $this->item->title, 50); ?></h5> <h5 class="toc-title"><?php echo Text::_('TPL_MOKOCASSIOPEIA_TOC_TITLE'); ?></h5>
</nav> </nav>
</div> </div>
</div> </div>
<!-- Article Content --> <!-- Article Content -->
<div class="col-lg-9 col-md-8 order-md-2" data-toc-scope> <div class="col-lg-9 col-md-8 order-md-2">
<meta itemprop="inLanguage" content="<?php echo ($this->item->language === '*') ? Factory::getApplication()->get('language') : $this->item->language; ?>" /> <meta itemprop="inLanguage" content="<?php echo ($this->item->language === '*') ? Factory::getApplication()->get('language') : $this->item->language; ?>" />
<?php if ($this->params->get('show_page_heading')) : ?> <?php if ($this->params->get('show_page_heading')) : ?>
@@ -91,7 +85,7 @@ $assocParam = (Associations::isEnabled() && $params->get('show_associations'));
<?php echo LayoutHelper::render('joomla.content.tags', $this->item->tags->itemTags); ?> <?php echo LayoutHelper::render('joomla.content.tags', $this->item->tags->itemTags); ?>
<?php endif; ?> <?php endif; ?>
<div class="article-content" itemprop="articleBody"> <div class="article-content" itemprop="articleBody" data-toc-scope>
<?php echo $this->item->text; ?> <?php echo $this->item->text; ?>
</div> </div>

View File

@@ -1,17 +1,10 @@
<?php <?php
/** /**
* @package Joomla.Site * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @subpackage Templates.MokoCassiopeia
* *
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> * This file is part of a Moko Consulting project.
* @license GNU General Public License version 3 or later; see LICENSE.txt
* *
* FILE INFORMATION * SPDX-License-Identifier: GPL-3.0-or-later
* DEFGROUP: Joomla.Template.Site
* INGROUP: MokoCassiopeia
* PATH: ./templates/mokocassiopeia/html/com_content/article/toc-right.php
* VERSION: 03.06.02
* BRIEF: Article layout with table of contents on the right side using Bootstrap TOC
*/ */
defined('_JEXEC') or die; defined('_JEXEC') or die;
@@ -19,6 +12,7 @@ defined('_JEXEC') or die;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Associations; use Joomla\CMS\Language\Associations;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper; use Joomla\CMS\Layout\LayoutHelper;
// Load Bootstrap TOC assets // Load Bootstrap TOC assets
@@ -40,7 +34,7 @@ $assocParam = (Associations::isEnabled() && $params->get('show_associations'));
<div class="com-content-article item-page<?php echo $this->pageclass_sfx; ?>"> <div class="com-content-article item-page<?php echo $this->pageclass_sfx; ?>">
<div class="row"> <div class="row">
<!-- Article Content --> <!-- Article Content -->
<div class="col-lg-9 col-md-8 order-md-1" data-toc-scope> <div class="col-lg-9 col-md-8 order-md-1">
<meta itemprop="inLanguage" content="<?php echo ($this->item->language === '*') ? Factory::getApplication()->get('language') : $this->item->language; ?>" /> <meta itemprop="inLanguage" content="<?php echo ($this->item->language === '*') ? Factory::getApplication()->get('language') : $this->item->language; ?>" />
<?php if ($this->params->get('show_page_heading')) : ?> <?php if ($this->params->get('show_page_heading')) : ?>
@@ -82,7 +76,7 @@ $assocParam = (Associations::isEnabled() && $params->get('show_associations'));
<?php echo LayoutHelper::render('joomla.content.tags', $this->item->tags->itemTags); ?> <?php echo LayoutHelper::render('joomla.content.tags', $this->item->tags->itemTags); ?>
<?php endif; ?> <?php endif; ?>
<div class="article-content" itemprop="articleBody"> <div class="article-content" itemprop="articleBody" data-toc-scope>
<?php echo $this->item->text; ?> <?php echo $this->item->text; ?>
</div> </div>
@@ -108,7 +102,7 @@ $assocParam = (Associations::isEnabled() && $params->get('show_associations'));
<div class="col-lg-3 col-md-4 order-md-2 mb-4"> <div class="col-lg-3 col-md-4 order-md-2 mb-4">
<div class="sticky-top toc-wrapper" style="top: 20px;"> <div class="sticky-top toc-wrapper" style="top: 20px;">
<nav id="toc" data-toggle="toc" class="toc-container"> <nav id="toc" data-toggle="toc" class="toc-container">
<h5 class="toc-title"><?php echo HTMLHelper::_('string.truncate', $this->item->title, 50); ?></h5> <h5 class="toc-title"><?php echo Text::_('TPL_MOKOCASSIOPEIA_TOC_TITLE'); ?></h5>
</nav> </nav>
</div> </div>
</div> </div>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><title></title>

View File

@@ -1,167 +0,0 @@
<?php
/**
* @package JEM
* @subpackage com_jem
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for JEM calendar view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\HTML\HTMLHelper;
$events = $this->rows ?? [];
$date = $this->date ?? null;
$year = $this->year ?? date('Y');
$month = $this->month ?? date('m');
?>
<div class="jem-calendar-responsive jem-component">
<div class="jem-calendar__container">
<!-- Calendar Header -->
<div class="jem-calendar__header">
<h1 class="jem-calendar__title">
<?php echo Text::_('COM_JEM_CALENDAR'); ?>
</h1>
</div>
<!-- Calendar Navigation -->
<div class="jem-calendar__navigation">
<a href="<?php echo Route::_('index.php?option=com_jem&view=calendar&year=' . ($month == 1 ? $year - 1 : $year) . '&month=' . ($month == 1 ? 12 : $month - 1)); ?>"
class="jem-calendar__nav-button jem-calendar__nav-prev"
aria-label="<?php echo Text::_('COM_JEM_PREVIOUS_MONTH'); ?>">
<span aria-hidden="true">&#8249;</span>
</a>
<h2 class="jem-calendar__current-month">
<?php echo HTMLHelper::_('date', $year . '-' . $month . '-01', 'F Y'); ?>
</h2>
<a href="<?php echo Route::_('index.php?option=com_jem&view=calendar&year=' . ($month == 12 ? $year + 1 : $year) . '&month=' . ($month == 12 ? 1 : $month + 1)); ?>"
class="jem-calendar__nav-button jem-calendar__nav-next"
aria-label="<?php echo Text::_('COM_JEM_NEXT_MONTH'); ?>">
<span aria-hidden="true">&#8250;</span>
</a>
</div>
<!-- Calendar Grid -->
<div class="jem-calendar__grid">
<!-- Weekday Headers -->
<div class="jem-calendar__weekdays">
<?php
$weekDays = [
Text::_('SUN'),
Text::_('MON'),
Text::_('TUE'),
Text::_('WED'),
Text::_('THU'),
Text::_('FRI'),
Text::_('SAT')
];
foreach ($weekDays as $day) : ?>
<div class="jem-calendar__weekday">
<?php echo $day; ?>
</div>
<?php endforeach; ?>
</div>
<!-- Calendar Days -->
<div class="jem-calendar__days">
<?php
// Generate calendar days
$firstDay = mktime(0, 0, 0, $month, 1, $year);
$daysInMonth = date('t', $firstDay);
$dayOfWeek = date('w', $firstDay);
// Empty cells before first day
for ($i = 0; $i < $dayOfWeek; $i++) : ?>
<div class="jem-calendar__day jem-calendar__day--empty"></div>
<?php endfor;
// Days with events
for ($day = 1; $day <= $daysInMonth; $day++) :
$currentDate = sprintf('%04d-%02d-%02d', $year, $month, $day);
$hasEvents = false;
$dayEvents = [];
// Check for events on this day
if (!empty($events)) {
foreach ($events as $event) {
if (!empty($event->dates) && date('Y-m-d', strtotime($event->dates)) == $currentDate) {
$hasEvents = true;
$dayEvents[] = $event;
}
}
}
$isToday = ($currentDate == date('Y-m-d'));
$classes = 'jem-calendar__day';
if ($hasEvents) {
$classes .= ' jem-calendar__day--has-events';
}
if ($isToday) {
$classes .= ' jem-calendar__day--today';
}
?>
<div class="<?php echo $classes; ?>" data-date="<?php echo $currentDate; ?>">
<div class="jem-calendar__day-number">
<?php echo $day; ?>
</div>
<?php if ($hasEvents) : ?>
<div class="jem-calendar__day-events">
<span class="jem-calendar__event-indicator"
aria-label="<?php echo Text::sprintf('COM_JEM_EVENTS_COUNT', count($dayEvents)); ?>">
<?php echo count($dayEvents); ?>
</span>
</div>
<?php endif; ?>
</div>
<?php endfor; ?>
</div>
</div>
<!-- Events List for Selected/Current Day -->
<?php if (!empty($events)) : ?>
<div class="jem-calendar__events-list">
<h3 class="jem-calendar__events-title">
<?php echo Text::_('COM_JEM_UPCOMING_EVENTS'); ?>
</h3>
<div class="jem-calendar__events">
<?php foreach ($events as $event) : ?>
<div class="jem-calendar__event-item">
<div class="jem-calendar__event-date">
<?php if (!empty($event->dates)) : ?>
<time datetime="<?php echo $this->escape($event->dates); ?>">
<?php echo HTMLHelper::_('date', $event->dates, Text::_('DATE_FORMAT_LC4')); ?>
</time>
<?php endif; ?>
</div>
<h4 class="jem-calendar__event-title">
<?php if (!empty($event->slug)) : ?>
<a href="<?php echo Route::_('index.php?option=com_jem&view=event&id=' . $event->slug); ?>"
class="jem-calendar__event-link">
<?php echo $this->escape($event->title); ?>
</a>
<?php else : ?>
<?php echo $this->escape($event->title); ?>
<?php endif; ?>
</h4>
<?php if (!empty($event->venue)) : ?>
<div class="jem-calendar__event-venue">
📍 <?php echo $this->escape($event->venue); ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
</div>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><title></title>

View File

@@ -1,111 +0,0 @@
<?php
/**
* @package JEM
* @subpackage com_jem
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for JEM categories view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
$categories = $this->categories ?? [];
?>
<div class="jem-categories-responsive jem-component">
<div class="jem-categories__container">
<!-- Categories Header -->
<div class="jem-categories__header">
<h1 class="jem-categories__title">
<?php echo Text::_('COM_JEM_CATEGORIES'); ?>
</h1>
</div>
<?php if (!empty($categories)) : ?>
<div class="jem-categories__list">
<?php foreach ($categories as $category) : ?>
<div class="jem-categories__item">
<div class="jem-categories__item-inner">
<!-- Category Image -->
<?php if (!empty($category->image)) : ?>
<div class="jem-categories__image-wrapper">
<img src="<?php echo $this->escape($category->image); ?>"
alt="<?php echo $this->escape($category->catname); ?>"
class="jem-categories__image"
loading="lazy">
</div>
<?php endif; ?>
<!-- Category Content -->
<div class="jem-categories__content">
<!-- Category Title -->
<h2 class="jem-categories__category-title">
<?php if (!empty($category->slug)) : ?>
<a href="<?php echo Route::_('index.php?option=com_jem&view=category&id=' . $category->slug); ?>"
class="jem-categories__link">
<?php echo $this->escape($category->catname); ?>
</a>
<?php else : ?>
<?php echo $this->escape($category->catname); ?>
<?php endif; ?>
</h2>
<!-- Category Description -->
<?php if (!empty($category->catdescription)) : ?>
<div class="jem-categories__description">
<?php echo $category->catdescription; ?>
</div>
<?php endif; ?>
<!-- Event Count -->
<?php if (isset($category->eventcount)) : ?>
<div class="jem-categories__meta">
<span class="jem-categories__event-count">
<?php echo Text::sprintf('COM_JEM_EVENTS_COUNT_FULL', (int) $category->eventcount); ?>
</span>
</div>
<?php endif; ?>
<!-- View Category Button -->
<?php if (!empty($category->slug)) : ?>
<div class="jem-categories__actions">
<a href="<?php echo Route::_('index.php?option=com_jem&view=category&id=' . $category->slug); ?>"
class="jem-categories__button btn btn-primary"
aria-label="<?php echo Text::sprintf('COM_JEM_VIEW_CATEGORY', $this->escape($category->catname)); ?>">
<?php echo Text::_('COM_JEM_VIEW_CATEGORY'); ?>
</a>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Pagination -->
<?php if (!empty($this->pagination)) : ?>
<div class="jem-categories__pagination">
<?php echo $this->pagination->getPagesLinks(); ?>
</div>
<?php endif; ?>
<?php else : ?>
<div class="jem-categories__empty">
<p class="jem-categories__empty-message">
<?php echo Text::_('COM_JEM_NO_CATEGORIES'); ?>
</p>
</div>
<?php endif; ?>
</div>
</div>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><title></title>

View File

@@ -1,212 +0,0 @@
<?php
/**
* @package JEM
* @subpackage com_jem
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for JEM event details view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\HTML\HTMLHelper;
$item = $this->item ?? null;
$params = $this->params ?? null;
if (!$item) {
return;
}
?>
<div class="jem-event-responsive jem-component">
<div class="jem-event__container">
<!-- Event Header -->
<div class="jem-event__header">
<h1 class="jem-event__title">
<?php echo $this->escape($item->title); ?>
</h1>
</div>
<!-- Event Image -->
<?php if (!empty($item->datimage)) : ?>
<div class="jem-event__image-wrapper">
<img src="<?php echo $this->escape($item->datimage); ?>"
alt="<?php echo $this->escape($item->title); ?>"
class="jem-event__image"
loading="lazy">
</div>
<?php endif; ?>
<!-- Event Meta Information -->
<div class="jem-event__meta">
<!-- Date and Time -->
<div class="jem-event__meta-item jem-event__date">
<span class="jem-event__meta-icon" aria-hidden="true">📅</span>
<div class="jem-event__meta-content">
<strong class="jem-event__meta-label">
<?php echo Text::_('COM_JEM_DATE'); ?>:
</strong>
<?php if (!empty($item->dates)) : ?>
<time datetime="<?php echo $this->escape($item->dates); ?>"
class="jem-event__datetime">
<?php echo HTMLHelper::_('date', $item->dates, Text::_('DATE_FORMAT_LC3')); ?>
</time>
<?php endif; ?>
<?php if (!empty($item->enddates) && $item->enddates != $item->dates) : ?>
<span class="jem-event__date-separator"> - </span>
<time datetime="<?php echo $this->escape($item->enddates); ?>"
class="jem-event__datetime">
<?php echo HTMLHelper::_('date', $item->enddates, Text::_('DATE_FORMAT_LC3')); ?>
</time>
<?php endif; ?>
</div>
</div>
<!-- Time -->
<?php if (!empty($item->times)) : ?>
<div class="jem-event__meta-item jem-event__time">
<span class="jem-event__meta-icon" aria-hidden="true">🕐</span>
<div class="jem-event__meta-content">
<strong class="jem-event__meta-label">
<?php echo Text::_('COM_JEM_TIME'); ?>:
</strong>
<span class="jem-event__time-value">
<?php echo $this->escape($item->times); ?>
<?php if (!empty($item->endtimes)) : ?>
- <?php echo $this->escape($item->endtimes); ?>
<?php endif; ?>
</span>
</div>
</div>
<?php endif; ?>
<!-- Venue -->
<?php if (!empty($item->venue)) : ?>
<div class="jem-event__meta-item jem-event__venue">
<span class="jem-event__meta-icon" aria-hidden="true">📍</span>
<div class="jem-event__meta-content">
<strong class="jem-event__meta-label">
<?php echo Text::_('COM_JEM_VENUE'); ?>:
</strong>
<?php if (!empty($item->venueslug)) : ?>
<a href="<?php echo Route::_('index.php?option=com_jem&view=venue&id=' . $item->venueslug); ?>"
class="jem-event__venue-link">
<?php echo $this->escape($item->venue); ?>
</a>
<?php else : ?>
<span class="jem-event__venue-name">
<?php echo $this->escape($item->venue); ?>
</span>
<?php endif; ?>
<?php if (!empty($item->street) || !empty($item->city)) : ?>
<div class="jem-event__address">
<?php if (!empty($item->street)) : ?>
<span class="jem-event__street">
<?php echo $this->escape($item->street); ?>
</span>
<?php endif; ?>
<?php if (!empty($item->city)) : ?>
<span class="jem-event__city">
<?php echo $this->escape($item->city); ?>
</span>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<!-- Categories -->
<?php if (!empty($item->categories)) : ?>
<div class="jem-event__meta-item jem-event__categories">
<span class="jem-event__meta-icon" aria-hidden="true">🏷️</span>
<div class="jem-event__meta-content">
<strong class="jem-event__meta-label">
<?php echo Text::_('COM_JEM_CATEGORIES'); ?>:
</strong>
<div class="jem-event__category-list">
<?php foreach ($item->categories as $category) : ?>
<span class="jem-event__category-badge">
<?php echo $this->escape($category->catname); ?>
</span>
<?php endforeach; ?>
</div>
</div>
</div>
<?php endif; ?>
</div>
<!-- Event Description -->
<?php if (!empty($item->fulltext)) : ?>
<div class="jem-event__description">
<h2 class="jem-event__description-title">
<?php echo Text::_('COM_JEM_DESCRIPTION'); ?>
</h2>
<div class="jem-event__description-content">
<?php echo $item->fulltext; ?>
</div>
</div>
<?php endif; ?>
<!-- Event Registration -->
<?php if (!empty($item->registra) && $item->registra == 1) : ?>
<div class="jem-event__registration">
<h2 class="jem-event__registration-title">
<?php echo Text::_('COM_JEM_REGISTRATION'); ?>
</h2>
<?php if (!empty($item->maxplaces)) : ?>
<p class="jem-event__capacity">
<strong><?php echo Text::_('COM_JEM_MAX_PLACES'); ?>:</strong>
<?php echo (int) $item->maxplaces; ?>
</p>
<?php endif; ?>
<?php if (!empty($item->waitinglist)) : ?>
<p class="jem-event__waitinglist">
<?php echo Text::_('COM_JEM_WAITING_LIST_ENABLED'); ?>
</p>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Event Contact -->
<?php if (!empty($item->contactname)) : ?>
<div class="jem-event__contact">
<h2 class="jem-event__contact-title">
<?php echo Text::_('COM_JEM_CONTACT'); ?>
</h2>
<p class="jem-event__contact-info">
<strong><?php echo Text::_('COM_JEM_NAME'); ?>:</strong>
<?php echo $this->escape($item->contactname); ?>
</p>
<?php if (!empty($item->contactemail)) : ?>
<p class="jem-event__contact-info">
<strong><?php echo Text::_('COM_JEM_EMAIL'); ?>:</strong>
<a href="mailto:<?php echo $this->escape($item->contactemail); ?>"
class="jem-event__contact-link">
<?php echo $this->escape($item->contactemail); ?>
</a>
</p>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Back Button -->
<div class="jem-event__actions">
<a href="<?php echo Route::_('index.php?option=com_jem&view=eventslist'); ?>"
class="jem-event__button btn btn-secondary">
<?php echo Text::_('COM_JEM_BACK_TO_EVENTS'); ?>
</a>
</div>
</div>
</div>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><title></title>

View File

@@ -1,147 +0,0 @@
<?php
/**
* @package JEM
* @subpackage com_jem
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for JEM events list view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\HTML\HTMLHelper;
// Load JEM helper if available
if (file_exists(JPATH_SITE . '/components/com_jem/helpers/helper.php')) {
require_once JPATH_SITE . '/components/com_jem/helpers/helper.php';
}
$items = $this->items ?? [];
$params = $this->params ?? null;
?>
<div class="jem-eventslist-responsive jem-component">
<div class="jem-eventslist__container">
<?php if (!empty($this->pageheading)) : ?>
<div class="jem-eventslist__header">
<h1 class="jem-eventslist__title">
<?php echo $this->escape($this->pageheading); ?>
</h1>
</div>
<?php endif; ?>
<?php if (!empty($items)) : ?>
<div class="jem-eventslist__list">
<?php foreach ($items as $item) : ?>
<div class="jem-eventslist__item">
<div class="jem-eventslist__item-inner">
<!-- Event Date -->
<div class="jem-eventslist__date">
<?php if (!empty($item->dates)) : ?>
<time datetime="<?php echo $this->escape($item->dates); ?>"
class="jem-eventslist__datetime">
<?php echo HTMLHelper::_('date', $item->dates, Text::_('DATE_FORMAT_LC4')); ?>
</time>
<?php endif; ?>
<?php if (!empty($item->enddates) && $item->enddates != $item->dates) : ?>
<span class="jem-eventslist__date-separator"> - </span>
<time datetime="<?php echo $this->escape($item->enddates); ?>"
class="jem-eventslist__datetime">
<?php echo HTMLHelper::_('date', $item->enddates, Text::_('DATE_FORMAT_LC4')); ?>
</time>
<?php endif; ?>
</div>
<!-- Event Title -->
<h2 class="jem-eventslist__event-title">
<?php if (!empty($item->slug)) : ?>
<a href="<?php echo Route::_('index.php?option=com_jem&view=event&id=' . $item->slug); ?>"
class="jem-eventslist__link">
<?php echo $this->escape($item->title); ?>
</a>
<?php else : ?>
<?php echo $this->escape($item->title); ?>
<?php endif; ?>
</h2>
<!-- Event Venue -->
<?php if (!empty($item->venue)) : ?>
<div class="jem-eventslist__venue">
<span class="jem-eventslist__venue-icon" aria-hidden="true">📍</span>
<?php if (!empty($item->venueslug)) : ?>
<a href="<?php echo Route::_('index.php?option=com_jem&view=venue&id=' . $item->venueslug); ?>"
class="jem-eventslist__venue-link">
<?php echo $this->escape($item->venue); ?>
</a>
<?php else : ?>
<span class="jem-eventslist__venue-name">
<?php echo $this->escape($item->venue); ?>
</span>
<?php endif; ?>
<?php if (!empty($item->city)) : ?>
<span class="jem-eventslist__city">
, <?php echo $this->escape($item->city); ?>
</span>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Event Description -->
<?php if (!empty($item->introtext)) : ?>
<div class="jem-eventslist__description">
<?php echo $item->introtext; ?>
</div>
<?php endif; ?>
<!-- Event Categories -->
<?php if (!empty($item->categories)) : ?>
<div class="jem-eventslist__categories">
<?php foreach ($item->categories as $category) : ?>
<span class="jem-eventslist__category-badge">
<?php echo $this->escape($category->catname); ?>
</span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Read More Button -->
<?php if (!empty($item->slug)) : ?>
<div class="jem-eventslist__actions">
<a href="<?php echo Route::_('index.php?option=com_jem&view=event&id=' . $item->slug); ?>"
class="jem-eventslist__button btn btn-primary"
aria-label="<?php echo Text::sprintf('COM_JEM_READ_MORE_ABOUT', $this->escape($item->title)); ?>">
<?php echo Text::_('COM_JEM_READ_MORE'); ?>
</a>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Pagination -->
<?php if (!empty($this->pagination)) : ?>
<div class="jem-eventslist__pagination">
<?php echo $this->pagination->getPagesLinks(); ?>
</div>
<?php endif; ?>
<?php else : ?>
<div class="jem-eventslist__empty">
<p class="jem-eventslist__empty-message">
<?php echo Text::_('COM_JEM_NO_EVENTS'); ?>
</p>
</div>
<?php endif; ?>
</div>
</div>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><title></title>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><title></title>

View File

@@ -1,188 +0,0 @@
<?php
/**
* @package JEM
* @subpackage com_jem
*
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for JEM venue view
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\HTML\HTMLHelper;
$venue = $this->venue ?? null;
$events = $this->rows ?? [];
if (!$venue) {
return;
}
?>
<div class="jem-venue-responsive jem-component">
<div class="jem-venue__container">
<!-- Venue Header -->
<div class="jem-venue__header">
<h1 class="jem-venue__title">
<?php echo $this->escape($venue->venue); ?>
</h1>
</div>
<!-- Venue Image -->
<?php if (!empty($venue->locimage)) : ?>
<div class="jem-venue__image-wrapper">
<img src="<?php echo $this->escape($venue->locimage); ?>"
alt="<?php echo $this->escape($venue->venue); ?>"
class="jem-venue__image"
loading="lazy">
</div>
<?php endif; ?>
<!-- Venue Information -->
<div class="jem-venue__info">
<!-- Address -->
<?php if (!empty($venue->street) || !empty($venue->city) || !empty($venue->postalCode)) : ?>
<div class="jem-venue__info-item jem-venue__address">
<span class="jem-venue__info-icon" aria-hidden="true">📍</span>
<div class="jem-venue__info-content">
<strong class="jem-venue__info-label">
<?php echo Text::_('COM_JEM_ADDRESS'); ?>:
</strong>
<address class="jem-venue__address-content">
<?php if (!empty($venue->street)) : ?>
<div class="jem-venue__street">
<?php echo $this->escape($venue->street); ?>
</div>
<?php endif; ?>
<?php if (!empty($venue->postalCode) || !empty($venue->city)) : ?>
<div class="jem-venue__city-line">
<?php if (!empty($venue->postalCode)) : ?>
<span class="jem-venue__postal">
<?php echo $this->escape($venue->postalCode); ?>
</span>
<?php endif; ?>
<?php if (!empty($venue->city)) : ?>
<span class="jem-venue__city">
<?php echo $this->escape($venue->city); ?>
</span>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (!empty($venue->state)) : ?>
<div class="jem-venue__state">
<?php echo $this->escape($venue->state); ?>
</div>
<?php endif; ?>
<?php if (!empty($venue->country)) : ?>
<div class="jem-venue__country">
<?php echo $this->escape($venue->country); ?>
</div>
<?php endif; ?>
</address>
</div>
</div>
<?php endif; ?>
<!-- Website -->
<?php if (!empty($venue->url)) : ?>
<div class="jem-venue__info-item jem-venue__website">
<span class="jem-venue__info-icon" aria-hidden="true">🌐</span>
<div class="jem-venue__info-content">
<strong class="jem-venue__info-label">
<?php echo Text::_('COM_JEM_WEBSITE'); ?>:
</strong>
<a href="<?php echo $this->escape($venue->url); ?>"
target="_blank"
rel="noopener noreferrer"
class="jem-venue__link">
<?php echo $this->escape($venue->url); ?>
</a>
</div>
</div>
<?php endif; ?>
<!-- Description -->
<?php if (!empty($venue->locdescription)) : ?>
<div class="jem-venue__description">
<h2 class="jem-venue__description-title">
<?php echo Text::_('COM_JEM_DESCRIPTION'); ?>
</h2>
<div class="jem-venue__description-content">
<?php echo $venue->locdescription; ?>
</div>
</div>
<?php endif; ?>
</div>
<!-- Map -->
<?php if (!empty($venue->latitude) && !empty($venue->longitude)) : ?>
<div class="jem-venue__map">
<h2 class="jem-venue__map-title">
<?php echo Text::_('COM_JEM_LOCATION'); ?>
</h2>
<div class="jem-venue__map-container">
<!-- Map would be rendered here by JEM's map functionality -->
<div class="jem-venue__map-placeholder">
<p><?php echo Text::_('COM_JEM_MAP_VIEW'); ?></p>
<p>
<a href="https://www.google.com/maps?q=<?php echo $venue->latitude; ?>,<?php echo $venue->longitude; ?>"
target="_blank"
rel="noopener noreferrer"
class="jem-venue__map-link btn btn-primary">
<?php echo Text::_('COM_JEM_VIEW_ON_MAP'); ?>
</a>
</p>
</div>
</div>
</div>
<?php endif; ?>
<!-- Events at this Venue -->
<?php if (!empty($events)) : ?>
<div class="jem-venue__events">
<h2 class="jem-venue__events-title">
<?php echo Text::_('COM_JEM_EVENTS_AT_VENUE'); ?>
</h2>
<div class="jem-venue__events-list">
<?php foreach ($events as $event) : ?>
<div class="jem-venue__event-item">
<div class="jem-venue__event-date">
<?php if (!empty($event->dates)) : ?>
<time datetime="<?php echo $this->escape($event->dates); ?>">
<?php echo HTMLHelper::_('date', $event->dates, Text::_('DATE_FORMAT_LC4')); ?>
</time>
<?php endif; ?>
</div>
<h3 class="jem-venue__event-title">
<?php if (!empty($event->slug)) : ?>
<a href="<?php echo Route::_('index.php?option=com_jem&view=event&id=' . $event->slug); ?>"
class="jem-venue__event-link">
<?php echo $this->escape($event->title); ?>
</a>
<?php else : ?>
<?php echo $this->escape($event->title); ?>
<?php endif; ?>
</h3>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- Back Button -->
<div class="jem-venue__actions">
<a href="<?php echo Route::_('index.php?option=com_jem&view=eventslist'); ?>"
class="jem-venue__button btn btn-secondary">
<?php echo Text::_('COM_JEM_BACK_TO_EVENTS'); ?>
</a>
</div>
</div>
</div>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><title></title></head><body></body></html>

View File

@@ -1,70 +0,0 @@
<?php
/**
* @package Kunena
* @subpackage com_kunena
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for Kunena category list
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
$this->document->addStyleDeclaration('
.kunena-category-list-responsive {
width: 100%;
}
.kunena-category-responsive {
background: var(--body-bg);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: 1rem;
margin-bottom: 1rem;
transition: all 0.2s;
}
.kunena-category-responsive:hover {
background: var(--secondary-bg);
border-color: var(--color-primary);
}
@media (max-width: 575.98px) {
.kunena-category-responsive {
padding: 0.75rem;
}
}
');
?>
<div class="kunena-category-list-responsive">
<?php if (!empty($this->categories)) : ?>
<?php foreach ($this->categories as $category) : ?>
<div class="kunena-category-responsive">
<h3 class="kunena-category__title">
<a href="<?php echo $category->getUrl(); ?>">
<?php echo $this->escape($category->name); ?>
</a>
</h3>
<?php if ($category->description) : ?>
<div class="kunena-category__description">
<?php echo $category->displayField('description'); ?>
</div>
<?php endif; ?>
<div class="kunena-category__meta">
<span><?php echo Text::_('COM_KUNENA_TOPICS'); ?>: <?php echo $category->numTopics; ?></span>
<span><?php echo Text::_('COM_KUNENA_POSTS'); ?>: <?php echo $category->numPosts; ?></span>
</div>
</div>
<?php endforeach; ?>
<?php else : ?>
<div class="alert alert-info">
<?php echo Text::_('COM_KUNENA_NO_CATEGORIES'); ?>
</div>
<?php endif; ?>
</div>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><title></title></head><body></body></html>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><title></title></head><body></body></html>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><title></title></head><body></body></html>

View File

@@ -1,141 +0,0 @@
<?php
/**
* @package OS Membership Pro
* @subpackage com_osmembership
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for OS Membership plans list
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
$this->document->addStyleDeclaration('
.osmembership-plans-responsive {
display: grid;
gap: 2rem;
grid-template-columns: 1fr;
}
.osmembership-plan-card {
background: var(--body-bg);
border: 2px solid var(--border-color);
border-radius: var(--border-radius);
padding: 2rem;
transition: all 0.3s;
display: flex;
flex-direction: column;
}
.osmembership-plan-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
border-color: var(--color-primary);
}
.osmembership-plan-card--featured {
border-color: var(--color-primary);
position: relative;
}
.osmembership-plan-card--featured::before {
content: "' . Text::_('OSM_POPULAR') . '";
position: absolute;
top: -12px;
right: 20px;
background: var(--color-primary);
color: white;
padding: 0.25rem 1rem;
border-radius: 1rem;
font-size: 0.875rem;
font-weight: 600;
}
@media (min-width: 768px) {
.osmembership-plans-responsive {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 992px) {
.osmembership-plans-responsive {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 1200px) {
.osmembership-plans-responsive.osmembership-plans--many {
grid-template-columns: repeat(4, 1fr);
}
}
');
?>
<div class="osmembership-plans-responsive <?php echo count($this->items) > 3 ? 'osmembership-plans--many' : ''; ?>">
<?php foreach ($this->items as $item) : ?>
<div class="osmembership-plan-card <?php echo $item->featured ? 'osmembership-plan-card--featured' : ''; ?>">
<?php if (!empty($item->image)) : ?>
<div class="osmembership-plan__image" style="margin-bottom: 1.5rem;">
<img src="<?php echo $item->image; ?>"
alt="<?php echo htmlspecialchars($item->title, ENT_COMPAT, 'UTF-8'); ?>"
style="width: 100%; height: auto; border-radius: var(--border-radius);" />
</div>
<?php endif; ?>
<h2 class="osmembership-plan__title" style="margin: 0 0 1rem 0; font-size: 1.75rem; font-weight: 700;">
<?php echo htmlspecialchars($item->title, ENT_COMPAT, 'UTF-8'); ?>
</h2>
<div class="osmembership-plan__pricing" style="margin-bottom: 1.5rem;">
<?php if ($item->price > 0) : ?>
<div style="font-size: 2.5rem; font-weight: 700; color: var(--color-primary); line-height: 1;">
<span style="font-size: 1.5rem; vertical-align: super;"><?php echo $this->config->currency_symbol; ?></span>
<?php echo number_format($item->price, 0); ?>
</div>
<?php if ($item->subscription_length > 0) : ?>
<div style="color: var(--gray-600); margin-top: 0.5rem;">
<?php echo Text::_('OSM_PER') . ' ' . $item->subscription_length . ' ' . Text::_('OSM_' . strtoupper($item->subscription_length_unit)); ?>
</div>
<?php endif; ?>
<?php else : ?>
<div style="font-size: 2.5rem; font-weight: 700; color: var(--success);">
<?php echo Text::_('OSM_FREE'); ?>
</div>
<?php endif; ?>
</div>
<?php if (!empty($item->short_description)) : ?>
<div class="osmembership-plan__description" style="margin-bottom: 1.5rem; color: var(--gray-600); line-height: 1.6;">
<?php echo $item->short_description; ?>
</div>
<?php endif; ?>
<?php if (!empty($item->features)) : ?>
<div class="osmembership-plan__features" style="flex: 1; margin-bottom: 1.5rem;">
<ul style="list-style: none; padding: 0; margin: 0;">
<?php foreach (explode("\n", $item->features) as $feature) : ?>
<?php if (trim($feature)) : ?>
<li style="padding: 0.5rem 0; display: flex; align-items: flex-start; gap: 0.5rem;">
<span class="icon-check" style="color: var(--success); flex-shrink: 0; margin-top: 0.25rem;"></span>
<span><?php echo htmlspecialchars(trim($feature), ENT_COMPAT, 'UTF-8'); ?></span>
</li>
<?php endif; ?>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<div class="osmembership-plan__actions">
<a href="<?php echo OSMembershipHelperRoute::getRegistrationRoute($item->id); ?>"
class="btn btn-primary"
style="width: 100%; min-height: 48px; display: inline-flex; align-items: center; justify-content: center; text-decoration: none;">
<?php echo Text::_('OSM_SUBSCRIBE_NOW'); ?>
<span class="icon-chevron-right" style="margin-left: 0.5rem;"></span>
</a>
</div>
</div>
<?php endforeach; ?>
</div>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><title></title></head><body></body></html>

View File

@@ -1,82 +0,0 @@
<?php
/**
* @package AcyMailing
* @subpackage mod_acymailing
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_acymailing module
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-acymailing mod-acymailing-responsive ' . $moduleclass_sfx;
?>
<div class="<?php echo $wrapperClass; ?>">
<?php if (!empty($formDisplay)) : ?>
<div class="mod-acymailing__form-container">
<?php if ($params->get('intro_text')) : ?>
<div class="mod-acymailing__intro">
<?php echo $params->get('intro_text'); ?>
</div>
<?php endif; ?>
<?php echo $formDisplay; ?>
<?php if ($params->get('outro_text')) : ?>
<div class="mod-acymailing__outro">
<?php echo $params->get('outro_text'); ?>
</div>
<?php endif; ?>
</div>
<?php else : ?>
<div class="mod-acymailing__empty">
<p><?php echo Text::_('MOD_ACYMAILING_NO_FORM'); ?></p>
</div>
<?php endif; ?>
</div>
<style>
/* Override AcyMailing inline styles for mobile responsiveness */
.mod-acymailing-responsive .acymailing_module input[type="email"],
.mod-acymailing-responsive .acymailing_module input[type="text"] {
min-height: 44px !important;
font-size: 1rem !important;
padding: 0.5rem 0.75rem !important;
border-radius: var(--border-radius, 0.375rem) !important;
border: 1px solid var(--input-border-color, #dee2e6) !important;
width: 100% !important;
box-sizing: border-box !important;
}
.mod-acymailing-responsive .acymailing_module button[type="submit"],
.mod-acymailing-responsive .acymailing_module input[type="submit"] {
min-height: 44px !important;
padding: 0.625rem 1rem !important;
font-size: 1rem !important;
border-radius: var(--border-radius, 0.375rem) !important;
cursor: pointer !important;
}
@media (max-width: 575.98px) {
.mod-acymailing-responsive .acymailing_module input[type="email"],
.mod-acymailing-responsive .acymailing_module input[type="text"] {
font-size: 16px !important;
min-height: 48px !important;
padding: 0.75rem 1rem !important;
}
.mod-acymailing-responsive .acymailing_module button[type="submit"],
.mod-acymailing-responsive .acymailing_module input[type="submit"] {
min-height: 48px !important;
width: 100% !important;
}
}
</style>

View File

@@ -0,0 +1,36 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_articles_archive.
* Adds showtitle support.
*/
defined('_JEXEC') or die;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-articles-archive<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-archive__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-articles-archive__list">
<?php foreach ($list as $item) : ?>
<li class="mod-articles-archive__item">
<a href="<?php echo $item->link; ?>"><?php echo $item->text; ?></a>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,44 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_articles_categories.
* Adds showtitle support.
*/
defined('_JEXEC') or die;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
$showDescription = $params->get('show_description', 0);
$numitems = $params->get('numitems', 0);
?>
<div class="mod-articles-categories<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-categories__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-articles-categories__list">
<?php foreach ($list as $item) : ?>
<li class="mod-articles-categories__item">
<a href="<?php echo $item->link; ?>"><?php echo $item->title; ?></a>
<?php if ($numitems) : ?>
<span class="mod-articles-categories__count">(<?php echo $item->numitems; ?>)</span>
<?php endif; ?>
<?php if ($showDescription && !empty($item->description)) : ?>
<p class="mod-articles-categories__description"><?php echo $item->description; ?></p>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,78 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_articles_category.
* Adds showtitle support and respects module settings.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
Factory::getApplication()->getLanguage()->load('mod_articles_category', JPATH_SITE);
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-articles-category<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-category__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-articles-category__list">
<?php foreach ($list as $item) : ?>
<li class="mod-articles-category__item" itemscope itemtype="https://schema.org/Article">
<?php if ($params->get('link_titles') == 1) : ?>
<a class="mod-articles-category__link" href="<?php echo $item->link; ?>" itemprop="url">
<span itemprop="name"><?php echo $item->title; ?></span>
</a>
<?php else : ?>
<span itemprop="name"><?php echo $item->title; ?></span>
<?php endif; ?>
<?php if ($item->displayHits) : ?>
<span class="mod-articles-category__hits">
(<?php echo $item->displayHits; ?>)
</span>
<?php endif; ?>
<?php if ($params->get('show_author', 0)) : ?>
<span class="mod-articles-category__author">
<?php echo $item->displayAuthorName; ?>
</span>
<?php endif; ?>
<?php if ($item->displayDate) : ?>
<time class="mod-articles-category__date" datetime="<?php echo HTMLHelper::_('date', $item->displayDate, 'c'); ?>" itemprop="datePublished">
<?php echo $item->displayDate; ?>
</time>
<?php endif; ?>
<?php if ($params->get('show_introtext', 0)) : ?>
<div class="mod-articles-category__intro" itemprop="description">
<?php echo $item->displayIntrotext; ?>
</div>
<?php endif; ?>
<?php if ($params->get('show_readmore', 0)) : ?>
<a class="mod-articles-category__readmore" href="<?php echo $item->link; ?>" itemprop="url">
<?php echo Text::_('MOD_ARTICLES_CATEGORY_READ_MORE_TITLE'); ?>
</a>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,40 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_articles_latest.
* Adds showtitle support and respects module settings.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-articles-latest<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-latest__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-articles-latest__list">
<?php foreach ($list as $item) : ?>
<li class="mod-articles-latest__item" itemscope itemtype="https://schema.org/Article">
<a href="<?php echo $item->link; ?>" itemprop="url">
<span itemprop="name"><?php echo $item->title; ?></span>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,58 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_articles_news (newsflash).
* Adds showtitle support with card-based layout.
*/
defined('_JEXEC') or die;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-articles-news newsflash<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-news__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php foreach ($list as $item) : ?>
<div class="mod-articles-news__item" itemscope itemtype="https://schema.org/Article">
<?php if ($params->get('item_title')) : ?>
<h4 class="mod-articles-news__item-title" itemprop="name">
<?php if ($item->link !== '' && $params->get('link_titles')) : ?>
<a href="<?php echo $item->link; ?>" itemprop="url"><?php echo $item->title; ?></a>
<?php else : ?>
<?php echo $item->title; ?>
<?php endif; ?>
</h4>
<?php endif; ?>
<?php if (!empty($item->afterDisplayTitle)) : ?>
<?php echo $item->afterDisplayTitle; ?>
<?php endif; ?>
<?php if ($params->get('show_introtext', 1)) : ?>
<div class="mod-articles-news__intro" itemprop="description">
<?php echo $item->introtext; ?>
</div>
<?php endif; ?>
<?php if (isset($item->readmore) && $item->readmore) : ?>
<a class="mod-articles-news__readmore" href="<?php echo $item->link; ?>" itemprop="url">
<?php echo $item->linkText; ?>
</a>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,38 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_articles_popular.
* Adds showtitle support and respects module settings.
*/
defined('_JEXEC') or die;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-articles-popular<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-articles-popular__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-articles-popular__list">
<?php foreach ($list as $item) : ?>
<li class="mod-articles-popular__item" itemscope itemtype="https://schema.org/Article">
<a href="<?php echo $item->link; ?>" itemprop="url">
<span itemprop="name"><?php echo $item->title; ?></span>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,52 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_banners.
* Adds showtitle support.
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-banners<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-banners__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php foreach ($list as $item) : ?>
<div class="mod-banners__item">
<?php $link = $item->params->get('url') ?: ''; ?>
<?php if ($item->type == 1) : ?>
<?php // Image banner ?>
<?php $imageUrl = $item->params->get('imageurl', ''); ?>
<?php $alt = htmlspecialchars($item->name, ENT_COMPAT, 'UTF-8'); ?>
<?php if ($link) : ?>
<a href="<?php echo htmlspecialchars($link, ENT_COMPAT, 'UTF-8'); ?>" target="_blank" rel="noopener noreferrer">
<img src="<?php echo htmlspecialchars($imageUrl, ENT_COMPAT, 'UTF-8'); ?>" alt="<?php echo $alt; ?>" class="mod-banners__image" loading="lazy" />
</a>
<?php else : ?>
<img src="<?php echo htmlspecialchars($imageUrl, ENT_COMPAT, 'UTF-8'); ?>" alt="<?php echo $alt; ?>" class="mod-banners__image" loading="lazy" />
<?php endif; ?>
<?php else : ?>
<?php // Custom HTML banner ?>
<?php echo $item->custombannercode; ?>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,48 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_breadcrumbs.
* Bootstrap 5 breadcrumb with schema.org BreadcrumbList markup.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
Factory::getApplication()->getLanguage()->load('mod_breadcrumbs', JPATH_SITE);
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<nav class="mod-breadcrumbs<?php echo $suffix ? ' ' . $suffix : ''; ?>" aria-label="<?php echo Text::_('MOD_BREADCRUMBS_HERE'); ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-breadcrumbs__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ol class="breadcrumb" itemscope itemtype="https://schema.org/BreadcrumbList">
<?php foreach ($list as $key => $item) : ?>
<?php
$isLast = ($key === array_key_last($list));
?>
<li class="breadcrumb-item<?php echo $isLast ? ' active' : ''; ?>" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"
<?php echo $isLast ? ' aria-current="page"' : ''; ?>>
<?php if (!$isLast && $item->link) : ?>
<a href="<?php echo $item->link; ?>" itemprop="item">
<span itemprop="name"><?php echo $item->name; ?></span>
</a>
<?php else : ?>
<span itemprop="name"><?php echo $item->name; ?></span>
<?php endif; ?>
<meta itemprop="position" content="<?php echo $key + 1; ?>" />
</li>
<?php endforeach; ?>
</ol>
</nav>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><title></title></head><body></body></html>

View File

@@ -1,164 +0,0 @@
<?php
/**
* @package Community Builder
* @subpackage mod_cblogin
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_cblogin module
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
// Ensure module language file is loaded
$lang = Factory::getLanguage();
$lang->load('mod_cblogin', JPATH_SITE);
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-cblogin mod-cblogin-responsive ' . $moduleclass_sfx;
?>
<div class="<?php echo $wrapperClass; ?>">
<?php if ($type === 'logout') : ?>
<div class="mod-cblogin__logout">
<?php if ($params->get('greeting', 1)) : ?>
<div class="mod-cblogin__greeting">
<?php if ($cbUser) : ?>
<div class="mod-cblogin__avatar">
<?php echo $cbUser->getField('avatar', null, 'html', 'none', 'list'); ?>
</div>
<div class="mod-cblogin__user-info">
<span class="mod-cblogin__username">
<?php echo htmlspecialchars($cbUser->getField('formatname', null, 'html', 'none', 'list'), ENT_COMPAT, 'UTF-8'); ?>
</span>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<form action="<?php echo $action; ?>" method="post" class="mod-cblogin__form">
<div class="mod-cblogin__actions">
<?php if ($params->get('profileLink', 1) && $cbUser) : ?>
<a href="<?php echo $cbUser->getField('canvas', null, 'html', 'none', 'profile'); ?>" class="mod-cblogin__btn mod-cblogin__btn--profile btn btn-secondary">
<span class="icon-user" aria-hidden="true"></span>
<?php echo Text::_('MOD_CBLOGIN_PROFILE'); ?>
</a>
<?php endif; ?>
<button type="submit" name="Submit" class="mod-cblogin__btn mod-cblogin__btn--logout btn btn-primary">
<span class="icon-sign-out" aria-hidden="true"></span>
<?php echo Text::_('MOD_CBLOGIN_LOGOUT'); ?>
</button>
</div>
<input type="hidden" name="op2" value="logout" />
<input type="hidden" name="lang" value="<?php echo $lang; ?>" />
<input type="hidden" name="return" value="<?php echo $return; ?>" />
<?php echo $securityToken; ?>
</form>
</div>
<?php else : ?>
<form action="<?php echo $action; ?>" method="post" name="login<?php echo $moduleId; ?>" id="login<?php echo $moduleId; ?>" class="mod-cblogin__form mod-cblogin__form--login">
<?php if ($params->get('pretext')) : ?>
<div class="mod-cblogin__pretext">
<?php echo $params->get('pretext'); ?>
</div>
<?php endif; ?>
<div class="mod-cblogin__fields">
<div class="mod-cblogin__field">
<label for="modloginusername<?php echo $moduleId; ?>" class="mod-cblogin__label">
<?php echo Text::_('MOD_CBLOGIN_USERNAME'); ?>
</label>
<input
id="modloginusername<?php echo $moduleId; ?>"
type="text"
name="username"
class="mod-cblogin__input form-control"
placeholder="<?php echo Text::_('MOD_CBLOGIN_USERNAME'); ?>"
autocomplete="username"
required
/>
</div>
<div class="mod-cblogin__field">
<label for="modloginpass<?php echo $moduleId; ?>" class="mod-cblogin__label">
<?php echo Text::_('MOD_CBLOGIN_PASSWORD'); ?>
</label>
<input
id="modloginpass<?php echo $moduleId; ?>"
type="password"
name="passwd"
class="mod-cblogin__input form-control"
placeholder="<?php echo Text::_('MOD_CBLOGIN_PASSWORD'); ?>"
autocomplete="current-password"
required
/>
</div>
<?php if ($params->get('remember_me', 1)) : ?>
<div class="mod-cblogin__remember">
<input
id="modloginrememberme<?php echo $moduleId; ?>"
type="checkbox"
name="remember"
class="mod-cblogin__checkbox"
value="yes"
/>
<label for="modloginrememberme<?php echo $moduleId; ?>" class="mod-cblogin__remember-label">
<?php echo Text::_('MOD_CBLOGIN_REMEMBER_ME'); ?>
</label>
</div>
<?php endif; ?>
</div>
<div class="mod-cblogin__actions">
<button type="submit" name="Submit" class="mod-cblogin__btn mod-cblogin__btn--submit btn btn-primary">
<span class="icon-sign-in" aria-hidden="true"></span>
<?php echo Text::_('MOD_CBLOGIN_LOGIN'); ?>
</button>
</div>
<div class="mod-cblogin__links">
<?php if ($params->get('lostpassword_link', 1)) : ?>
<a href="<?php echo $lostPasswordLink; ?>" class="mod-cblogin__link">
<?php echo Text::_('MOD_CBLOGIN_FORGOT_PASSWORD'); ?>
<span class="icon-chevron-right" aria-hidden="true"></span>
</a>
<?php endif; ?>
<?php if ($params->get('lostusername_link', 1)) : ?>
<a href="<?php echo $lostUsernameLink; ?>" class="mod-cblogin__link">
<?php echo Text::_('MOD_CBLOGIN_FORGOT_USERNAME'); ?>
<span class="icon-chevron-right" aria-hidden="true"></span>
</a>
<?php endif; ?>
<?php if ($params->get('registration_link', 1)) : ?>
<a href="<?php echo $registrationLink; ?>" class="mod-cblogin__link">
<?php echo Text::_('MOD_CBLOGIN_REGISTER'); ?>
<span class="icon-chevron-right" aria-hidden="true"></span>
</a>
<?php endif; ?>
</div>
<?php if ($params->get('posttext')) : ?>
<div class="mod-cblogin__posttext">
<?php echo $params->get('posttext'); ?>
</div>
<?php endif; ?>
<input type="hidden" name="op2" value="login" />
<input type="hidden" name="lang" value="<?php echo $lang; ?>" />
<input type="hidden" name="return" value="<?php echo $return; ?>" />
<input type="hidden" name="message" value="0" />
<input type="hidden" name="loginfrom" value="loginmodule" />
<?php echo $securityToken; ?>
</form>
<?php endif; ?>
</div>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><title></title></head><body></body></html>

View File

@@ -1,99 +0,0 @@
<?php
/**
* @package Community Builder
* @subpackage mod_comprofilerOnline
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_comprofilerOnline module
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
// Ensure module language file is loaded
$lang = Factory::getLanguage();
$lang->load('mod_comprofilerOnline', JPATH_SITE);
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-cb-online mod-cb-online-responsive ' . $moduleclass_sfx;
?>
<div class="<?php echo $wrapperClass; ?>">
<?php if (!empty($onlineUsers)) : ?>
<div class="mod-cb-online__stats">
<div class="mod-cb-online__count">
<span class="mod-cb-online__count-number"><?php echo $totalOnline; ?></span>
<span class="mod-cb-online__count-label">
<?php echo $totalOnline == 1 ? Text::_('MOD_CB_ONLINE_USER') : Text::_('MOD_CB_ONLINE_USERS'); ?>
</span>
</div>
<?php if ($params->get('show_guest_count', 1)) : ?>
<div class="mod-cb-online__breakdown">
<span class="mod-cb-online__breakdown-item">
<span class="icon-users" aria-hidden="true"></span>
<?php echo $membersOnline; ?> <?php echo Text::_('MOD_CB_ONLINE_MEMBERS'); ?>
</span>
<span class="mod-cb-online__breakdown-item">
<span class="icon-eye" aria-hidden="true"></span>
<?php echo $guestsOnline; ?> <?php echo Text::_('MOD_CB_ONLINE_GUESTS'); ?>
</span>
</div>
<?php endif; ?>
</div>
<?php if ($params->get('show_user_list', 1) && !empty($onlineUsers)) : ?>
<div class="mod-cb-online__users">
<h<?php echo $params->get('header_level', 3); ?> class="mod-cb-online__heading">
<?php echo Text::_('MOD_CB_ONLINE_WHO_IS_ONLINE'); ?>
</h<?php echo $params->get('header_level', 3); ?>>
<ul class="mod-cb-online__list">
<?php foreach ($onlineUsers as $user) : ?>
<li class="mod-cb-online__user">
<?php if ($params->get('show_avatar', 1) && !empty($user->avatar)) : ?>
<div class="mod-cb-online__avatar">
<?php echo $user->avatar; ?>
</div>
<?php endif; ?>
<div class="mod-cb-online__info">
<?php if ($params->get('link_names', 1) && !empty($user->link)) : ?>
<a href="<?php echo $user->link; ?>" class="mod-cb-online__name">
<?php echo htmlspecialchars($user->name, ENT_COMPAT, 'UTF-8'); ?>
</a>
<?php else : ?>
<span class="mod-cb-online__name">
<?php echo htmlspecialchars($user->name, ENT_COMPAT, 'UTF-8'); ?>
</span>
<?php endif; ?>
<?php if ($params->get('show_status', 1) && !empty($user->status)) : ?>
<span class="mod-cb-online__status">
<?php echo htmlspecialchars($user->status, ENT_COMPAT, 'UTF-8'); ?>
</span>
<?php endif; ?>
</div>
<?php if ($params->get('show_online_icon', 1)) : ?>
<span class="mod-cb-online__indicator" title="<?php echo Text::_('MOD_CB_ONLINE_NOW'); ?>">
<span class="icon-checkmark" aria-hidden="true"></span>
</span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php else : ?>
<div class="mod-cb-online__empty">
<p><?php echo Text::_('MOD_CB_ONLINE_NO_USERS'); ?></p>
</div>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,39 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_custom.
* Adds showtitle support and respects all module settings.
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Uri\Uri;
$modId = 'mod-custom' . $module->id;
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
if ($params->get('backgroundimage')) {
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $app->getDocument()->getWebAssetManager();
$wa->addInlineStyle(
'#' . $modId . '{background-image: url("' . Uri::root(true) . '/' . HTMLHelper::_('cleanImageURL', $params->get('backgroundimage'))->url . '");}',
['name' => $modId]
);
}
?>
<div class="mod-custom custom<?php echo $suffix ? ' ' . $suffix : ''; ?>" id="<?php echo $modId; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-custom__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php echo $module->content; ?>
</div>

View File

@@ -1,12 +1,14 @@
<?php <?php
/** /**
* @package Joomla.Site * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @subpackage mod_custom
* *
* @copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> * This file is part of a Moko Consulting project.
* @license GNU General Public License version 2 or later; see LICENSE.txt
* *
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Template override for mod_custom adding banner-overlay wrapper pattern. * Template override for mod_custom adding banner-overlay wrapper pattern.
* Based on Cassiopeia's banner layout approach. * Based on Cassiopeia's banner layout approach.
*/ */
@@ -17,7 +19,9 @@ use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Uri\Uri; use Joomla\CMS\Uri\Uri;
$modId = 'mod-custom' . $module->id; $modId = 'mod-custom' . $module->id;
$moduleclass = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8'); $suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
if ($params->get('backgroundimage')) { if ($params->get('backgroundimage')) {
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
@@ -28,9 +32,11 @@ if ($params->get('backgroundimage')) {
); );
} }
?> ?>
<div class="mod-custom custom banner-overlay custom-hero<?php echo $suffix ? ' ' . $suffix : ''; ?>" id="<?php echo $modId; ?>">
<div class="mod-custom custom banner-overlay custom-hero<?php echo $moduleclass ? ' ' . $moduleclass : ''; ?>" id="<?php echo $modId; ?>">
<div class="overlay"> <div class="overlay">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-custom__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php echo $module->content; ?> <?php echo $module->content; ?>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,88 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_feed.
* Adds showtitle support.
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
if (!$feed) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
$rssurl = $params->get('rssurl', '');
$rsstitle = $params->get('rsstitle', 1);
$rssdesc = $params->get('rssrtl', 0) ? ' feed-rtl' : '';
$rssimage = $params->get('rssimage', 1);
$rssitems = $params->get('rssitems', 5);
$rssitemdesc = $params->get('rssitemdesc', 1);
$word_count = $params->get('word_count', 0);
?>
<div class="mod-feed<?php echo $suffix ? ' ' . $suffix : ''; ?><?php echo $rssdesc; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-feed__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php if ($feed->title && $rsstitle) : ?>
<h4 class="mod-feed__feed-title">
<?php if (!empty($rssurl)) : ?>
<a href="<?php echo htmlspecialchars($rssurl, ENT_COMPAT, 'UTF-8'); ?>" target="_blank" rel="noopener noreferrer">
<?php echo $feed->title; ?>
</a>
<?php else : ?>
<?php echo $feed->title; ?>
<?php endif; ?>
</h4>
<?php endif; ?>
<?php if ($feed->description && $rssdesc) : ?>
<p class="mod-feed__description"><?php echo $feed->description; ?></p>
<?php endif; ?>
<?php if ($rssimage && $feed->image) : ?>
<img src="<?php echo $feed->image->uri; ?>" alt="<?php echo $feed->image->title ?? ''; ?>" class="mod-feed__image" />
<?php endif; ?>
<?php if (!empty($feed->items)) : ?>
<ul class="mod-feed__list">
<?php for ($i = 0, $max = min(count($feed->items), $rssitems); $i < $max; $i++) :
$item = $feed->items[$i];
?>
<li class="mod-feed__item">
<?php if (!empty($item->uri)) : ?>
<a href="<?php echo htmlspecialchars($item->uri, ENT_COMPAT, 'UTF-8'); ?>" target="_blank" rel="noopener noreferrer">
<?php echo $item->title; ?>
</a>
<?php else : ?>
<?php echo $item->title; ?>
<?php endif; ?>
<?php if ($rssitemdesc && !empty($item->content)) :
$desc = $item->content;
if ($word_count) {
$words = explode(' ', strip_tags($desc));
if (count($words) > $word_count) {
$desc = implode(' ', array_slice($words, 0, $word_count)) . '&hellip;';
}
}
?>
<p class="mod-feed__item-description"><?php echo $desc; ?></p>
<?php endif; ?>
</li>
<?php endfor; ?>
</ul>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,85 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_finder (Smart Search).
* Bootstrap 5 search form with showtitle support.
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
// Load component language for search labels
$lang = $app->getLanguage();
$lang->load('com_finder', JPATH_SITE);
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
$showLabel = $params->get('show_label', 1);
$labelClass = (!$showLabel ? 'visually-hidden ' : '') . 'finder';
Text::script('MOD_FINDER_SEARCH_VALUE');
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $app->getDocument()->getWebAssetManager();
$wa->getRegistry()->addExtensionRegistryFile('com_finder');
if ($params->get('show_autosuggest', 1)) {
$wa->usePreset('awesomplete');
$app->getDocument()->addScriptOptions('finder-search', ['url' => Route::_('index.php?option=com_finder&task=suggestions.suggest&format=json&tmpl=component', false)]);
Text::script('COM_FINDER_SEARCH_FORM_LIST_LABEL');
Text::script('JLIB_JS_AJAX_ERROR_OTHER');
Text::script('JLIB_JS_AJAX_ERROR_PARSE');
}
$wa->useScript('com_finder.finder');
?>
<div class="mod-finder<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-finder__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<form class="mod-finder__form js-finder-searchform form-search" action="<?php echo Route::_($route); ?>" method="get" role="search">
<label for="mod-finder-searchword<?php echo $module->id; ?>" class="<?php echo $labelClass; ?>">
<?php echo $params->get('alt_label', Text::_('JSEARCH_FILTER_SUBMIT')); ?>
</label>
<div class="input-group">
<input type="text" name="q" id="mod-finder-searchword<?php echo $module->id; ?>"
class="js-finder-search-query form-control"
value="<?php echo htmlspecialchars($app->getInput()->get('q', '', 'string'), ENT_COMPAT, 'UTF-8'); ?>"
placeholder="<?php echo Text::_('MOD_FINDER_SEARCH_VALUE'); ?>">
<?php if ($params->get('show_button', 0)) : ?>
<button class="btn btn-primary" type="submit">
<span class="fa-solid fa-magnifying-glass" aria-hidden="true"></span>
<span class="visually-hidden"><?php echo Text::_('JSEARCH_FILTER_SUBMIT'); ?></span>
</button>
<?php endif; ?>
</div>
<?php $show_advanced = $params->get('show_advanced', 0); ?>
<?php if ($show_advanced == 2) : ?>
<a href="<?php echo Route::_($route); ?>" class="mod-finder__advanced-link mt-2 d-inline-block">
<?php echo Text::_('COM_FINDER_ADVANCED_SEARCH'); ?>
</a>
<?php elseif ($show_advanced == 1) : ?>
<div class="mod-finder__advanced js-finder-advanced mt-2">
<?php echo HTMLHelper::_('filter.select', $query, $params); ?>
</div>
<?php endif; ?>
<?php
$finderHelper = $app->bootModule('mod_finder', 'site')->getHelper('FinderHelper');
echo $finderHelper->getHiddenFields($route);
?>
</form>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,29 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_footer.
* Adds showtitle support and respects module settings.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-footer<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-footer__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<div class="mod-footer__line1"><?php echo $lineone; ?></div>
<div class="mod-footer__line2"><?php echo Text::_('MOD_FOOTER_LINE2'); ?></div>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><title></title></head><body></body></html>

View File

@@ -1,110 +0,0 @@
<?php
/**
* @package HikaShop
* @subpackage mod_hikashop_cart
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_hikashop_cart module
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-hikashop-cart mod-hikashop-cart-responsive ' . $moduleclass_sfx;
?>
<div class="<?php echo $wrapperClass; ?>" id="hikashop_cart_module<?php echo $params->get('id'); ?>">
<?php if (!empty($cart->products)) : ?>
<div class="mod-hikashop-cart__header">
<span class="mod-hikashop-cart__icon" aria-hidden="true">
<span class="icon-basket"></span>
</span>
<div class="mod-hikashop-cart__summary">
<div class="mod-hikashop-cart__count">
<?php echo count($cart->products); ?>
<?php echo count($cart->products) == 1 ? Text::_('ITEM') : Text::_('ITEMS'); ?>
</div>
<?php if (!empty($cart->total)) : ?>
<div class="mod-hikashop-cart__total">
<?php echo $cart->total->price_value_with_tax_formated; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php if ($params->get('show_products', 1)) : ?>
<div class="mod-hikashop-cart__products">
<?php foreach ($cart->products as $product) : ?>
<div class="mod-hikashop-cart__product">
<?php if (!empty($product->images[0]) && $params->get('show_image', 1)) : ?>
<div class="mod-hikashop-cart__product-image">
<img src="<?php echo $product->images[0]->file_path; ?>"
alt="<?php echo htmlspecialchars($product->product_name, ENT_COMPAT, 'UTF-8'); ?>" />
</div>
<?php endif; ?>
<div class="mod-hikashop-cart__product-details">
<div class="mod-hikashop-cart__product-name">
<?php echo htmlspecialchars($product->product_name, ENT_COMPAT, 'UTF-8'); ?>
</div>
<div class="mod-hikashop-cart__product-quantity">
<?php echo Text::_('QUANTITY'); ?>:
<span class="mod-hikashop-cart__quantity-value"><?php echo $product->cart_product_quantity; ?></span>
</div>
<?php if (!empty($product->prices[0])) : ?>
<div class="mod-hikashop-cart__product-price">
<?php echo $product->prices[0]->price_value_with_tax_formated; ?>
</div>
<?php endif; ?>
</div>
<?php if ($params->get('show_delete', 1)) : ?>
<div class="mod-hikashop-cart__product-remove">
<a href="#"
class="mod-hikashop-cart__remove-btn hikashop_cart_product_delete"
data-product-id="<?php echo $product->product_id; ?>"
title="<?php echo Text::_('HIKA_DELETE'); ?>"
aria-label="<?php echo Text::_('HIKA_DELETE') . ' ' . htmlspecialchars($product->product_name, ENT_COMPAT, 'UTF-8'); ?>">
<span class="icon-remove" aria-hidden="true"></span>
</a>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="mod-hikashop-cart__actions">
<?php if ($params->get('show_cart_button', 1)) : ?>
<a href="<?php echo hikashop_completeLink('cart'); ?>"
class="mod-hikashop-cart__btn mod-hikashop-cart__btn--view btn btn-secondary">
<?php echo Text::_('HIKASHOP_CART_VIEW'); ?>
</a>
<?php endif; ?>
<?php if ($params->get('show_checkout_button', 1)) : ?>
<a href="<?php echo hikashop_completeLink('checkout'); ?>"
class="mod-hikashop-cart__btn mod-hikashop-cart__btn--checkout btn btn-primary">
<?php echo Text::_('HIKASHOP_CHECKOUT'); ?>
</a>
<?php endif; ?>
</div>
<?php else : ?>
<div class="mod-hikashop-cart__empty">
<span class="mod-hikashop-cart__empty-icon" aria-hidden="true">
<span class="icon-basket"></span>
</span>
<p class="mod-hikashop-cart__empty-text">
<?php echo Text::_('HIKASHOP_CART_EMPTY'); ?>
</p>
</div>
<?php endif; ?>
</div>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><title></title></head><body></body></html>

View File

@@ -1,112 +0,0 @@
<?php
/**
* @package K2
* @subpackage mod_k2_content
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_k2_content module
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-k2-content mod-k2-content-responsive ' . $moduleclass_sfx;
?>
<div class="<?php echo $wrapperClass; ?>">
<?php if (count($items)) : ?>
<ul class="mod-k2-content__list">
<?php foreach ($items as $key => $item) : ?>
<li class="mod-k2-content__item">
<?php if ($params->get('itemImage') && !empty($item->imageXSmall)) : ?>
<div class="mod-k2-content__image">
<a href="<?php echo $item->link; ?>" title="<?php echo htmlspecialchars($item->title, ENT_COMPAT, 'UTF-8'); ?>">
<img src="<?php echo $item->imageXSmall; ?>" alt="<?php echo htmlspecialchars($item->title, ENT_COMPAT, 'UTF-8'); ?>" />
</a>
</div>
<?php endif; ?>
<div class="mod-k2-content__content">
<?php if ($params->get('itemTitle')) : ?>
<h<?php echo $params->get('item_heading', 4); ?> class="mod-k2-content__title">
<a href="<?php echo $item->link; ?>">
<?php echo htmlspecialchars($item->title, ENT_COMPAT, 'UTF-8'); ?>
</a>
</h<?php echo $params->get('item_heading', 4); ?>>
<?php endif; ?>
<?php if ($params->get('itemAuthor') || $params->get('itemDateCreated') || $params->get('itemCategory') || $params->get('itemHits')) : ?>
<div class="mod-k2-content__meta">
<?php if ($params->get('itemAuthor')) : ?>
<span class="mod-k2-content__author">
<span class="icon-user" aria-hidden="true"></span>
<?php echo $item->author; ?>
</span>
<?php endif; ?>
<?php if ($params->get('itemDateCreated')) : ?>
<span class="mod-k2-content__date">
<span class="icon-calendar" aria-hidden="true"></span>
<time datetime="<?php echo HTMLHelper::_('date', $item->created, 'c'); ?>">
<?php echo HTMLHelper::_('date', $item->created, Text::_('DATE_FORMAT_LC3')); ?>
</time>
</span>
<?php endif; ?>
<?php if ($params->get('itemCategory')) : ?>
<span class="mod-k2-content__category">
<span class="icon-folder" aria-hidden="true"></span>
<a href="<?php echo $item->categoryLink; ?>">
<?php echo $item->categoryname; ?>
</a>
</span>
<?php endif; ?>
<?php if ($params->get('itemHits')) : ?>
<span class="mod-k2-content__hits">
<span class="icon-eye" aria-hidden="true"></span>
<?php echo $item->hits; ?> <?php echo Text::_('MOD_K2_CONTENT_HITS'); ?>
</span>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($params->get('itemIntroText') && !empty($item->introtext)) : ?>
<div class="mod-k2-content__intro">
<?php echo $item->introtext; ?>
</div>
<?php endif; ?>
<?php if ($params->get('itemReadMore')) : ?>
<div class="mod-k2-content__readmore">
<a href="<?php echo $item->link; ?>" class="mod-k2-content__readmore-link btn btn-secondary">
<?php echo Text::_('MOD_K2_CONTENT_READ_MORE'); ?>
<span class="icon-chevron-right" aria-hidden="true"></span>
</a>
</div>
<?php endif; ?>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php if ($params->get('itemCustomLink')) : ?>
<div class="mod-k2-content__custom-link">
<a href="<?php echo $params->get('itemCustomLinkURL'); ?>" class="btn btn-primary">
<?php echo $params->get('itemCustomLinkTitle'); ?>
</a>
</div>
<?php endif; ?>
<?php else : ?>
<div class="mod-k2-content__empty">
<p><?php echo Text::_('MOD_K2_CONTENT_NO_ITEMS'); ?></p>
</div>
<?php endif; ?>
</div>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><title></title></head><body></body></html>

View File

@@ -1,110 +0,0 @@
<?php
/**
* @package Kunena
* @subpackage mod_kunenalatest
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_kunenalatest module
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-kunena-latest mod-kunena-latest-responsive ' . $moduleclass_sfx;
?>
<div class="<?php echo $wrapperClass; ?>">
<?php if (!empty($posts)) : ?>
<ul class="mod-kunena-latest__list">
<?php foreach ($posts as $post) : ?>
<li class="mod-kunena-latest__item">
<?php if ($params->get('sh_userpic', 1) && !empty($post->getAuthor()->getAvatarImage())) : ?>
<div class="mod-kunena-latest__avatar">
<?php echo $post->getAuthor()->getAvatarImage('', 40, 40); ?>
</div>
<?php endif; ?>
<div class="mod-kunena-latest__content">
<?php if ($params->get('sh_topic', 1)) : ?>
<h<?php echo $params->get('header_level', 4); ?> class="mod-kunena-latest__title">
<a href="<?php echo $post->getUrl(); ?>">
<?php echo htmlspecialchars($post->subject, ENT_COMPAT, 'UTF-8'); ?>
</a>
</h<?php echo $params->get('header_level', 4); ?>>
<?php endif; ?>
<div class="mod-kunena-latest__meta">
<?php if ($params->get('sh_username', 1)) : ?>
<span class="mod-kunena-latest__author">
<span class="icon-user" aria-hidden="true"></span>
<a href="<?php echo $post->getAuthor()->getLink(); ?>">
<?php echo $post->getAuthor()->getName(); ?>
</a>
</span>
<?php endif; ?>
<?php if ($params->get('sh_time', 1)) : ?>
<span class="mod-kunena-latest__date">
<span class="icon-clock" aria-hidden="true"></span>
<time datetime="<?php echo HTMLHelper::_('date', $post->time, 'c'); ?>">
<?php echo $post->getTime(); ?>
</time>
</span>
<?php endif; ?>
<?php if ($params->get('sh_category', 1)) : ?>
<span class="mod-kunena-latest__category">
<span class="icon-folder" aria-hidden="true"></span>
<a href="<?php echo $post->getCategory()->getUrl(); ?>">
<?php echo $post->getCategory()->name; ?>
</a>
</span>
<?php endif; ?>
<?php if ($params->get('sh_hits', 0)) : ?>
<span class="mod-kunena-latest__hits">
<span class="icon-eye" aria-hidden="true"></span>
<?php echo $post->getTopic()->hits; ?>
</span>
<?php endif; ?>
<?php if ($params->get('sh_replies', 0)) : ?>
<span class="mod-kunena-latest__replies">
<span class="icon-comments" aria-hidden="true"></span>
<?php echo $post->getTopic()->getReplies(); ?>
</span>
<?php endif; ?>
</div>
<?php if ($params->get('sh_text', 0) && !empty($post->message)) : ?>
<div class="mod-kunena-latest__excerpt">
<?php echo KunenaHtmlParser::parseBBCode($post->message, $params->get('txt_len', 50)); ?>
</div>
<?php endif; ?>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php if ($params->get('more_link', 1)) : ?>
<div class="mod-kunena-latest__more">
<a href="<?php echo KunenaRoute::_('index.php?option=com_kunena'); ?>"
class="mod-kunena-latest__more-link btn btn-secondary">
<?php echo Text::_('MOD_KUNENALATEST_MORE'); ?>
<span class="icon-chevron-right" aria-hidden="true"></span>
</a>
</div>
<?php endif; ?>
<?php else : ?>
<div class="mod-kunena-latest__empty">
<p><?php echo Text::_('MOD_KUNENALATEST_NO_POSTS'); ?></p>
</div>
<?php endif; ?>
</div>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><title></title></head><body></body></html>

View File

@@ -1,187 +0,0 @@
<?php
/**
* @package Kunena
* @subpackage mod_kunenalogin
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_kunenalogin module
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-kunena-login mod-kunena-login-responsive ' . $moduleclass_sfx;
?>
<div class="<?php echo $wrapperClass; ?>">
<?php if ($kunena_my->exists()) : ?>
<!-- Logged in state -->
<div class="mod-kunena-login__profile">
<?php if ($params->get('showAvatar', 1)) : ?>
<div class="mod-kunena-login__avatar">
<?php echo $kunena_my->getAvatarImage('', 60, 60); ?>
</div>
<?php endif; ?>
<div class="mod-kunena-login__user-info">
<div class="mod-kunena-login__username">
<a href="<?php echo $kunena_my->getURL(); ?>">
<?php echo $kunena_my->getName(); ?>
</a>
</div>
<?php if ($params->get('showRank', 1) && !empty($kunena_my->getRank())) : ?>
<div class="mod-kunena-login__rank">
<?php echo $kunena_my->getRank(); ?>
</div>
<?php endif; ?>
</div>
</div>
<?php if ($params->get('showStats', 1)) : ?>
<div class="mod-kunena-login__stats">
<div class="mod-kunena-login__stat">
<span class="mod-kunena-login__stat-label"><?php echo Text::_('MOD_KUNENALOGIN_POSTS'); ?>:</span>
<span class="mod-kunena-login__stat-value"><?php echo $kunena_my->posts; ?></span>
</div>
<?php if ($params->get('showKarma', 0) && isset($kunena_my->karma)) : ?>
<div class="mod-kunena-login__stat">
<span class="mod-kunena-login__stat-label"><?php echo Text::_('MOD_KUNENALOGIN_KARMA'); ?>:</span>
<span class="mod-kunena-login__stat-value"><?php echo $kunena_my->karma; ?></span>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="mod-kunena-login__actions">
<?php if ($params->get('showProfile', 1)) : ?>
<a href="<?php echo $kunena_my->getURL(); ?>" class="mod-kunena-login__btn btn btn-secondary">
<span class="icon-user" aria-hidden="true"></span>
<?php echo Text::_('MOD_KUNENALOGIN_PROFILE'); ?>
</a>
<?php endif; ?>
<?php if ($params->get('showMessages', 1)) : ?>
<a href="<?php echo KunenaRoute::_('index.php?option=com_kunena&view=user&layout=messages'); ?>"
class="mod-kunena-login__btn btn btn-secondary">
<span class="icon-envelope" aria-hidden="true"></span>
<?php echo Text::_('MOD_KUNENALOGIN_PRIVATE_MESSAGES'); ?>
<?php if (!empty($private_messages)) : ?>
<span class="mod-kunena-login__badge"><?php echo $private_messages; ?></span>
<?php endif; ?>
</a>
<?php endif; ?>
<form action="<?php echo Route::_('index.php', true); ?>" method="post" class="mod-kunena-login__logout-form">
<button type="submit" class="mod-kunena-login__btn mod-kunena-login__btn--logout btn btn-primary">
<span class="icon-sign-out" aria-hidden="true"></span>
<?php echo Text::_('MOD_KUNENALOGIN_LOGOUT'); ?>
</button>
<input type="hidden" name="option" value="com_users" />
<input type="hidden" name="task" value="user.logout" />
<input type="hidden" name="return" value="<?php echo $return; ?>" />
<?php echo JHtml::_('form.token'); ?>
</form>
</div>
<?php else : ?>
<!-- Login form -->
<form action="<?php echo Route::_('index.php', true); ?>" method="post" class="mod-kunena-login__form">
<?php if ($params->get('pretext')) : ?>
<div class="mod-kunena-login__pretext">
<?php echo $params->get('pretext'); ?>
</div>
<?php endif; ?>
<div class="mod-kunena-login__fields">
<div class="mod-kunena-login__field">
<label for="kunena-login-username-<?php echo $module->id; ?>" class="mod-kunena-login__label">
<?php echo Text::_('MOD_KUNENALOGIN_USERNAME'); ?>
</label>
<input
id="kunena-login-username-<?php echo $module->id; ?>"
type="text"
name="username"
class="mod-kunena-login__input form-control"
placeholder="<?php echo Text::_('MOD_KUNENALOGIN_USERNAME'); ?>"
autocomplete="username"
required
/>
</div>
<div class="mod-kunena-login__field">
<label for="kunena-login-password-<?php echo $module->id; ?>" class="mod-kunena-login__label">
<?php echo Text::_('MOD_KUNENALOGIN_PASSWORD'); ?>
</label>
<input
id="kunena-login-password-<?php echo $module->id; ?>"
type="password"
name="password"
class="mod-kunena-login__input form-control"
placeholder="<?php echo Text::_('MOD_KUNENALOGIN_PASSWORD'); ?>"
autocomplete="current-password"
required
/>
</div>
<?php if ($params->get('showRememberMe', 1)) : ?>
<div class="mod-kunena-login__remember">
<input
id="kunena-login-remember-<?php echo $module->id; ?>"
type="checkbox"
name="remember"
class="mod-kunena-login__checkbox"
value="yes"
/>
<label for="kunena-login-remember-<?php echo $module->id; ?>" class="mod-kunena-login__remember-label">
<?php echo Text::_('MOD_KUNENALOGIN_REMEMBER_ME'); ?>
</label>
</div>
<?php endif; ?>
</div>
<div class="mod-kunena-login__actions">
<button type="submit" class="mod-kunena-login__btn mod-kunena-login__btn--submit btn btn-primary">
<span class="icon-sign-in" aria-hidden="true"></span>
<?php echo Text::_('MOD_KUNENALOGIN_LOGIN'); ?>
</button>
</div>
<div class="mod-kunena-login__links">
<?php if ($params->get('showRegister', 1) && $usersConfig->get('allowUserRegistration')) : ?>
<a href="<?php echo Route::_('index.php?option=com_users&view=registration'); ?>"
class="mod-kunena-login__link">
<?php echo Text::_('MOD_KUNENALOGIN_REGISTER'); ?>
<span class="icon-chevron-right" aria-hidden="true"></span>
</a>
<?php endif; ?>
<?php if ($params->get('showForgot', 1)) : ?>
<a href="<?php echo Route::_('index.php?option=com_users&view=reset'); ?>"
class="mod-kunena-login__link">
<?php echo Text::_('MOD_KUNENALOGIN_FORGOT_PASSWORD'); ?>
<span class="icon-chevron-right" aria-hidden="true"></span>
</a>
<?php endif; ?>
</div>
<?php if ($params->get('posttext')) : ?>
<div class="mod-kunena-login__posttext">
<?php echo $params->get('posttext'); ?>
</div>
<?php endif; ?>
<input type="hidden" name="option" value="com_users" />
<input type="hidden" name="task" value="user.login" />
<input type="hidden" name="return" value="<?php echo $return; ?>" />
<?php echo JHtml::_('form.token'); ?>
</form>
<?php endif; ?>
</div>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><title></title></head><body></body></html>

View File

@@ -1,74 +0,0 @@
<?php
/**
* @package Kunena
* @subpackage mod_kunenasearch
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_kunenasearch module
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-kunena-search mod-kunena-search-responsive ' . $moduleclass_sfx;
$button_pos = $params->get('button_pos', 'right');
$button_text = $params->get('button_text', '');
?>
<div class="<?php echo $wrapperClass; ?>">
<form action="<?php echo KunenaRoute::_('index.php?option=com_kunena&view=search'); ?>"
method="post"
class="mod-kunena-search__form mod-kunena-search__form--button-<?php echo $button_pos; ?>">
<?php if ($button_pos === 'top' || $button_pos === 'left') : ?>
<div class="mod-kunena-search__button-wrapper mod-kunena-search__button-wrapper--<?php echo $button_pos; ?>">
<button type="submit" class="mod-kunena-search__button btn btn-primary">
<?php if ($button_text) : ?>
<?php echo htmlspecialchars($button_text, ENT_COMPAT, 'UTF-8'); ?>
<?php else : ?>
<span class="icon-search" aria-hidden="true"></span>
<span class="visually-hidden"><?php echo Text::_('MOD_KUNENASEARCH_SEARCH'); ?></span>
<?php endif; ?>
</button>
</div>
<?php endif; ?>
<div class="mod-kunena-search__input-wrapper">
<label for="mod-kunena-search-<?php echo $module->id; ?>" class="visually-hidden">
<?php echo Text::_('MOD_KUNENASEARCH_SEARCH_FORUM'); ?>
</label>
<input
type="search"
name="q"
id="mod-kunena-search-<?php echo $module->id; ?>"
class="mod-kunena-search__input form-control"
placeholder="<?php echo Text::_('MOD_KUNENASEARCH_SEARCH_FORUM'); ?>"
aria-label="<?php echo Text::_('MOD_KUNENASEARCH_SEARCH_FORUM'); ?>"
value="<?php echo htmlspecialchars($params->get('default_value', ''), ENT_COMPAT, 'UTF-8'); ?>"
/>
</div>
<?php if ($button_pos === 'bottom' || $button_pos === 'right') : ?>
<div class="mod-kunena-search__button-wrapper mod-kunena-search__button-wrapper--<?php echo $button_pos; ?>">
<button type="submit" class="mod-kunena-search__button btn btn-primary">
<?php if ($button_text) : ?>
<?php echo htmlspecialchars($button_text, ENT_COMPAT, 'UTF-8'); ?>
<?php else : ?>
<span class="icon-search" aria-hidden="true"></span>
<span class="visually-hidden"><?php echo Text::_('MOD_KUNENASEARCH_SEARCH'); ?></span>
<?php endif; ?>
</button>
</div>
<?php endif; ?>
<input type="hidden" name="task" value="results" />
<input type="hidden" name="option" value="com_kunena" />
</form>
</div>

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><title></title></head><body></body></html>

View File

@@ -1,100 +0,0 @@
<?php
/**
* @package Kunena
* @subpackage mod_kunenastats
*
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*
* Mobile responsive override for mod_kunenastats module
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
$moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
// Add responsive wrapper class
$wrapperClass = 'mod-kunena-stats mod-kunena-stats-responsive ' . $moduleclass_sfx;
?>
<div class="<?php echo $wrapperClass; ?>">
<div class="mod-kunena-stats__container">
<?php if ($params->get('sh_latestMemberCount', 1)) : ?>
<div class="mod-kunena-stats__stat">
<div class="mod-kunena-stats__icon">
<span class="icon-users" aria-hidden="true"></span>
</div>
<div class="mod-kunena-stats__content">
<div class="mod-kunena-stats__value"><?php echo $kunena_stats->memberCount; ?></div>
<div class="mod-kunena-stats__label"><?php echo Text::_('MOD_KUNENASTATS_MEMBERS'); ?></div>
</div>
</div>
<?php endif; ?>
<?php if ($params->get('sh_latestMember', 1) && !empty($kunena_stats->latestMember)) : ?>
<div class="mod-kunena-stats__stat mod-kunena-stats__stat--latest-member">
<div class="mod-kunena-stats__icon">
<span class="icon-user-plus" aria-hidden="true"></span>
</div>
<div class="mod-kunena-stats__content">
<div class="mod-kunena-stats__label"><?php echo Text::_('MOD_KUNENASTATS_LATEST_MEMBER'); ?></div>
<div class="mod-kunena-stats__value mod-kunena-stats__value--link">
<a href="<?php echo $kunena_stats->latestMember->getURL(); ?>">
<?php echo $kunena_stats->latestMember->getName(); ?>
</a>
</div>
</div>
</div>
<?php endif; ?>
<?php if ($params->get('sh_messageCount', 1)) : ?>
<div class="mod-kunena-stats__stat">
<div class="mod-kunena-stats__icon">
<span class="icon-comments" aria-hidden="true"></span>
</div>
<div class="mod-kunena-stats__content">
<div class="mod-kunena-stats__value"><?php echo $kunena_stats->messageCount; ?></div>
<div class="mod-kunena-stats__label"><?php echo Text::_('MOD_KUNENASTATS_MESSAGES'); ?></div>
</div>
</div>
<?php endif; ?>
<?php if ($params->get('sh_topicCount', 1)) : ?>
<div class="mod-kunena-stats__stat">
<div class="mod-kunena-stats__icon">
<span class="icon-folder-open" aria-hidden="true"></span>
</div>
<div class="mod-kunena-stats__content">
<div class="mod-kunena-stats__value"><?php echo $kunena_stats->topicCount; ?></div>
<div class="mod-kunena-stats__label"><?php echo Text::_('MOD_KUNENASTATS_TOPICS'); ?></div>
</div>
</div>
<?php endif; ?>
<?php if ($params->get('sh_todayTopicCount', 0)) : ?>
<div class="mod-kunena-stats__stat">
<div class="mod-kunena-stats__icon">
<span class="icon-calendar-check" aria-hidden="true"></span>
</div>
<div class="mod-kunena-stats__content">
<div class="mod-kunena-stats__value"><?php echo $kunena_stats->todayTopicCount; ?></div>
<div class="mod-kunena-stats__label"><?php echo Text::_('MOD_KUNENASTATS_TODAY_TOPICS'); ?></div>
</div>
</div>
<?php endif; ?>
<?php if ($params->get('sh_yesterdayTopicCount', 0)) : ?>
<div class="mod-kunena-stats__stat">
<div class="mod-kunena-stats__icon">
<span class="icon-calendar" aria-hidden="true"></span>
</div>
<div class="mod-kunena-stats__content">
<div class="mod-kunena-stats__value"><?php echo $kunena_stats->yesterdayTopicCount; ?></div>
<div class="mod-kunena-stats__label"><?php echo Text::_('MOD_KUNENASTATS_YESTERDAY_TOPICS'); ?></div>
</div>
</div>
<?php endif; ?>
</div>
</div>

View File

@@ -0,0 +1,63 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_languages.
* Adds showtitle support.
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
if (empty($list)) {
return;
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-languages<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-languages__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-languages__list">
<?php foreach ($list as $language) : ?>
<?php $isActive = $language->active ? ' active' : ''; ?>
<li class="mod-languages__item<?php echo $isActive; ?>" dir="<?php echo $language->rtl ? 'rtl' : 'ltr'; ?>">
<?php if ($language->active) : ?>
<span class="mod-languages__link mod-languages__link--active" lang="<?php echo $language->sef; ?>">
<?php else : ?>
<a class="mod-languages__link" href="<?php echo htmlspecialchars($language->link, ENT_COMPAT, 'UTF-8'); ?>" lang="<?php echo $language->sef; ?>">
<?php endif; ?>
<?php if ($params->get('image', 1)) : ?>
<?php if ($language->image) : ?>
<?php echo HTMLHelper::_('image', 'mod_languages/' . $language->image . '.gif', '', null, true); ?>
<?php else : ?>
<span class="mod-languages__badge badge bg-secondary"><?php echo strtoupper($language->sef); ?></span>
<?php endif; ?>
<?php endif; ?>
<?php if ($params->get('show_name', 1)) : ?>
<?php echo $language->title_native; ?>
<?php endif; ?>
<?php if ($language->active) : ?>
</span>
<?php else : ?>
</a>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,126 @@
<?php
/**
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_login.
* Bootstrap 5 login form with showtitle support.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
Factory::getApplication()->getLanguage()->load('mod_login', JPATH_SITE);
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<div class="mod-login<?php echo $suffix ? ' ' . $suffix : ''; ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-login__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<?php if ($type === 'logout') : ?>
<form action="<?php echo Route::_('index.php', true); ?>" method="post" class="mod-login__form mod-login__form--logout">
<?php if ($params->get('greeting', 1)) : ?>
<div class="mod-login__greeting">
<?php if (!empty($user->name)) : ?>
<span class="mod-login__name"><?php echo Text::sprintf('MOD_LOGIN_HINAME', htmlspecialchars($user->name, ENT_COMPAT, 'UTF-8')); ?></span>
<?php else : ?>
<span class="mod-login__name"><?php echo Text::sprintf('MOD_LOGIN_HINAME', htmlspecialchars($user->username, ENT_COMPAT, 'UTF-8')); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="mod-login__submit">
<button type="submit" name="Submit" class="btn btn-primary w-100"><?php echo Text::_('JLOGOUT'); ?></button>
</div>
<input type="hidden" name="option" value="com_users">
<input type="hidden" name="task" value="user.logout">
<input type="hidden" name="return" value="<?php echo $return; ?>">
<?php echo HTMLHelper::_('form.token'); ?>
</form>
<?php else : ?>
<form action="<?php echo Route::_('index.php', true); ?>" method="post" class="mod-login__form mod-login__form--login">
<?php if ($params->get('pretext')) : ?>
<div class="mod-login__pretext"><?php echo $params->get('pretext'); ?></div>
<?php endif; ?>
<div class="mod-login__field mb-3">
<label for="modlgn-username-<?php echo $module->id; ?>" class="form-label visually-hidden"><?php echo Text::_('JGLOBAL_USERNAME'); ?></label>
<div class="input-group">
<span class="input-group-text"><i class="fa-solid fa-user" aria-hidden="true"></i></span>
<input id="modlgn-username-<?php echo $module->id; ?>" type="text" name="username" class="form-control" autocomplete="username" placeholder="<?php echo Text::_('JGLOBAL_USERNAME'); ?>">
</div>
</div>
<div class="mod-login__field mb-3">
<label for="modlgn-passwd-<?php echo $module->id; ?>" class="form-label visually-hidden"><?php echo Text::_('JGLOBAL_PASSWORD'); ?></label>
<div class="input-group">
<span class="input-group-text"><i class="fa-solid fa-lock" aria-hidden="true"></i></span>
<input id="modlgn-passwd-<?php echo $module->id; ?>" type="password" name="password" class="form-control" autocomplete="current-password" placeholder="<?php echo Text::_('JGLOBAL_PASSWORD'); ?>">
</div>
</div>
<?php if (!empty($twofactormethods) && count($twofactormethods) > 1) : ?>
<div class="mod-login__field mb-3">
<label for="modlgn-secretkey-<?php echo $module->id; ?>" class="form-label visually-hidden"><?php echo Text::_('JGLOBAL_SECRETKEY'); ?></label>
<div class="input-group">
<span class="input-group-text"><i class="fa-solid fa-shield-halved" aria-hidden="true"></i></span>
<input id="modlgn-secretkey-<?php echo $module->id; ?>" type="text" name="secretkey" class="form-control" autocomplete="one-time-code" placeholder="<?php echo Text::_('JGLOBAL_SECRETKEY'); ?>">
</div>
</div>
<?php endif; ?>
<?php if ($params->get('remember', 1)) : ?>
<div class="mod-login__remember form-check mb-3">
<input id="modlgn-remember-<?php echo $module->id; ?>" type="checkbox" name="remember" class="form-check-input" value="yes">
<label for="modlgn-remember-<?php echo $module->id; ?>" class="form-check-label"><?php echo Text::_('JGLOBAL_REMEMBER_ME'); ?></label>
</div>
<?php endif; ?>
<div class="mod-login__submit mb-3">
<button type="submit" name="Submit" class="btn btn-primary w-100"><?php echo Text::_('JLOGIN'); ?></button>
</div>
<?php $usersConfig = \Joomla\CMS\Component\ComponentHelper::getParams('com_users'); ?>
<ul class="mod-login__options list-unstyled small">
<?php if ($usersConfig->get('allowUserRegistration')) : ?>
<li>
<a href="<?php echo Route::_('index.php?option=com_users&view=registration'); ?>">
<?php echo Text::_('MOD_LOGIN_REGISTER'); ?>
</a>
</li>
<?php endif; ?>
<li>
<a href="<?php echo Route::_('index.php?option=com_users&view=remind'); ?>">
<?php echo Text::_('MOD_LOGIN_FORGOT_YOUR_USERNAME'); ?>
</a>
</li>
<li>
<a href="<?php echo Route::_('index.php?option=com_users&view=reset'); ?>">
<?php echo Text::_('MOD_LOGIN_FORGOT_YOUR_PASSWORD'); ?>
</a>
</li>
</ul>
<input type="hidden" name="option" value="com_users">
<input type="hidden" name="task" value="user.login">
<input type="hidden" name="return" value="<?php echo $return; ?>">
<?php echo HTMLHelper::_('form.token'); ?>
<?php if ($params->get('posttext')) : ?>
<div class="mod-login__posttext"><?php echo $params->get('posttext'); ?></div>
<?php endif; ?>
</form>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,76 @@
<!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,95 @@
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Default layout override for mod_menu.
* Simple list menu with showtitle support, suitable for sidebars and footers.
*/
defined('_JEXEC') or die;
use Joomla\CMS\Helper\ModuleHelper;
$id = '';
if ($tagId = $params->get('tag_id', '')) {
$id = ' id="' . $tagId . '"';
}
$suffix = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COMPAT, 'UTF-8');
$headerTag = htmlspecialchars($params->get('header_tag', 'h3'), ENT_COMPAT, 'UTF-8');
$headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'UTF-8');
?>
<nav class="mod-menu<?php echo $suffix ? ' ' . $suffix : ''; ?>"<?php echo $id; ?> aria-label="<?php echo htmlspecialchars($module->title, ENT_COMPAT, 'UTF-8'); ?>">
<?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-menu__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?>
<ul class="mod-menu__list nav flex-column">
<?php foreach ($list as $i => &$item) :
$itemParams = $item->getParams();
$class = 'nav-item mod-menu__item item-' . $item->id;
if ($item->id == $default_id) {
$class .= ' default';
}
if ($item->id == $active_id || ($item->type === 'alias' && $itemParams->get('aliasoptions') == $active_id)) {
$class .= ' current';
}
if (in_array($item->id, $path)) {
$class .= ' active';
} elseif ($item->type === 'alias') {
$aliasToId = $itemParams->get('aliasoptions');
if (count($path) > 0 && $aliasToId == $path[count($path) - 1]) {
$class .= ' active';
} elseif (in_array($aliasToId, $path)) {
$class .= ' alias-parent-active';
}
}
if ($item->type === 'separator') {
$class .= ' divider';
}
if ($item->deeper) {
$class .= ' deeper';
}
if ($item->parent) {
$class .= ' parent';
}
echo '<li class="' . $class . '">';
switch ($item->type) :
case 'separator':
case 'component':
case 'heading':
case 'url':
require ModuleHelper::getLayoutPath('mod_menu', 'default_' . $item->type);
break;
default:
require ModuleHelper::getLayoutPath('mod_menu', 'default_url');
break;
endswitch;
if ($item->deeper) {
echo '<ul class="mod-menu__sub nav flex-column ms-3">';
} elseif ($item->shallower) {
echo '</li>';
echo str_repeat('</ul></li>', $item->level_diff);
} else {
echo '</li>';
}
endforeach;
?></ul>
</nav>

View File

@@ -1,9 +1,76 @@
<!DOCTYPE html> <!-- Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<title></title> <title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head> </head>
<body> <body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body> </body>
</html> </html>

View File

@@ -1,11 +1,13 @@
<?php <?php
/** /**
* @package Joomla.Site * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @subpackage mod_menu
* *
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech> * This file is part of a Moko Consulting project.
* @license GNU General Public License version 2 or later; see LICENSE.txt
* *
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Main Menu - Mobile responsive collapsible dropdown menu override * Main Menu - Mobile responsive collapsible dropdown menu override
* Bootstrap 5 responsive navbar with hamburger menu * Bootstrap 5 responsive navbar with hamburger menu
*/ */
@@ -29,7 +31,7 @@ $moduleclass_sfx = htmlspecialchars($params->get('moduleclass_sfx', ''), ENT_COM
<div class="container-fluid"> <div class="container-fluid">
<!-- Hamburger toggle button for mobile --> <!-- Hamburger toggle button for mobile -->
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainMenuCollapse" aria-controls="mainMenuCollapse" aria-expanded="false" aria-label="Toggle Main Menu"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainMenuCollapse" aria-controls="mainMenuCollapse" aria-expanded="false" aria-label="Toggle Main Menu">
<span class="navbar-toggler-icon"></span> <span class="fa-solid fa-bars" aria-hidden="true"></span>
</button> </button>
<!-- Collapsible menu content --> <!-- Collapsible menu content -->

View File

@@ -1,11 +1,13 @@
<?php <?php
/** /**
* @package Joomla.Site * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @subpackage mod_menu
* *
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech> * This file is part of a Moko Consulting project.
* @license GNU General Public License version 2 or later; see LICENSE.txt
* *
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Main Menu - Component item layout * Main Menu - Component item layout
*/ */

View File

@@ -1,11 +1,13 @@
<?php <?php
/** /**
* @package Joomla.Site * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @subpackage mod_menu
* *
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech> * This file is part of a Moko Consulting project.
* @license GNU General Public License version 2 or later; see LICENSE.txt
* *
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Main Menu - Heading item layout * Main Menu - Heading item layout
*/ */

View File

@@ -1,11 +1,13 @@
<?php <?php
/** /**
* @package Joomla.Site * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @subpackage mod_menu
* *
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech> * This file is part of a Moko Consulting project.
* @license GNU General Public License version 2 or later; see LICENSE.txt
* *
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Main Menu - Separator item layout * Main Menu - Separator item layout
*/ */

View File

@@ -1,11 +1,13 @@
<?php <?php
/** /**
* @package Joomla.Site * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @subpackage mod_menu
* *
* @copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech> * This file is part of a Moko Consulting project.
* @license GNU General Public License version 2 or later; see LICENSE.txt
* *
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**
* Main Menu - URL item layout * Main Menu - URL item layout
*/ */

Some files were not shown because too many files have changed in this diff Show More