Compare commits

..

165 Commits

Author SHA1 Message Date
462bce226d Merge pull request 'Update ghcr.io/renovatebot/renovate Docker tag to v43.2.3' (#577) from renovate/ghcr.io-renovatebot-renovate-43.x into main
Some checks failed
Laravel / laravel-tests (push) Has been cancelled
renovate / renovate (push) Has been cancelled
2026-02-03 10:39:57 +00:00
Renovate Bot
2c3ae4ee86 Update ghcr.io/renovatebot/renovate Docker tag to v43.2.3
Some checks failed
Laravel / laravel-tests (pull_request) Failing after 4m41s
2026-02-03 10:39:55 +00:00
f4d6277646 Update .gitea/workflows/laravel.yml
Some checks failed
Laravel / laravel-tests (push) Failing after 4m57s
renovate / renovate (push) Has been cancelled
2026-02-03 10:34:11 +00:00
5e47287593 Update .gitea/workflows/laravel.yml
Some checks failed
Laravel / laravel-tests (push) Failing after 4m44s
renovate / renovate (push) Successful in 42s
2026-02-03 09:31:13 +00:00
dd5eb6782d disable posts
Some checks failed
Laravel / laravel-tests (push) Failing after 5m5s
renovate / renovate (push) Successful in 45s
2026-02-03 19:20:29 +10:00
0312e1dbea Merge pull request 'Update ghcr.io/renovatebot/renovate Docker tag to v43' (#574) from renovate/ghcr.io-renovatebot-renovate-43.x into main
Some checks failed
Laravel / laravel-tests (push) Failing after 6m29s
renovate / renovate (push) Successful in 1m10s
Reviewed-on: #574
2026-02-03 08:54:32 +00:00
b29ea655ba Merge branch 'main' into renovate/ghcr.io-renovatebot-renovate-43.x 2026-02-03 08:54:21 +00:00
Renovate Bot
8d06374bef Update ghcr.io/renovatebot/renovate Docker tag to v43 2026-02-03 08:53:24 +00:00
5ca0afc385 updated rules
Some checks failed
Laravel / laravel-tests (push) Has been cancelled
renovate / renovate (push) Has been cancelled
2026-02-03 18:53:22 +10:00
cc5a0de05d php 8.4 2026-02-03 18:52:22 +10:00
fe84e20645 disconnect github 2026-02-03 18:52:02 +10:00
2ba8881f3b Add .gitea/workflows/laravel.yml
Some checks failed
Laravel / laravel-tests (push) Failing after 2m54s
renovate / renovate (push) Successful in 46s
2026-02-03 08:49:50 +00:00
ce5c97290f Update .gitea/workflows/renovate.yaml
Some checks are pending
renovate / renovate (push) Waiting to run
2026-02-03 08:41:28 +00:00
Renovate Bot
0c7407c11b Update ghcr.io/renovatebot/renovate Docker tag to v37.440.7
Some checks failed
renovate / renovate (push) Has been cancelled
2026-02-03 08:40:50 +00:00
fe5ab7b0bf Update .gitea/workflows/renovate.yaml
Some checks failed
renovate / renovate (push) Failing after 20s
2026-02-03 08:36:12 +00:00
954b83ebba Update renovate-config.json
Some checks failed
renovate / renovate (push) Has been cancelled
2026-02-03 08:36:00 +00:00
9bfe23df9c Update .gitea/workflows/renovate.yaml
Some checks failed
renovate / renovate (push) Failing after 16s
2026-02-03 08:34:58 +00:00
5f78a9e500 Update renovate-config.cjs
Some checks failed
renovate / renovate (push) Failing after 16s
2026-02-03 08:34:30 +00:00
d47672dbe3 Update .gitea/workflows/renovate.yaml
Some checks failed
renovate / renovate (push) Failing after 13s
2026-02-03 08:31:17 +00:00
c3b898d99e Update .gitea/workflows/renovate.yaml
Some checks failed
renovate / renovate (push) Failing after 14s
2026-02-03 08:29:27 +00:00
b13b22c359 Update .gitea/workflows/renovate.yaml
Some checks failed
renovate / renovate (push) Failing after 21s
2026-02-03 08:27:28 +00:00
fa505f56ee Update .gitea/workflows/renovate.yaml
Some checks failed
renovate / renovate (push) Failing after 19s
2026-02-03 08:25:51 +00:00
464c7d5aa1 Update .gitea/workflows/renovate.yaml
Some checks failed
renovate / renovate (push) Failing after 24s
2026-02-03 08:23:55 +00:00
260b794111 Add renovate-config.js
Some checks failed
renovate / renovate (push) Failing after 2m40s
2026-02-03 08:18:55 +00:00
e2567c1b97 Update .gitea/workflows/renovate.yaml
Some checks are pending
renovate / renovate (push) Has started running
2026-02-03 08:18:29 +00:00
7302a36067 Update .gitea/workflows/renovate.yaml
Some checks failed
renovate / renovate (push) Failing after 22s
2026-02-03 08:10:39 +00:00
ac7b02f320 dependency updates
Some checks failed
renovate / renovate (push) Failing after 22s
2026-02-03 18:07:39 +10:00
0fbeb192c1 Add .gitea/workflows/renovate.yaml
Some checks failed
renovate / renovate (push) Failing after 55s
2026-02-03 07:42:37 +00:00
5c3788eb6d Add renovate.json
Some checks failed
Laravel / laravel-tests (push) Failing after 3m39s
2026-02-03 07:39:32 +00:00
James Collins
f01051107e Merge pull request #572 from STEMMechanics/dependabot/composer/livewire/livewire-4.1.2
Bump livewire/livewire from 4.1.0 to 4.1.2
2026-02-03 15:19:46 +10:00
dependabot[bot]
cf029bc86e Bump livewire/livewire from 4.1.0 to 4.1.2
Bumps [livewire/livewire](https://github.com/livewire/livewire) from 4.1.0 to 4.1.2.
- [Release notes](https://github.com/livewire/livewire/releases)
- [Commits](https://github.com/livewire/livewire/compare/v4.1.0...v4.1.2)

---
updated-dependencies:
- dependency-name: livewire/livewire
  dependency-version: 4.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 05:08:46 +00:00
James Collins
17ab10c1c5 Merge pull request #571 from STEMMechanics/dependabot/npm_and_yarn/autoprefixer-10.4.24
Bump autoprefixer from 10.4.23 to 10.4.24
2026-02-02 10:47:23 +10:00
dependabot[bot]
dc5c387f7a Bump autoprefixer from 10.4.23 to 10.4.24
Bumps [autoprefixer](https://github.com/postcss/autoprefixer) from 10.4.23 to 10.4.24.
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.4.23...10.4.24)

---
updated-dependencies:
- dependency-name: autoprefixer
  dependency-version: 10.4.24
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-02 00:44:17 +00:00
James Collins
3b4d2b2784 Merge pull request #570 from STEMMechanics/dependabot/composer/psy/psysh-0.12.19
Bump psy/psysh from 0.12.18 to 0.12.19
2026-01-31 08:35:50 +10:00
dependabot[bot]
3b3dc276fc Bump psy/psysh from 0.12.18 to 0.12.19
Bumps [psy/psysh](https://github.com/bobthecow/psysh) from 0.12.18 to 0.12.19.
- [Release notes](https://github.com/bobthecow/psysh/releases)
- [Commits](https://github.com/bobthecow/psysh/compare/v0.12.18...v0.12.19)

---
updated-dependencies:
- dependency-name: psy/psysh
  dependency-version: 0.12.19
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-30 22:31:55 +00:00
James Collins
ee460ec076 Merge pull request #569 from STEMMechanics/dependabot/npm_and_yarn/tiptap/extension-text-align-3.18.0
Bump @tiptap/extension-text-align from 3.17.1 to 3.18.0
2026-01-30 11:20:28 +10:00
James Collins
24e7c6a008 Merge pull request #568 from STEMMechanics/dependabot/npm_and_yarn/tiptap/extension-image-3.18.0
Bump @tiptap/extension-image from 3.17.1 to 3.18.0
2026-01-30 11:20:16 +10:00
dependabot[bot]
1a6a1dab47 Bump @tiptap/extension-image from 3.17.1 to 3.18.0
Bumps [@tiptap/extension-image](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-image) from 3.17.1 to 3.18.0.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/develop/packages/extension-image/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.18.0/packages/extension-image)

---
updated-dependencies:
- dependency-name: "@tiptap/extension-image"
  dependency-version: 3.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-30 00:52:33 +00:00
dependabot[bot]
c85031ab49 Bump @tiptap/extension-text-align from 3.17.1 to 3.18.0
Bumps [@tiptap/extension-text-align](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-text-align) from 3.17.1 to 3.18.0.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/develop/packages/extension-text-align/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.18.0/packages/extension-text-align)

---
updated-dependencies:
- dependency-name: "@tiptap/extension-text-align"
  dependency-version: 3.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-30 00:52:32 +00:00
James Collins
24efcab8da Merge pull request #567 from STEMMechanics/dependabot/npm_and_yarn/tiptap/extension-typography-3.18.0
Bump @tiptap/extension-typography from 3.17.1 to 3.18.0
2026-01-30 10:51:48 +10:00
James Collins
182e4e8de8 Merge pull request #565 from STEMMechanics/dependabot/npm_and_yarn/tiptap/extension-subscript-3.18.0
Bump @tiptap/extension-subscript from 3.17.1 to 3.18.0
2026-01-30 10:51:36 +10:00
James Collins
6b62e45acf Merge pull request #566 from STEMMechanics/dependabot/npm_and_yarn/tiptap/starter-kit-3.18.0
Bump @tiptap/starter-kit from 3.17.1 to 3.18.0
2026-01-30 10:51:23 +10:00
dependabot[bot]
a37f744caa Bump @tiptap/extension-typography from 3.17.1 to 3.18.0
Bumps [@tiptap/extension-typography](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-typography) from 3.17.1 to 3.18.0.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/develop/packages/extension-typography/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.18.0/packages/extension-typography)

---
updated-dependencies:
- dependency-name: "@tiptap/extension-typography"
  dependency-version: 3.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-30 00:43:34 +00:00
dependabot[bot]
8eca69335f Bump @tiptap/starter-kit from 3.17.1 to 3.18.0
Bumps [@tiptap/starter-kit](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/starter-kit) from 3.17.1 to 3.18.0.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/develop/packages/starter-kit/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.18.0/packages/starter-kit)

---
updated-dependencies:
- dependency-name: "@tiptap/starter-kit"
  dependency-version: 3.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-30 00:43:22 +00:00
dependabot[bot]
87e70704c1 Bump @tiptap/extension-subscript from 3.17.1 to 3.18.0
Bumps [@tiptap/extension-subscript](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-subscript) from 3.17.1 to 3.18.0.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/develop/packages/extension-subscript/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.18.0/packages/extension-subscript)

---
updated-dependencies:
- dependency-name: "@tiptap/extension-subscript"
  dependency-version: 3.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-30 00:43:10 +00:00
James Collins
db1821bcef Merge pull request #564 from STEMMechanics/copilot/add-code-styling-setup
Add Laravel Pint configuration for Shift compatibility
2026-01-29 15:16:17 +10:00
copilot-swe-agent[bot]
62583c4869 Fix pint.json: remove incorrect braces rule setting
Co-authored-by: nomadjimbob <26953208+nomadjimbob@users.noreply.github.com>
2026-01-29 05:13:01 +00:00
copilot-swe-agent[bot]
066b7b1790 Add code style documentation to README
Co-authored-by: nomadjimbob <26953208+nomadjimbob@users.noreply.github.com>
2026-01-29 05:12:18 +00:00
copilot-swe-agent[bot]
d38422e16c Add Laravel Pint configuration and composer scripts
Co-authored-by: nomadjimbob <26953208+nomadjimbob@users.noreply.github.com>
2026-01-29 05:11:55 +00:00
copilot-swe-agent[bot]
ed22521e6f Initial plan 2026-01-29 05:10:08 +00:00
James Collins
3218f55a31 Merge pull request #562 from STEMMechanics/dependabot/npm_and_yarn/tiptap/extension-underline-3.18.0
Bump @tiptap/extension-underline from 3.17.1 to 3.18.0
2026-01-29 14:00:50 +10:00
dependabot[bot]
be1fa8f432 Bump @tiptap/extension-underline from 3.17.1 to 3.18.0
Bumps [@tiptap/extension-underline](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-underline) from 3.17.1 to 3.18.0.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/develop/packages/extension-underline/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.18.0/packages/extension-underline)

---
updated-dependencies:
- dependency-name: "@tiptap/extension-underline"
  dependency-version: 3.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 04:00:02 +00:00
James Collins
c0c572397a Merge pull request #561 from STEMMechanics/dependabot/npm_and_yarn/tiptap/extension-link-3.18.0
Bump @tiptap/extension-link from 3.17.1 to 3.18.0
2026-01-29 13:59:10 +10:00
James Collins
049d4849aa Merge pull request #560 from STEMMechanics/dependabot/npm_and_yarn/tiptap/extension-superscript-3.18.0
Bump @tiptap/extension-superscript from 3.17.1 to 3.18.0
2026-01-29 13:58:58 +10:00
James Collins
e89656367f Merge pull request #559 from STEMMechanics/dependabot/composer/phpunit/phpunit-12.5.8
Bump phpunit/phpunit from 10.5.62 to 12.5.8
2026-01-29 13:58:41 +10:00
dependabot[bot]
e910ef7e84 Bump phpunit/phpunit from 10.5.62 to 12.5.8
Bumps [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) from 10.5.62 to 12.5.8.
- [Release notes](https://github.com/sebastianbergmann/phpunit/releases)
- [Changelog](https://github.com/sebastianbergmann/phpunit/blob/12.5.8/ChangeLog-12.5.md)
- [Commits](https://github.com/sebastianbergmann/phpunit/compare/10.5.62...12.5.8)

---
updated-dependencies:
- dependency-name: phpunit/phpunit
  dependency-version: 12.5.8
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 03:58:02 +00:00
dependabot[bot]
db0e927056 Bump @tiptap/extension-link from 3.17.1 to 3.18.0
Bumps [@tiptap/extension-link](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-link) from 3.17.1 to 3.18.0.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/develop/packages/extension-link/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.18.0/packages/extension-link)

---
updated-dependencies:
- dependency-name: "@tiptap/extension-link"
  dependency-version: 3.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 03:57:20 +00:00
James Collins
cec55028d6 Merge pull request #557 from STEMMechanics/dependabot/composer/livewire/livewire-4.1.0 2026-01-29 13:57:08 +10:00
James Collins
0ae09c40ae Merge pull request #556 from STEMMechanics/dependabot/npm_and_yarn/tiptap/extension-highlight-3.18.0
Bump @tiptap/extension-highlight from 3.17.1 to 3.18.0
2026-01-29 13:56:18 +10:00
James Collins
1cf2ec1ce7 Merge pull request #555 from STEMMechanics/dependabot/composer/laravel/framework-12.49.0
Bump laravel/framework from 12.48.1 to 12.49.0
2026-01-29 13:56:06 +10:00
dependabot[bot]
693df3a05b Bump @tiptap/extension-superscript from 3.17.1 to 3.18.0
Bumps [@tiptap/extension-superscript](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-superscript) from 3.17.1 to 3.18.0.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/develop/packages/extension-superscript/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.18.0/packages/extension-superscript)

---
updated-dependencies:
- dependency-name: "@tiptap/extension-superscript"
  dependency-version: 3.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 00:43:35 +00:00
dependabot[bot]
c0d1913660 Bump livewire/livewire from 3.7.6 to 4.1.0
Bumps [livewire/livewire](https://github.com/livewire/livewire) from 3.7.6 to 4.1.0.
- [Release notes](https://github.com/livewire/livewire/releases)
- [Commits](https://github.com/livewire/livewire/compare/v3.7.6...v4.1.0)

---
updated-dependencies:
- dependency-name: livewire/livewire
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 00:43:20 +00:00
dependabot[bot]
8c387280a6 Bump @tiptap/extension-highlight from 3.17.1 to 3.18.0
Bumps [@tiptap/extension-highlight](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-highlight) from 3.17.1 to 3.18.0.
- [Release notes](https://github.com/ueberdosis/tiptap/releases)
- [Changelog](https://github.com/ueberdosis/tiptap/blob/develop/packages/extension-highlight/CHANGELOG.md)
- [Commits](https://github.com/ueberdosis/tiptap/commits/v3.18.0/packages/extension-highlight)

---
updated-dependencies:
- dependency-name: "@tiptap/extension-highlight"
  dependency-version: 3.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 00:43:11 +00:00
dependabot[bot]
a1ab7b6257 Bump laravel/framework from 12.48.1 to 12.49.0
Bumps [laravel/framework](https://github.com/laravel/framework) from 12.48.1 to 12.49.0.
- [Release notes](https://github.com/laravel/framework/releases)
- [Changelog](https://github.com/laravel/framework/blob/12.x/CHANGELOG.md)
- [Commits](https://github.com/laravel/framework/compare/v12.48.1...v12.49.0)

---
updated-dependencies:
- dependency-name: laravel/framework
  dependency-version: 12.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-29 00:43:07 +00:00
James Collins
36d4f08aaf Merge pull request #554 from STEMMechanics/dependabot/composer/symfony/process-7.4.5
Bump symfony/process from 7.4.4 to 7.4.5
2026-01-29 07:50:12 +10:00
dependabot[bot]
f7de4f49d3 Bump symfony/process from 7.4.4 to 7.4.5
Bumps [symfony/process](https://github.com/symfony/process) from 7.4.4 to 7.4.5.
- [Release notes](https://github.com/symfony/process/releases)
- [Changelog](https://github.com/symfony/process/blob/8.1/CHANGELOG.md)
- [Commits](https://github.com/symfony/process/compare/v7.4.4...v7.4.5)

---
updated-dependencies:
- dependency-name: symfony/process
  dependency-version: 7.4.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-28 21:39:53 +00:00
James Collins
06018c405d Merge pull request #553 from STEMMechanics/dependabot/composer/phpunit/phpunit-10.5.62
Bump phpunit/phpunit from 10.5.61 to 10.5.62
2026-01-28 16:24:47 +10:00
dependabot[bot]
03c17200a0 Bump phpunit/phpunit from 10.5.61 to 10.5.62
Bumps [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) from 10.5.61 to 10.5.62.
- [Release notes](https://github.com/sebastianbergmann/phpunit/releases)
- [Changelog](https://github.com/sebastianbergmann/phpunit/blob/10.5.62/ChangeLog-10.5.md)
- [Commits](https://github.com/sebastianbergmann/phpunit/compare/10.5.61...10.5.62)

---
updated-dependencies:
- dependency-name: phpunit/phpunit
  dependency-version: 10.5.62
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-28 04:44:31 +00:00
519c2a0fec update to php 8.4 2026-01-28 14:43:38 +10:00
James Collins
2b8788f716 Merge pull request #552 from STEMMechanics/dependabot/npm_and_yarn/axios-1.13.4
Bump axios from 1.13.3 to 1.13.4
2026-01-28 14:33:55 +10:00
dependabot[bot]
72be071536 Bump axios from 1.13.3 to 1.13.4
Bumps [axios](https://github.com/axios/axios) from 1.13.3 to 1.13.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.3...v1.13.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.13.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-28 00:43:12 +00:00
37b923fbda use important on hidden for placeholder 2026-01-27 20:30:01 +10:00
github-actions[bot]
292901a02e Dependency update 2026-01-26 20:19:20 +00:00
github-actions[bot]
12a6629a4b Dependency update 2026-01-19 20:17:13 +00:00
9d9a8ed9f5 exclude vendor in tests 2026-01-15 08:56:12 +10:00
1a03fed3bd added snyk test 2026-01-15 08:54:06 +10:00
43e66b2004 path traversal in chunk unlink fix 2026-01-15 08:33:38 +10:00
8babb4c836 added unlink safeguard 2026-01-15 08:28:41 +10:00
4eb3dfbb64 fix potential path traversal 2026-01-15 08:23:03 +10:00
33d390a612 fix open redirect 2026-01-15 08:01:05 +10:00
cad78c30ae added peer dependency 2026-01-15 07:57:10 +10:00
f8acdae237 fix path traversal risk 2026-01-15 07:56:32 +10:00
github-actions[bot]
63582dc306 Dependency update 2026-01-12 20:17:47 +00:00
github-actions[bot]
c96fb95f27 Dependency update 2026-01-05 20:18:40 +00:00
1160c8b077 holiday message 2026-01-01 09:11:28 +10:00
a4b3405f8a vite update 2026-01-01 09:06:16 +10:00
9d5396ca9a tiptap and vite update 2026-01-01 09:02:08 +10:00
de1483d409 tiptap update 2026-01-01 09:01:15 +10:00
James Collins
a37386565a Merge pull request #530 from STEMMechanics/dependabot/npm_and_yarn/laravel-vite-plugin-2.0.1
Bump laravel-vite-plugin from 1.3.0 to 2.0.1
2026-01-01 08:46:08 +10:00
dependabot[bot]
ff8b6549ce Bump laravel-vite-plugin from 1.3.0 to 2.0.1
Bumps [laravel-vite-plugin](https://github.com/laravel/vite-plugin) from 1.3.0 to 2.0.1.
- [Release notes](https://github.com/laravel/vite-plugin/releases)
- [Changelog](https://github.com/laravel/vite-plugin/blob/2.x/CHANGELOG.md)
- [Upgrade guide](https://github.com/laravel/vite-plugin/blob/2.x/UPGRADE.md)
- [Commits](https://github.com/laravel/vite-plugin/compare/v1.3.0...v2.0.1)

---
updated-dependencies:
- dependency-name: laravel-vite-plugin
  dependency-version: 2.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-31 22:18:17 +00:00
github-actions[bot]
d550bc60e2 Dependency update 2025-12-31 21:02:22 +00:00
James Collins
307573a3a8 Update workflow to commit changes directly
Replaced the Create PR step with a Commit and push step to directly commit changes to the main branch.
2026-01-01 07:01:35 +10:00
James Collins
d9fc6c95f3 Update PHP version in dependency update workflow 2026-01-01 06:58:23 +10:00
James Collins
a65f0eead6 Add workflow for automated dependency updates 2026-01-01 06:56:13 +10:00
ca025ca2e8 dependency updates 2025-12-23 10:39:55 +10:00
74aef68edc remove posts reference 2025-12-23 10:34:05 +10:00
c0e595f88a dependency updates 2025-11-27 10:15:26 +10:00
aab4bc0d46 dependency updates 2025-11-26 10:10:57 +10:00
f2708e1325 dependency updates 2025-11-19 10:32:18 +10:00
James Collins
2e2b70ab7c Merge pull request #533 from STEMMechanics/dependabot/npm_and_yarn/multi-74f7c1f85a
Bump esbuild and vite
2025-11-19 10:28:45 +10:00
dependabot[bot]
e42d54554a Bump esbuild and vite
Bumps [esbuild](https://github.com/evanw/esbuild) to 0.25.12 and updates ancestor dependency [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). These dependencies need to be updated together.


Updates `esbuild` from 0.21.5 to 0.25.12
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.21.5...v0.25.12)

Updates `vite` from 5.4.21 to 7.2.2
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.2.2/packages/vite)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.25.12
  dependency-type: indirect
- dependency-name: vite
  dependency-version: 7.2.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-19 00:25:53 +00:00
6cb24f1500 dependency updates 2025-11-19 10:24:23 +10:00
f7cc086f37 added jenkins link 2025-11-19 10:23:05 +10:00
8463da7842 opacity fixes 2025-11-17 09:26:48 +10:00
James Collins
322d547c92 Merge pull request #526 from STEMMechanics/laravel12
laravel 12 upgrade
2025-11-17 09:09:10 +10:00
30104ece71 laravel 12 upgrade 2025-11-17 09:08:07 +10:00
44f359ff9c fix timings 2025-11-16 23:15:49 +10:00
20f36d519a fix timings 2025-11-16 23:14:55 +10:00
e358e9fb5d fix timings 2025-11-16 23:13:28 +10:00
b882d92328 fix timings 2025-11-16 23:08:34 +10:00
3257aa9ee9 added bot checks 2025-11-16 23:04:50 +10:00
0bcd6f5e86 added bot checks 2025-11-16 23:00:21 +10:00
75d958856a rename controller 2025-11-16 22:59:07 +10:00
71eb00d010 unsubscribe fixes 2025-11-16 22:56:16 +10:00
eab3d062f5 unsubscribe fixes 2025-11-16 22:48:17 +10:00
1afa22e2f4 logging 2025-11-16 22:19:40 +10:00
b85d039c36 fix var name 2025-11-16 22:14:04 +10:00
c1a4fd13d5 fix var name 2025-11-16 22:04:01 +10:00
9a1ffe835c fix var name 2025-11-16 22:02:32 +10:00
c3b9482d35 obsolete directives 2025-11-16 22:00:14 +10:00
bc8f9149dc fix unsubscribe link 2025-11-16 21:57:41 +10:00
c60213257b force SSL 2025-11-16 21:41:14 +10:00
6a78ba2bb2 composer updates 2025-11-16 21:12:40 +10:00
a5f7ce8393 updated subscription elements 2025-11-16 21:10:34 +10:00
4e1505c5c2 updated subscription elements 2025-11-16 19:07:07 +10:00
e967bdde71 updated footer and added about page 2025-11-16 16:20:41 +10:00
74e9e39722 updated address 2025-11-16 16:02:34 +10:00
0df4033fca package updates 2025-11-16 15:41:11 +10:00
e02770cc85 added roave/security-advisories 2025-11-16 15:32:35 +10:00
3687af2656 remove blog posts 2025-11-16 15:31:29 +10:00
b168931266 upgraded packages 2025-11-10 16:46:10 +10:00
b669dd319e fixed bad left offset of backdrop in dropdown 2025-11-10 16:43:26 +10:00
e37b9a30a4 dependency updates 2025-08-28 20:17:42 +10:00
436d4b8acf update 2025-08-28 20:12:49 +10:00
a2eb1d5d1b search bar focus and select fix 2025-08-28 20:12:31 +10:00
be4fdb2f80 updated to handle local caching 2025-08-28 20:03:30 +10:00
538f324ff4 captcha cleanup and added 2fa logins 2024-09-28 11:51:28 +10:00
59ca73519d added instructions 2024-09-28 09:23:16 +10:00
6bc2b888a4 change timer 2024-09-28 09:18:43 +10:00
be8b2d48b3 update newsletter schedule 2024-09-27 22:38:11 +10:00
5f631a5c3d remove user data 2024-09-27 22:26:58 +10:00
fea3756eab fix bad checkbox variable 2024-09-27 22:26:25 +10:00
6d8db2cd80 fix bad variable name 2024-09-27 22:23:57 +10:00
9725f4944f fix bad variable name 2024-09-27 22:23:01 +10:00
9b1b92d0cf added email subscriptions 2024-09-27 22:17:39 +10:00
b10b6b712e added email subscriptions 2024-09-27 22:16:29 +10:00
db018e9120 fix invalid tag 2024-09-27 19:58:57 +10:00
1444bc9aa4 fallback if firstname is missing 2024-09-27 19:56:32 +10:00
9e7fc79fa1 add search option to navbar slide out 2024-09-27 18:08:25 +10:00
06460d9677 update home to shipping address 2024-09-27 18:04:12 +10:00
beed9f9c11 update home to shipping address 2024-09-27 17:59:27 +10:00
38b3d5d367 positioning updates 2024-09-27 17:30:00 +10:00
ad080b19a2 fix asset links 2024-09-27 14:47:59 +10:00
274d9759b6 fix small screen layouts 2024-09-27 14:26:46 +10:00
d992570ee8 fix number formatting 2024-09-27 14:22:18 +10:00
d72c08b4c9 fix selection 2024-09-27 14:22:03 +10:00
7baea36628 fix single decimal point pricing 2024-09-27 13:39:59 +10:00
b20c79b679 updated search and added past workshop page 2024-09-27 13:33:50 +10:00
5cbebd8840 remove 2024-09-27 11:26:24 +10:00
d36979cbbd updated 2024-09-27 11:25:49 +10:00
1c28cd7902 updated 2024-09-27 11:24:55 +10:00
df19e43112 update includes 2024-09-27 11:24:47 +10:00
5a65517d2b added past index route 2024-09-27 11:19:32 +10:00
49eb388041 updated mast to support tabs 2024-09-27 11:17:49 +10:00
659ae2e3ac bts update 2024-09-27 10:27:54 +10:00
109 changed files with 7977 additions and 4229 deletions

View File

@@ -3,39 +3,59 @@ name: Laravel
on:
push:
branches: ["main"]
pull_request:
jobs:
laravel-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: "8.2"
- uses: actions/checkout@v3
php-version: "8.4"
- name: Copy .env
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
- name: Install Dependencies
- name: Install PHP Dependencies
run: composer install -q --no-interaction --no-progress --prefer-dist
- name: Generate key
run: php artisan key:generate
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Create Database
run: |
mkdir -p database
touch database/database.sqlite
- name: Execute tests (Unit and Feature tests) via PHPUnit
- name: Install Node.js
uses: actions/setup-node@v2
with:
node-version: "20"
- name: Install Node dependencies
run: npm ci
- name: Build frontend
run: npm run build
- name: Run migrations
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
run: php artisan migrate --force
- name: Run PHPUnit
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
run: vendor/bin/phpunit
- name: Install Node.js
uses: actions/setup-node@v2
with:
node-version: "16.x"
- name: Install dependencies
run: npm ci
- name: Run Vue tests
env:
LARAVEL_BYPASS_ENV_CHECK: "1"

View File

@@ -0,0 +1,24 @@
name: renovate
on:
workflow_dispatch:
schedule:
- cron: "@daily"
push:
branches:
- main
jobs:
renovate:
runs-on: ubuntu-latest
container: ghcr.io/renovatebot/renovate:43.2.3
steps:
- uses: actions/checkout@v4
- run: renovate
working-directory: ${{ gitea.workspace }}
env:
RENOVATE_CONFIG_FILE: "renovate-config.json"
LOG_LEVEL: "info"
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}

View File

@@ -1,15 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "daily"

3
.gitignore vendored
View File

@@ -15,7 +15,7 @@ app/storage/
# Laravel 5 & Lumen specific
public/storage
public/hot
public/hot*
# Laravel 5 & Lumen specific with changed public path
public_html/storage
@@ -259,3 +259,4 @@ phpcbf.phar
### PHPStorm ###
.idea/

View File

@@ -49,6 +49,24 @@ We would like to extend our thanks to the following sponsors for funding Laravel
- **[byte5](https://byte5.de)**
- **[OP.GG](https://op.gg)**
## Code Style
This project uses [Laravel Pint](https://laravel.com/docs/pint) for code styling. Pint is an opinionated PHP code style fixer for minimalists, built on top of PHP-CS-Fixer.
To automatically fix code style issues, run:
```bash
composer pint
```
To check for code style issues without fixing them:
```bash
composer pint-test
```
The code style configuration can be found in `pint.json`.
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).

View File

@@ -6,10 +6,11 @@ use App\Helpers;
use App\Jobs\SendEmail;
use App\Mail\UserEmailUpdateRequest;
use App\Models\User;
use App\Providers\QRCodeProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use RobThree\Auth\Algorithm;
use RobThree\Auth\TwoFactorAuth;
class AccountController extends Controller
{
@@ -50,11 +51,11 @@ class AccountController extends Controller
'email' => ['required', 'email', 'unique:users,email,' . $user->id],
'phone' => 'required',
'home_address' => 'required_with:home_city,home_postcode,home_country,home_state',
'home_city' => 'required_with:home_address,home_postcode,home_country,home_state',
'home_postcode' => 'required_with:home_address,home_city,home_country,home_state',
'home_country' => 'required_with:home_address,home_city,home_postcode,home_state',
'home_state' => 'required_with:home_address,home_city,home_postcode,home_country',
'shipping_address' => 'required_with:shipping_city,shipping_postcode,shipping_country,shipping_state',
'shipping_city' => 'required_with:shipping_address,shipping_postcode,shipping_country,shipping_state',
'shipping_postcode' => 'required_with:shipping_address,shipping_city,shipping_country,shipping_state',
'shipping_country' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_state',
'shipping_state' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_country',
'billing_address' => 'required_with:billing_city,billing_postcode,billing_country,billing_state',
'billing_city' => 'required_with:billing_address,billing_postcode,billing_country,billing_state',
@@ -68,11 +69,11 @@ class AccountController extends Controller
'email.email' => __('validation.custom_messages.email_invalid'),
'phone.required' => __('validation.custom_messages.phone_required'),
'home_address.required' => __('validation.custom_messages.home_address_required'),
'home_city.required' => __('validation.custom_messages.home_city_required'),
'home_postcode.required' => __('validation.custom_messages.home_postcode_required'),
'home_country.required' => __('validation.custom_messages.home_country_required'),
'home_state.required' => __('validation.custom_messages.home_state_required'),
'shipping_address.required' => __('validation.custom_messages.shipping_address_required'),
'shipping_city.required' => __('validation.custom_messages.shipping_city_required'),
'shipping_postcode.required' => __('validation.custom_messages.shipping_postcode_required'),
'shipping_country.required' => __('validation.custom_messages.shipping_country_required'),
'shipping_state.required' => __('validation.custom_messages.shipping_state_required'),
'billing_address.required' => __('validation.custom_messages.billing_address_required'),
'billing_city.required' => __('validation.custom_messages.billing_city_required'),
@@ -130,4 +131,110 @@ class AccountController extends Controller
session()->flash('message-type', 'success');
return redirect()->route('index');
}
public static function getTFAInstance()
{
$tfa = new TwoFactorAuth(new QRCodeProvider(), 'STEMMechanics', 6, 30, Algorithm::Sha512);
$tfa->ensureCorrectTime();
return $tfa;
}
public function show_tfa()
{
$user = auth()->user();
if ($user->tfa_secret === null) {
$tfa = self::getTFAInstance();
$secret = $tfa->createSecret();
return response()->json([
'secret' => $secret,
]);
} else {
abort(404);
}
}
public function show_tfa_image(Request $request)
{
$user = auth()->user();
if ($user->tfa_secret === null && $request->has('secret')) {
$tfa = self::getTFAInstance();
$qrCodeProvider = new QRCodeProvider();
$qrCode = $qrCodeProvider->getQRCodeImage(
$tfa->getQRText($user->email, $request->get('secret')),
200
);
return response()->stream(function () use ($qrCode) {
echo $qrCode;
}, 200, ['Content-Type' => $qrCodeProvider->getMimeType()]);
} else {
abort(404);
}
}
public function post_tfa(Request $request)
{
$user = auth()->user();
if ($user->tfa_secret === null && $request->has('secret') && $request->has('code')) {
$secret = $request->get('secret');
$code = $request->get('code');
$tfa = self::getTFAInstance();
if ($tfa->verifyCode($secret, $code, 4)) {
$user->tfa_secret = $secret;
$user->save();
$codes = $user->generateBackupCodes();
return response()->json([
'success' => true,
'codes' => $codes
]);
} else {
return response()->json([
'success' => false,
]);
}
} else {
abort(403);
}
}
public function destroy_tfa(Request $request)
{
$user = auth()->user();
if ($user->tfa_secret !== null) {
$user->tfa_secret = null;
$user->save();
$user->backupCodes()->delete();
return response()->json([
'success' => true,
]);
} else {
abort(403);
}
}
public function post_tfa_reset_backup_codes(Request $request)
{
$user = auth()->user();
if ($user->tfa_secret !== null) {
$codes = $user->generateBackupCodes();
return response()->json([
'success' => true,
'codes' => $codes
]);
} else {
abort(403);
}
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Jobs\SendEmail;
use App\Mail\UserEmailUpdateConfirm;
use App\Mail\UserLogin;
use App\Mail\UserLoginBackupCode;
use App\Mail\UserRegister;
use App\Mail\UserWelcome;
use App\Models\Token;
@@ -47,13 +48,60 @@ class AuthController extends Controller
{
$request->validate([
'email' => 'required|email',
'captcha' => 'required_captcha',
], [
'email.required' => __('validation.custom_messages.email_required'),
'email.email' => __('validation.custom_messages.email_invalid'),
]);
$forceEmailLogin = false;
if($request->has('code')) {
$user = User::where('email', $request->email)->whereNotNull('email_verified_at')->first();
if($user) {
$tfa = AccountController::getTFAInstance();
if ($request->code && $tfa->verifyCode($user->tfa_secret, $request->code, 4)) {
$data = ['url' => session()->pull('url.intended', null)];
return $this->loginByUser($user, $data);
}
}
return view('auth.login-2fa', ['email' => $request->email])->withErrors([
'code' => 'The 2FA code is not valid',
]);
}
if($request->has('backup_code')) {
$user = User::where('email', $request->email)->whereNotNull('email_verified_at')->first();
if($user) {
if($user->verifyBackupCode($request->backup_code)) {
$data = ['url' => session()->pull('url.intended', null)];
dispatch(new SendEmail($user->email, new UserLoginBackupCode($user->email)))->onQueue('mail');
return $this->loginByUser($user, $data);
}
}
return view('auth.login-2fa', ['email' => $request->email, 'method' => 'backup'])->withErrors([
'backup_code' => 'The backup code is not valid',
]);
}
if($request->has('method')) {
if($request->get('method') === 'email') {
$forceEmailLogin = true;
} else {
abort(404);
}
}
$user = User::where('email', $request->email)->whereNotNull('email_verified_at')->first();
if($user) {
if ($user) {
if (!$forceEmailLogin && $user->tfa_secret !== null) {
return view('auth.login-2fa', ['user' => $user]);
}
$token = $user->tokens()->create([
'type' => 'login',
'data' => ['url' => session()->pull('url.intended', null)],
@@ -190,6 +238,7 @@ class AuthController extends Controller
{
$request->validate([
'email' => 'required|email',
'captcha' => 'required_captcha',
], [
'email.required' => __('validation.custom_messages.email_required'),
'email.email' => __('validation.custom_messages.email_invalid')

View File

@@ -1,261 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\Event;
use Carbon\Carbon;
use Illuminate\Http\Request;
class EventController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$homeView = true;
$search = $request->get('search', '');
$query = Event::query();
if(!auth()->user()?->admin) {
$query = $query->where('status', '!=', 'draft');
}
if($request->has('search') && $request->search !== '') {
$homeView = false;
$query = $query->where(function ($query) use ($request) {
$query->where('title', 'like', '%' . $request->search . '%')
->orWhere('content', 'like', '%' . $request->search . '%');
});
}
if($request->has('location') && $request->location !== '') {
$homeView = false;
$query = $query->whereHas('location', function ($query) use ($request) {
$query->where('name', 'like', '%' . $request->location . '%');
});
}
if($request->has('date') && $request->date !== '') {
$homeView = false;
$dates = explode('-', $request->date);
$dates = array_map('trim', $dates);
$dates = array_map(function($date) {
$date = trim($date);
if(preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
return $date;
}
if(preg_match('/^(\d{2})-(\d{2})-(\d{2})$/', $date, $matches)) {
return '20' . $matches[1] . '-' . $matches[2] . '-' . $matches[3];
}
if(preg_match('/^\d{4}-\d{2}$/', $date)) {
return $date . '-01';
}
if(preg_match('/^\d{4}$/', $date)) {
return $date . '-01-01';
}
if(preg_match('/^(\d{2})\/(\d{2})\/(\d{2})$/', $date, $matches)) {
return '20' . $matches[3] . '-' . $matches[2] . '-' . $matches[1];
}
if(preg_match('/^(\d{2})\/(\d{2})\/(\d{4})$/', $date, $matches)) {
return $matches[3] . '-' . $matches[2] . '-' . $matches[1];
}
return '';
}, $dates);
if(count($dates) == 2) {
// If there are two dates, filter between starts_at and ends_at
$query = $query->whereDate('starts_at', '>=', $dates[0])
->whereDate('ends_at', '<=', $dates[1]);
} else {
// If there is one date, filter starts_at that date or newer
$query = $query->whereDate('starts_at', '>=', $dates[0]);
}
}
if($homeView) {
$query = $query->where('starts_at', '>=', Carbon::now()->subDays(8))
->orderBy('starts_at', 'asc');
} else {
$query = $query->orderBy('starts_at', 'asc');
}
$events = $query->paginate(12);
return view('event.index', [
'events' => $events,
'search' => $search,
]);
}
/**
* Display a listing of the resource.
*/
public function admin_index(Request $request)
{
$query = Event::query();
if($request->has('search')) {
$query->where('title', 'like', '%' . $request->search . '%');
$query->orWhere('content', 'like', '%' . $request->search . '%');
}
$events = $query->orderBy('starts_at', 'desc')->paginate(12)->onEachSide(1);
return view('admin.event.index', [
'events' => $events
]);
}
/**
* Show the form for creating a new resource.
*/
public function admin_create()
{
return view('admin.event.edit');
}
/**
* Store a newly created resource in storage.
*/
public function admin_store(Request $request)
{
$request->validate([
'title' => 'required',
'content' => 'required',
'starts_at' => 'required',
'ends_at' => 'required|after:starts_at',
'publish_at' => 'required',
'closes_at' => 'required',
'status' => 'required',
'hero_media_name' => 'required|exists:media,name',
'registration_data' => 'required_unless:registration,none',
], [
'title.required' => __('validation.custom_messages.title_required'),
'content.required' => __('validation.custom_messages.content_required'),
'starts_at.required' => __('validation.custom_messages.starts_at_required'),
'ends_at.required' => __('validation.custom_messages.ends_at_required'),
'ends_at.after' => __('validation.custom_messages.ends_at_after'),
'publish_at.required' => __('validation.custom_messages.publish_at_required'),
'closes_at.required' => __('validation.custom_messages.closes_at_required'),
'status.required' => __('validation.custom_messages.status_required'),
'hero_media_name.required' => __('validation.custom_messages.hero_media_name_required'),
'hero_media_name.exists' => __('validation.custom_messages.hero_media_name_exists'),
'registration_data.required_unless' => __('validation.custom_messages.registration_data_required_unless'),
]);
$eventData = $request->all();
$eventData['user_id'] = auth()->user()->id;
if($eventData['status'] === 'open' && Carbon::parse($eventData['starts_at'])->lt(Carbon::now())) {
$eventData['status'] = 'closed';
}
$event = Event::create($eventData);
$event->updateFiles($request->input('files'));
session()->flash('message', 'Event has been created');
session()->flash('message-title', 'Event created');
session()->flash('message-type', 'success');
return redirect()->route('admin.event.index');
}
/**
* Display the specified resource.
*/
public function show(Event $event)
{
if(!auth()->user()?->admin && $event->status == 'draft') {
abort(404);
}
return view('event.show', ['event' => $event]);
}
/**
* Show the form for editing the specified resource.
*/
public function admin_edit(Event $event)
{
return view('admin.event.edit', ['event' => $event]);
}
/**
* Update the specified resource in storage.
*/
public function admin_update(Request $request, Event $event)
{
$request->validate([
'title' => 'required',
'content' => 'required',
'starts_at' => 'required',
'ends_at' => 'required|after:starts_at',
'publish_at' => 'required',
'closes_at' => 'required',
'status' => 'required',
'hero_media_name' => 'required|exists:media,name',
'registration_data' => 'required_unless:registration,none',
], [
'title.required' => __('validation.custom_messages.title_required'),
'content.required' => __('validation.custom_messages.content_required'),
'starts_at.required' => __('validation.custom_messages.starts_at_required'),
'ends_at.required' => __('validation.custom_messages.ends_at_required'),
'ends_at.after' => __('validation.custom_messages.ends_at_after'),
'publish_at.required' => __('validation.custom_messages.publish_at_required'),
'closes_at.required' => __('validation.custom_messages.closes_at_required'),
'status.required' => __('validation.custom_messages.status_required'),
'hero_media_name.required' => __('validation.custom_messages.hero_media_name_required'),
'hero_media_name.exists' => __('validation.custom_messages.hero_media_name_exists'),
'registration_data.required_unless' => __('validation.custom_messages.registration_data_required_unless'),
]);
$eventData = $request->all();
if($eventData['status'] === 'open' && Carbon::parse($eventData['starts_at'])->lt(Carbon::now())) {
$eventData['status'] = 'closed';
}
$event->update($eventData);
$event->updateFiles($request->input('files'));
session()->flash('message', 'Event has been updated');
session()->flash('message-title', 'Event updated');
session()->flash('message-type', 'success');
return redirect()->route('admin.event.index');
}
/**
* Remove the specified resource from storage.
*/
public function admin_destroy(Event $event)
{
$event->delete();
session()->flash('message', 'Event has been deleted');
session()->flash('message-title', 'Event deleted');
session()->flash('message-type', 'danger');
return redirect()->route('admin.event.index');
}
/**
* Duplicate the specified resource.
*/
public function admin_duplicate(Event $event)
{
$newWorkshop = $event->replicate();
$newWorkshop->title = $newWorkshop->title . ' (copy)';
$newWorkshop->status = 'draft';
$newWorkshop->save();
foreach($event->files as $file) {
$newWorkshop->files()->attach($file->name);
}
session()->flash('message', 'Event has been duplicated');
session()->flash('message-title', 'Event duplicated');
session()->flash('message-type', 'success');
return redirect()->route('admin.event.edit', $newWorkshop);
}
}

View File

@@ -3,18 +3,18 @@
namespace App\Http\Controllers;
use App\Models\Post;
use App\Models\Event;
use App\Models\Workshop;
class HomeController extends Controller
{
public function index()
{
$posts = Post::query()->orderBy('created_at', 'desc')->limit(4)->get();
$events = Event::query()->where('starts_at', '>', now())->where('status', '!=', 'private')->orderBy('starts_at', 'asc')->limit(4)->get();
// $posts = Post::query()->orderBy('created_at', 'desc')->limit(4)->get();
$workshops = Workshop::query()->where('starts_at', '>', now())->where('status', '!=', 'private')->orderBy('starts_at', 'asc')->limit(4)->get();
return view('home', [
'posts' => $posts,
'events' => $events,
// 'posts' => $posts,
'workshops' => $workshops,
]);
}
}

View File

@@ -111,15 +111,17 @@ class MediaController extends Controller
public function admin_store(Request $request)
{
$file = null;
$cleanupPath = null;
// Check if the endpoint received a file...
if($request->hasFile('file')) {
try {
$file = $this->upload($request);
if($file === true) {
if(is_array($file) && !empty($file['chunk'])) {
return response()->json([
'message' => 'Chunk stored',
'upload_token' => $file['token'] ?? null,
]);
} else if(!$file) {
return response()->json([
@@ -150,8 +152,20 @@ class MediaController extends Controller
}
// else check if it received a file name of a previous upload...
} else if($request->has('file')) {
$tempFileName = sys_get_temp_dir() . '/chunk-' . Auth::id() . '-' . $request->file;
} else if($request->has('upload_token') || $request->has('file')) {
$uploadToken = $request->input('upload_token', $request->input('file'));
$chunkUploads = session()->get('chunk_uploads', []);
if(!is_string($uploadToken) || !isset($chunkUploads[$uploadToken])) {
return response()->json([
'message' => 'Could not find the referenced file on the server.',
'errors' => [
'file' => 'Could not find the referenced file on the server.'
]
], 422);
}
$tempFileName = $chunkUploads[$uploadToken];
if(!file_exists($tempFileName)) {
return response()->json([
'message' => 'Could not find the referenced file on the server.',
@@ -165,7 +179,16 @@ class MediaController extends Controller
if($fileMime === false) {
$fileMime = 'application/octet-stream';
}
$file = new UploadedFile($tempFileName, $request->file, $fileMime, null, true);
$fileName = $request->input('filename', 'upload');
$fileName = Helpers::cleanFileName($fileName);
if ($fileName === '') {
$fileName = 'upload';
}
$file = new UploadedFile($tempFileName, $fileName, $fileMime, null, true);
$cleanupPath = $tempFileName;
unset($chunkUploads[$uploadToken]);
session()->put('chunk_uploads', $chunkUploads);
}
// Check there is an actual file
@@ -242,7 +265,13 @@ class MediaController extends Controller
}
}
unlink($file->getRealPath());
if(is_string($cleanupPath)) {
$realPath = realpath($cleanupPath);
$tempDir = realpath(sys_get_temp_dir());
if($realPath !== false && $tempDir !== false && str_starts_with($realPath, $tempDir . DIRECTORY_SEPARATOR)) {
@unlink($realPath);
}
}
if($request->wantsJson()) {
return response()->json([
@@ -386,6 +415,10 @@ class MediaController extends Controller
$fileName = $request->input('filename', $file->getClientOriginalName());
$fileName = Helpers::cleanFileName($fileName);
if ($fileName === '') {
$extension = strtolower($file->getClientOriginalExtension());
$fileName = 'upload' . ($extension !== '' ? '.' . $extension : '');
}
if(($request->has('filestart') || $request->has('fileappend')) && $request->has('filesize')) {
$fileSize = $request->get('filesize');
@@ -394,7 +427,25 @@ class MediaController extends Controller
throw new FileTooLargeException('The file is larger than the maximum size allowed of ' . Helpers::bytesToString($max_size));
}
$tempFilePath = sys_get_temp_dir() . '/chunk-' . Auth::id() . '-' . $fileName;
$chunkUploads = session()->get('chunk_uploads', []);
$uploadToken = $request->input('upload_token');
if($request->has('filestart')) {
$uploadToken = bin2hex(random_bytes(16));
$tempFilePath = tempnam(sys_get_temp_dir(), 'chunk-' . Auth::id() . '-');
if($tempFilePath === false) {
throw new FileInvalidException('Unable to create a temporary upload file.');
}
$chunkUploads[$uploadToken] = $tempFilePath;
session()->put('chunk_uploads', $chunkUploads);
} else {
if(!is_string($uploadToken) || !isset($chunkUploads[$uploadToken])) {
throw new FileInvalidException('Invalid upload token.');
}
$tempFilePath = $chunkUploads[$uploadToken];
}
$filemode = 'a';
if($request->has('filestart')) {
@@ -415,9 +466,17 @@ class MediaController extends Controller
$fileMime = 'application/octet-stream';
}
if(is_string($uploadToken) && isset($chunkUploads[$uploadToken])) {
unset($chunkUploads[$uploadToken]);
session()->put('chunk_uploads', $chunkUploads);
}
return new UploadedFile($tempFilePath, $fileName, $fileMime, null, true);
} else {
return true;
return [
'chunk' => true,
'token' => $uploadToken,
];
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use App\Models\Workshop;
use Carbon\Carbon;
use Illuminate\Http\Request;
class SearchController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$search = $request->get('q', '');
$search_words = explode(' ', $search); // Split the search query into words[1]
$workshopQuery = Workshop::query()->where('status', '!=', 'draft');
$workshopQuery->where(function ($query) use ($search_words) {
foreach ($search_words as $word) {
$query->orWhere(function ($subQuery) use ($word) {
$subQuery->where('title', 'like', '%' . $word . '%')
->orWhere('content', 'like', '%' . $word . '%')
->orWhereHas('location', function ($locationQuery) use ($word) {
$locationQuery->where('name', 'like', '%' . $word . '%');
});
});
}
});
$workshops = $workshopQuery->orderBy('starts_at', 'desc')
->paginate(6, ['*'], 'workshop');
// $postQuery = Post::query()->where('status', 'published');
// $postQuery->where(function ($query) use ($search_words) {
// foreach ($search_words as $word) {
// $query->where(function ($subQuery) use ($word) {
// $subQuery->where('title', 'like', '%' . $word . '%')
// ->orWhere('content', 'like', '%' . $word . '%');
// });
// }
// });
//
// $posts = $postQuery->orderBy('created_at', 'desc')
// ->paginate(6, ['*'], 'post')
// ->onEachSide(1);
return view('search', [
'workshops' => $workshops,
// 'posts' => $posts,
'search' => $search,
]);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers;
use App\Models\EmailSubscriptions;
use App\Models\SentEmail;
use Illuminate\Http\Request;
class SubscribeController extends Controller
{
/**
* Display a listing of the resource.
*/
public function destroy($email)
{
$emailModel = SentEmail::where('id', $email)->first();
if (!$emailModel) {
// Email not found, redirect to home page with a message
return redirect()->route('index')->with([
'message' => 'The unsubscribe link is invalid or has expired.',
'message-title' => 'Invalid Unsubscribe Link',
'message-type' => 'warning'
]);
}
// Existing unsubscribe logic
$subscriptions = EmailSubscriptions::where('email', $emailModel->recipient)->get();
if ($subscriptions->isEmpty()) {
session()->flash('message', 'You are already unsubscribed.');
session()->flash('message-title', 'Already Unsubscribed');
session()->flash('message-type', 'info');
} else {
EmailSubscriptions::where('email', $emailModel->recipient)->delete();
session()->flash('message', 'You have been successfully unsubscribed.');
session()->flash('message-title', 'Unsubscribed');
session()->flash('message-type', 'success');
}
return redirect()->route('index');
}
}

View File

@@ -49,11 +49,11 @@ class UserController extends Controller
'email' => 'email|unique:users',
'phone' => '',
'home_address' => 'required_with:home_city,home_postcode,home_country,home_state',
'home_city' => 'required_with:home_address,home_postcode,home_country,home_state',
'home_postcode' => 'required_with:home_address,home_city,home_country,home_state',
'home_country' => 'required_with:home_address,home_city,home_postcode,home_state',
'home_state' => 'required_with:home_address,home_city,home_postcode,home_country',
'shipping_address' => 'required_with:shipping_city,shipping_postcode,shipping_country,shipping_state',
'shipping_city' => 'required_with:shipping_address,shipping_postcode,shipping_country,shipping_state',
'shipping_postcode' => 'required_with:shipping_address,shipping_city,shipping_country,shipping_state',
'shipping_country' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_state',
'shipping_state' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_country',
'billing_address' => 'required_with:billing_city,billing_postcode,billing_country,billing_state',
'billing_city' => 'required_with:billing_address,billing_postcode,billing_country,billing_state',
@@ -67,11 +67,11 @@ class UserController extends Controller
'email.email' => __('validation.custom_messages.email_invalid'),
'phone.required' => __('validation.custom_messages.phone_required'),
'home_address.required' => __('validation.custom_messages.home_address_required'),
'home_city.required' => __('validation.custom_messages.home_city_required'),
'home_postcode.required' => __('validation.custom_messages.home_postcode_required'),
'home_country.required' => __('validation.custom_messages.home_country_required'),
'home_state.required' => __('validation.custom_messages.home_state_required'),
'shipping_address.required' => __('validation.custom_messages.shipping_address_required'),
'shipping_city.required' => __('validation.custom_messages.shipping_city_required'),
'shipping_postcode.required' => __('validation.custom_messages.shipping_postcode_required'),
'shipping_country.required' => __('validation.custom_messages.shipping_country_required'),
'shipping_state.required' => __('validation.custom_messages.shipping_state_required'),
'billing_address.required' => __('validation.custom_messages.billing_address_required'),
'billing_city.required' => __('validation.custom_messages.billing_city_required'),
@@ -107,11 +107,11 @@ class UserController extends Controller
'email' => ['email', Rule::unique('users')->ignore($user->id)],
'phone' => '',
'home_address' => 'required_with:home_city,home_postcode,home_country,home_state',
'home_city' => 'required_with:home_address,home_postcode,home_country,home_state',
'home_postcode' => 'required_with:home_address,home_city,home_country,home_state',
'home_country' => 'required_with:home_address,home_city,home_postcode,home_state',
'home_state' => 'required_with:home_address,home_city,home_postcode,home_country',
'shipping_address' => 'required_with:shipping_city,shipping_postcode,shipping_country,shipping_state',
'shipping_city' => 'required_with:shipping_address,shipping_postcode,shipping_country,shipping_state',
'shipping_postcode' => 'required_with:shipping_address,shipping_city,shipping_country,shipping_state',
'shipping_country' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_state',
'shipping_state' => 'required_with:shipping_address,shipping_city,shipping_postcode,shipping_country',
'billing_address' => 'required_with:billing_city,billing_postcode,billing_country,billing_state',
'billing_city' => 'required_with:billing_address,billing_postcode,billing_country,billing_state',
@@ -125,11 +125,11 @@ class UserController extends Controller
'email.email' => __('validation.custom_messages.email_invalid'),
'phone.required' => __('validation.custom_messages.phone_required'),
'home_address.required' => __('validation.custom_messages.home_address_required'),
'home_city.required' => __('validation.custom_messages.home_city_required'),
'home_postcode.required' => __('validation.custom_messages.home_postcode_required'),
'home_country.required' => __('validation.custom_messages.home_country_required'),
'home_state.required' => __('validation.custom_messages.home_state_required'),
'shipping_address.required' => __('validation.custom_messages.shipping_address_required'),
'shipping_city.required' => __('validation.custom_messages.shipping_city_required'),
'shipping_postcode.required' => __('validation.custom_messages.shipping_postcode_required'),
'shipping_country.required' => __('validation.custom_messages.shipping_country_required'),
'shipping_state.required' => __('validation.custom_messages.shipping_state_required'),
'billing_address.required' => __('validation.custom_messages.billing_address_required'),
'billing_city.required' => __('validation.custom_messages.billing_city_required'),

View File

@@ -0,0 +1,209 @@
<?php
namespace App\Http\Controllers;
use App\Models\Workshop;
use Carbon\Carbon;
use Illuminate\Http\Request;
class WorkshopController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$query = Workshop::query();
$query = $query->where('starts_at', '>=', Carbon::now()->subDays(8))
->orderBy('starts_at', 'asc');
$workshops = $query->paginate(12);
return view('workshop.index', [
'workshops' => $workshops
]);
}
/**
* Display a listing of the resource.
*/
public function past_index()
{
$query = Workshop::query();
$query = $query->where('starts_at', '<', Carbon::now())
->orderBy('starts_at', 'desc');
$workshops = $query->paginate(12);
return view('workshop.index', [
'workshops' => $workshops
]);
}
/**
* Display a listing of the resource.
*/
public function admin_index(Request $request)
{
$query = Workshop::query();
if($request->has('search')) {
$query->where('title', 'like', '%' . $request->search . '%');
$query->orWhere('content', 'like', '%' . $request->search . '%');
}
$workshops = $query->orderBy('starts_at', 'desc')->paginate(12)->onEachSide(1);
return view('admin.workshop.index', [
'workshops' => $workshops
]);
}
/**
* Show the form for creating a new resource.
*/
public function admin_create()
{
return view('admin.workshop.edit');
}
/**
* Store a newly created resource in storage.
*/
public function admin_store(Request $request)
{
$request->validate([
'title' => 'required',
'content' => 'required',
'starts_at' => 'required',
'ends_at' => 'required|after:starts_at',
'publish_at' => 'required',
'closes_at' => 'required',
'status' => 'required',
'hero_media_name' => 'required|exists:media,name',
'registration_data' => 'required_unless:registration,none',
], [
'title.required' => __('validation.custom_messages.title_required'),
'content.required' => __('validation.custom_messages.content_required'),
'starts_at.required' => __('validation.custom_messages.starts_at_required'),
'ends_at.required' => __('validation.custom_messages.ends_at_required'),
'ends_at.after' => __('validation.custom_messages.ends_at_after'),
'publish_at.required' => __('validation.custom_messages.publish_at_required'),
'closes_at.required' => __('validation.custom_messages.closes_at_required'),
'status.required' => __('validation.custom_messages.status_required'),
'hero_media_name.required' => __('validation.custom_messages.hero_media_name_required'),
'hero_media_name.exists' => __('validation.custom_messages.hero_media_name_exists'),
'registration_data.required_unless' => __('validation.custom_messages.registration_data_required_unless'),
]);
$workshopData = $request->all();
$workshopData['user_id'] = auth()->user()->id;
if($workshopData['status'] === 'open' && Carbon::parse($workshopData['starts_at'])->lt(Carbon::now())) {
$workshopData['status'] = 'closed';
}
$workshop = Workshop::create($workshopData);
$workshop->updateFiles($request->input('files'));
session()->flash('message', 'Workshop has been created');
session()->flash('message-title', 'Workshop created');
session()->flash('message-type', 'success');
return redirect()->route('admin.workshop.index');
}
/**
* Display the specified resource.
*/
public function show(Workshop $workshop)
{
if(!auth()->user()?->admin && $workshop->status == 'draft') {
abort(404);
}
return view('workshop.show', ['workshop' => $workshop]);
}
/**
* Show the form for editing the specified resource.
*/
public function admin_edit(Workshop $workshop)
{
return view('admin.workshop.edit', ['workshop' => $workshop]);
}
/**
* Update the specified resource in storage.
*/
public function admin_update(Request $request, Workshop $workshop)
{
$request->validate([
'title' => 'required',
'content' => 'required',
'starts_at' => 'required',
'ends_at' => 'required|after:starts_at',
'publish_at' => 'required',
'closes_at' => 'required',
'status' => 'required',
'hero_media_name' => 'required|exists:media,name',
'registration_data' => 'required_unless:registration,none',
], [
'title.required' => __('validation.custom_messages.title_required'),
'content.required' => __('validation.custom_messages.content_required'),
'starts_at.required' => __('validation.custom_messages.starts_at_required'),
'ends_at.required' => __('validation.custom_messages.ends_at_required'),
'ends_at.after' => __('validation.custom_messages.ends_at_after'),
'publish_at.required' => __('validation.custom_messages.publish_at_required'),
'closes_at.required' => __('validation.custom_messages.closes_at_required'),
'status.required' => __('validation.custom_messages.status_required'),
'hero_media_name.required' => __('validation.custom_messages.hero_media_name_required'),
'hero_media_name.exists' => __('validation.custom_messages.hero_media_name_exists'),
'registration_data.required_unless' => __('validation.custom_messages.registration_data_required_unless'),
]);
$workshopData = $request->all();
if($workshopData['status'] === 'open' && Carbon::parse($workshopData['starts_at'])->lt(Carbon::now())) {
$workshopData['status'] = 'closed';
}
$workshop->update($workshopData);
$workshop->updateFiles($request->input('files'));
session()->flash('message', 'Workshop has been updated');
session()->flash('message-title', 'Workshop updated');
session()->flash('message-type', 'success');
return redirect()->route('admin.workshop.index');
}
/**
* Remove the specified resource from storage.
*/
public function admin_destroy(Workshop $workshop)
{
$workshop->delete();
session()->flash('message', 'Workshop has been deleted');
session()->flash('message-title', 'Workshop deleted');
session()->flash('message-type', 'danger');
return redirect()->route('admin.workshop.index');
}
/**
* Duplicate the specified resource.
*/
public function admin_duplicate(Workshop $workshop)
{
$newWorkshop = $workshop->replicate();
$newWorkshop->title = $newWorkshop->title . ' (copy)';
$newWorkshop->status = 'draft';
$newWorkshop->save();
foreach($workshop->files as $file) {
$newWorkshop->files()->attach($file->name);
}
session()->flash('message', 'Workshop has been duplicated');
session()->flash('message-title', 'Workshop duplicated');
session()->flash('message-type', 'success');
return redirect()->route('admin.workshop.edit', $newWorkshop);
}
}

View File

@@ -2,12 +2,14 @@
namespace App\Jobs;
use App\Models\SentEmail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
class SendEmail implements ShouldQueue
@@ -48,6 +50,18 @@ class SendEmail implements ShouldQueue
*/
public function handle(): void
{
// Record sent email
$sentEmail = SentEmail::create([
'recipient' => $this->to,
'mailable_class' => get_class($this->mailable)
]);
// Add unsubscribe link if mailable supports it
if (method_exists($this->mailable, 'withUnsubscribeLink')) {
$unsubscribeLink = route('unsubscribe', ['email' => $sentEmail->id]);
$this->mailable->withUnsubscribeLink($unsubscribeLink);
}
Mail::to($this->to)->send($this->mailable);
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace App\Livewire;
use App\Jobs\SendEmail;
use Carbon\Carbon;
use Livewire\Component;
use App\Models\EmailSubscriptions;
use App\Mail\UserWelcome;
class EmailSubscribe extends Component
{
public string $email = '';
public bool $success = false;
public string $message = '';
public string $trap = '';
public int $renderedAt; // unix timestamp
protected $rules = [
'email' => 'required|email|max:255',
];
public function mount()
{
$this->renderedAt = now()->timestamp;
}
public function subscribe(): void
{
$this->validate();
// 1. Honeypot - if this hidden field is filled, treat as success but do nothing
if (! empty($this->trap)) {
$this->reset(['email', 'trap']);
$this->success = true;
$this->message = 'Thanks, you have been subscribed to our newsletter.';
return;
}
// 2. Block submits in first 10 seconds after render
if (now()->timestamp - $this->renderedAt < 4) {
$this->success = false;
$this->message = 'That was a bit quick. Please wait a few seconds and try again.';
return;
}
// 3. Enforce 30 seconds between attempts per session
$lastAttempt = session('subscribe_last_attempt'); // int timestamp or null
if (! is_int($lastAttempt)) {
$lastAttempt = null;
}
$now = time();
if ($lastAttempt && ($now - $lastAttempt) < 20) {
$this->success = false;
$this->message = 'Please wait a little before trying again.';
return;
}
session(['subscribe_last_attempt' => $now]);
// 4. Limit to 5 attempts per session (your existing logic)
$attempts = session('subscribe_attempts', 0);
if ($attempts >= 5) {
$this->success = false;
$this->message = 'Too many attempts. Please try again in a little while.';
return;
}
session(['subscribe_attempts' => $attempts + 1]);
// Look up existing subscription by email
$subscription = EmailSubscriptions::where('email', $this->email)->first();
// If already confirmed, do not create a new record or resend confirmation
if ($subscription && $subscription->confirmed) {
// Optionally you could set a different flag or message here
$this->success = false;
$this->message = 'That email is already subscribed to our newsletter.';
} else {
// If no subscription exists, create a new unconfirmed one
if (!$subscription) {
$subscription = EmailSubscriptions::create([
'email' => $this->email,
'confirmed' => Carbon::now()
]);
$subscription->save();
}
dispatch(new SendEmail($subscription->email, new UserWelcome($subscription->email)))->onQueue('mail');
$this->success = true;
$this->message = 'Thanks, you have been subscribed to our newsletter.';
}
$this->reset(['email', 'trap']);
}
public function render()
{
return view('livewire.email-subscribe');
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Mail;
use App\Models\Workshop;
use App\Traits\HasUnsubscribeLink;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
class UpcomingWorkshops extends Mailable
{
use Queueable, SerializesModels, HasUnsubscribeLink;
public $subject;
public $email;
public $workshops;
public function __construct($email, $subject = 'Upcoming Workshops 🌟')
{
$this->subject = $subject;
$this->email = $email;
$this->workshops = $this->getUpcomingWorkshops();
}
private function getUpcomingWorkshops()
{
$startDate = Carbon::now()->addDays(3);
$endDate = Carbon::now()->addDays(42);
return Workshop::select('workshops.*', 'locations.name as location_name')
->join('locations', 'workshops.location_id', '=', 'locations.id')
->whereIn('workshops.status', ['open','scheduled'])
->whereBetween('workshops.starts_at', [$startDate, $endDate])
->where('locations.name', 'not like', '%private%')
->orderBy('locations.name')
->orderBy('workshops.starts_at')
->get();
}
public function build()
{
// Bail if there are no upcoming workshops
if ($this->workshops->isEmpty()) {
return false;
}
return $this
->subject($this->subject)
->markdown('emails.upcoming-workshops')
->with([
'email' => $this->email,
'workshops' => $this->workshops,
'unsubscribeLink' => $this->unsubscribeLink
]);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Mail;
use App\Models\Ticket;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Spatie\LaravelPdf\Facades\Pdf;
class UserLoginBackupCode extends Mailable
{
use Queueable, SerializesModels;
public $email;
public function __construct($email)
{
$this->email = $email;
}
public function build()
{
return $this
->subject('Hey, did you recently log in?')
->markdown('emails.login-backup-code')
->with([
'email' => $this->email,
]);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Mail;
use App\Models\Ticket;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Spatie\LaravelPdf\Facades\Pdf;
class UserLoginTFADisabled extends Mailable
{
use Queueable, SerializesModels;
public $email;
public function __construct($email)
{
$this->email = $email;
}
public function build()
{
return $this
->subject('Two-factor authentication disabled on your account')
->markdown('emails.login-tfa-disabled')
->with([
'email' => $this->email,
]);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Mail;
use App\Models\Ticket;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Spatie\LaravelPdf\Facades\Pdf;
class UserLoginTFAEnabled extends Mailable
{
use Queueable, SerializesModels;
public $email;
public function __construct($email)
{
$this->email = $email;
}
public function build()
{
return $this
->subject('Two-factor authentication enabled on your account')
->markdown('emails.login-tfa-enabled')
->with([
'email' => $this->email,
]);
}
}

View File

@@ -2,14 +2,14 @@
namespace App\Mail;
use App\Traits\HasUnsubscribeLink;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class UserWelcome extends Mailable
{
use Queueable, SerializesModels;
use Queueable, SerializesModels, HasUnsubscribeLink;
public $email;
@@ -25,6 +25,7 @@ class UserWelcome extends Mailable
->markdown('emails.welcome')
->with([
'email' => $this->email,
'unsubscribeLink' => $this->unsubscribeLink
]);
}
}

View File

@@ -7,8 +7,6 @@ use Illuminate\Database\Eloquent\Model;
class EmailSubscriptions extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*

30
app/Models/SentEmail.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class SentEmail extends Model
{
protected $fillable = ['recipient', 'mailable_class'];
public $incrementing = false;
protected $keyType = 'string';
/**
* Boot function from Laravel.
*
* @return void
*/
protected static function boot(): void
{
parent::boot();
static::creating(function ($model) {
if (empty($model->{$model->getKeyName()}) === true) {
$model->{$model->getKeyName()} = strtolower(Str::random(15));
}
});
}
}

View File

@@ -2,12 +2,16 @@
namespace App\Models;
use App\Jobs\SendEmail;
use App\Mail\UserLoginTFADisabled;
use App\Mail\UserLoginTFAEnabled;
use App\Traits\UUID;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;
class User extends Authenticatable implements MustVerifyEmail
{
@@ -24,19 +28,21 @@ class User extends Authenticatable implements MustVerifyEmail
'surname',
'email',
'phone',
'home_address',
'home_address2',
'home_city',
'home_postcode',
'home_state',
'home_country',
'shipping_address',
'shipping_address2',
'shipping_city',
'shipping_postcode',
'shipping_state',
'shipping_country',
'billing_address',
'billing_address2',
'billing_city',
'billing_postcode',
'billing_state',
'billing_country',
'subscribed'
'subscribed',
'tfa_secret',
'agree_tos',
];
/**
@@ -47,6 +53,7 @@ class User extends Authenticatable implements MustVerifyEmail
protected $hidden = [
'password',
'remember_token',
'tfa_secret'
];
/**
@@ -98,6 +105,15 @@ class User extends Authenticatable implements MustVerifyEmail
}
}
}
if ($user->isDirty('tfa_secret')) {
if($user->tfa_secret === null) {
$user->backupCodes()->delete();
dispatch(new SendEmail($user->email, new UserLoginTFADisabled($user->email)))->onQueue('mail');
} else {
dispatch(new SendEmail($user->email, new UserLoginTFAEnabled($user->email)))->onQueue('mail');
}
}
});
static::deleting(function ($user) {
@@ -176,4 +192,38 @@ class User extends Authenticatable implements MustVerifyEmail
{
return $this->admin === 1;
}
public function backupCodes()
{
return $this->hasMany(UserBackupCode::class);
}
public function generateBackupCodes()
{
$this->backupCodes()->delete();
$codes = [];
for ($i = 0; $i < 10; $i++) {
$code = strtoupper(bin2hex(random_bytes(4)));
$codes[] = $code;
UserBackupCode::create([
'user_id' => $this->id,
'code' => $code,
]);
}
return $codes;
}
public function verifyBackupCode($code)
{
$backupCodes = $this->backupCodes()->get();
foreach ($backupCodes as $backupCode) {
if (Hash::check($code, $backupCode->code)) {
$backupCode->delete();
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
class UserBackupCode extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'user_id',
'code'
];
/**
* Set the code attribute and automatically hash the code.
*
* @param string $value
* @return void
*/
public function setCodeAttribute($value)
{
$this->attributes['code'] = Hash::make($value);
}
/**
* Verify the given code against the stored hashed code.
*
* @param string $value
* @return bool
*/
public function verify($value)
{
return Hash::check($value, $this->code);
}
}

View File

@@ -7,7 +7,7 @@ use App\Traits\Slug;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Event extends Model
class Workshop extends Model
{
use HasFactory, Slug, HasFiles;

View File

@@ -3,7 +3,7 @@
namespace App\Providers;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@@ -21,6 +21,10 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
if ($this->app->environment('production')) {
URL::forceScheme('https');
}
Blade::directive('includeSVG', function ($arguments) {
list($path, $styles) = array_pad(explode(',', str_replace(['(', ')', ' ', "'"], '', $arguments), 2), 2, '');
$svgContent = file_get_contents(public_path($path));

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;
class CaptchaServiceProvider extends ServiceProvider
{
private string $captchaKey = '6Lc6BIAUAAAAAABZzv6J9ZQ7J9Zzv6J9ZQ7J9Zzv';
private int $timeThreshold = 750;
/**
* Register services.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*/
public function boot(): void
{
Blade::directive('captcha', function () {
return <<<EOT
<input type="text" name="captcha" autocomplete="off" style="position:absolute;left:-9999px;top:-9999px">
<script>
document.addEventListener('DOMContentLoaded', function() {
const errors = {!! json_encode(\$errors->getMessages()) !!};
if(errors && errors.captcha && errors.captcha.length) {
SM.alert('', errors.captcha[0], 'danger');
}
});
</script>
EOT;
});
Blade::directive('captchaScripts', function () {
return <<<EOT
<script>
document.addEventListener('DOMContentLoaded', function() {
window.setTimeout(function() {
const captchaList = document.querySelectorAll('input[name="captcha"]');
captchaList.forEach(function(captcha) {
if(captcha.value === '') {
captcha.value = '$this->captchaKey';
}
});
}, $this->timeThreshold);
});
</script>
EOT;
});
Validator::extend('required_captcha', function ($attribute, $value, $parameters, $validator) {
return $value === $this->captchaKey;
}, 'The form captcha failed to validate. Please try again.');
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Providers;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
class QRCodeProvider implements IQRCodeProvider
{
public function getMimeType(): string
{
return 'image/svg+xml';
}
public function getQRCodeImage(string $qrText, int $size): string
{
$options = new QROptions;
$options->outputBase64 = false;
$options->imageTransparent = true;
return (new QRCode($options))->render($qrText);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Traits;
trait HasUnsubscribeLink
{
protected $unsubscribeLink;
public function withUnsubscribeLink($link)
{
$this->unsubscribeLink = $link;
return $this;
}
}

View File

@@ -6,6 +6,8 @@ use Illuminate\Support\Str;
trait Slug
{
protected $appendsSlug = ['slug'];
/**
* Boot function from Laravel.
*
@@ -20,6 +22,16 @@ trait Slug
});
}
/**
* Initialize the trait.
*
* @return void
*/
public function initializeSlug(): void
{
$this->appends = array_merge($this->appends ?? [], $this->appendsSlug);
}
/**
* Get the value indicating whether the IDs are incrementing.
*
@@ -47,7 +59,7 @@ trait Slug
*/
public function getRouteKey()
{
return $this->slug();
return $this->slug;
}
/**
@@ -68,7 +80,7 @@ trait Slug
*
* @return string
*/
public function slug()
public function getSlugAttribute()
{
return Str::slug($this->title) . '-' . $this->id;
}

0
artisan Executable file → Normal file
View File

View File

@@ -2,4 +2,5 @@
return [
App\Providers\AppServiceProvider::class,
App\Providers\CaptchaServiceProvider::class,
];

View File

@@ -5,22 +5,24 @@
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.2",
"php": "^8.4",
"ext-imagick": "*",
"chillerlan/php-qrcode": "^5.0",
"gehrisandro/tailwind-merge-laravel": "^1.2",
"intervention/image": "^3.5",
"laravel/framework": "^11.0",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.9",
"livewire/livewire": "^3.4",
"livewire/livewire": "^4.1",
"php-ffmpeg/php-ffmpeg": "^1.2",
"ext-imagick": "*"
"robthree/twofactorauth": "^3.0"
},
"require-dev": {
"roave/security-advisories": "dev-latest",
"fakerphp/faker": "^1.23",
"laravel/pint": "^1.13",
"laravel/sail": "^1.26",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.0",
"phpunit/phpunit": "^10.5",
"phpunit/phpunit": "^12.5",
"spatie/laravel-ignition": "^2.4"
},
"autoload": {
@@ -50,6 +52,12 @@
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"pint": [
"./vendor/bin/pint"
],
"pint-test": [
"./vendor/bin/pint --test"
]
},
"extra": {
@@ -58,6 +66,9 @@
}
},
"config": {
"platform": {
"php": "8.4.17"
},
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,

4868
composer.lock generated

File diff suppressed because it is too large Load Diff

80
config/flare.php Normal file
View File

@@ -0,0 +1,80 @@
<?php
use Spatie\FlareClient\FlareMiddleware\AddGitInformation;
use Spatie\FlareClient\FlareMiddleware\RemoveRequestIp;
use Spatie\FlareClient\FlareMiddleware\CensorRequestBodyFields;
use Spatie\FlareClient\FlareMiddleware\CensorRequestHeaders;
use Spatie\LaravelIgnition\FlareMiddleware\AddDumps;
use Spatie\LaravelIgnition\FlareMiddleware\AddEnvironmentInformation;
use Spatie\LaravelIgnition\FlareMiddleware\AddExceptionInformation;
use Spatie\LaravelIgnition\FlareMiddleware\AddJobs;
use Spatie\LaravelIgnition\FlareMiddleware\AddLogs;
use Spatie\LaravelIgnition\FlareMiddleware\AddQueries;
use Spatie\LaravelIgnition\FlareMiddleware\AddNotifierName;
return [
/*
|
|--------------------------------------------------------------------------
| Flare API key
|--------------------------------------------------------------------------
|
| Specify Flare's API key below to enable error reporting to the service.
|
| More info: https://flareapp.io/docs/general/projects
|
*/
'key' => env('FLARE_KEY'),
/*
|--------------------------------------------------------------------------
| Middleware
|--------------------------------------------------------------------------
|
| These middleware will modify the contents of the report sent to Flare.
|
*/
'flare_middleware' => [
RemoveRequestIp::class,
AddGitInformation::class,
AddNotifierName::class,
AddEnvironmentInformation::class,
AddExceptionInformation::class,
AddDumps::class,
AddLogs::class => [
'maximum_number_of_collected_logs' => 200,
],
AddQueries::class => [
'maximum_number_of_collected_queries' => 200,
'report_query_bindings' => true,
],
AddJobs::class => [
'max_chained_job_reporting_depth' => 5,
],
CensorRequestBodyFields::class => [
'censor_fields' => [
'password',
'password_confirmation',
],
],
CensorRequestHeaders::class => [
'headers' => [
'API-KEY',
]
]
],
/*
|--------------------------------------------------------------------------
| Reporting log statements
|--------------------------------------------------------------------------
|
| If this setting is `false` log statements won't be sent as events to Flare,
| no matter which error level you specified in the Flare log channel.
|
*/
'send_logs_as_events' => true,
];

277
config/ignition.php Normal file
View File

@@ -0,0 +1,277 @@
<?php
use Spatie\Ignition\Solutions\SolutionProviders\BadMethodCallSolutionProvider;
use Spatie\Ignition\Solutions\SolutionProviders\MergeConflictSolutionProvider;
use Spatie\Ignition\Solutions\SolutionProviders\UndefinedPropertySolutionProvider;
use Spatie\LaravelIgnition\Recorders\DumpRecorder\DumpRecorder;
use Spatie\LaravelIgnition\Recorders\JobRecorder\JobRecorder;
use Spatie\LaravelIgnition\Recorders\LogRecorder\LogRecorder;
use Spatie\LaravelIgnition\Recorders\QueryRecorder\QueryRecorder;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\DefaultDbNameSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\GenericLaravelExceptionSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\IncorrectValetDbCredentialsSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\InvalidRouteActionSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingAppKeySolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingColumnSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingImportSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingLivewireComponentSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingMixManifestSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingViteManifestSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\RunningLaravelDuskInProductionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\TableNotFoundSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\UndefinedViewVariableSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\UnknownValidationSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\ViewNotFoundSolutionProvider;
use Spatie\LaravelIgnition\Solutions\SolutionProviders\OpenAiSolutionProvider;
return [
/*
|--------------------------------------------------------------------------
| Editor
|--------------------------------------------------------------------------
|
| Choose your preferred editor to use when clicking any edit button.
|
| Supported: "phpstorm", "vscode", "vscode-insiders", "textmate", "emacs",
| "sublime", "atom", "nova", "macvim", "idea", "netbeans",
| "xdebug", "phpstorm-remote"
|
*/
'editor' => env('IGNITION_EDITOR', 'phpstorm'),
/*
|--------------------------------------------------------------------------
| Theme
|--------------------------------------------------------------------------
|
| Here you may specify which theme Ignition should use.
|
| Supported: "light", "dark", "auto"
|
*/
'theme' => env('IGNITION_THEME', 'auto'),
/*
|--------------------------------------------------------------------------
| Sharing
|--------------------------------------------------------------------------
|
| You can share local errors with colleagues or others around the world.
| Sharing is completely free and doesn't require an account on Flare.
|
| If necessary, you can completely disable sharing below.
|
*/
'enable_share_button' => env('IGNITION_SHARING_ENABLED', true),
/*
|--------------------------------------------------------------------------
| Register Ignition commands
|--------------------------------------------------------------------------
|
| Ignition comes with an additional make command that lets you create
| new solution classes more easily. To keep your default Laravel
| installation clean, this command is not registered by default.
|
| You can enable the command registration below.
|
*/
'register_commands' => env('REGISTER_IGNITION_COMMANDS', false),
/*
|--------------------------------------------------------------------------
| Solution Providers
|--------------------------------------------------------------------------
|
| You may specify a list of solution providers (as fully qualified class
| names) that shouldn't be loaded. Ignition will ignore these classes
| and possible solutions provided by them will never be displayed.
|
*/
'solution_providers' => [
// from spatie/ignition
BadMethodCallSolutionProvider::class,
MergeConflictSolutionProvider::class,
UndefinedPropertySolutionProvider::class,
// from spatie/laravel-ignition
IncorrectValetDbCredentialsSolutionProvider::class,
MissingAppKeySolutionProvider::class,
DefaultDbNameSolutionProvider::class,
TableNotFoundSolutionProvider::class,
MissingImportSolutionProvider::class,
InvalidRouteActionSolutionProvider::class,
ViewNotFoundSolutionProvider::class,
RunningLaravelDuskInProductionProvider::class,
MissingColumnSolutionProvider::class,
UnknownValidationSolutionProvider::class,
MissingMixManifestSolutionProvider::class,
MissingViteManifestSolutionProvider::class,
MissingLivewireComponentSolutionProvider::class,
UndefinedViewVariableSolutionProvider::class,
GenericLaravelExceptionSolutionProvider::class,
OpenAiSolutionProvider::class,
],
/*
|--------------------------------------------------------------------------
| Ignored Solution Providers
|--------------------------------------------------------------------------
|
| You may specify a list of solution providers (as fully qualified class
| names) that shouldn't be loaded. Ignition will ignore these classes
| and possible solutions provided by them will never be displayed.
|
*/
'ignored_solution_providers' => [
],
/*
|--------------------------------------------------------------------------
| Runnable Solutions
|--------------------------------------------------------------------------
|
| Some solutions that Ignition displays are runnable and can perform
| various tasks. By default, runnable solutions are only enabled when your
| app has debug mode enabled and the environment is `local` or
| `development`.
|
| Using the `IGNITION_ENABLE_RUNNABLE_SOLUTIONS` environment variable, you
| can override this behaviour and enable or disable runnable solutions
| regardless of the application's environment.
|
| Default: env('IGNITION_ENABLE_RUNNABLE_SOLUTIONS')
|
*/
'enable_runnable_solutions' => env('IGNITION_ENABLE_RUNNABLE_SOLUTIONS'),
/*
|--------------------------------------------------------------------------
| Remote Path Mapping
|--------------------------------------------------------------------------
|
| If you are using a remote dev server, like Laravel Homestead, Docker, or
| even a remote VPS, it will be necessary to specify your path mapping.
|
| Leaving one, or both of these, empty or null will not trigger the remote
| URL changes and Ignition will treat your editor links as local files.
|
| "remote_sites_path" is an absolute base path for your sites or projects
| in Homestead, Vagrant, Docker, or another remote development server.
|
| Example value: "/home/vagrant/Code"
|
| "local_sites_path" is an absolute base path for your sites or projects
| on your local computer where your IDE or code editor is running on.
|
| Example values: "/Users/<name>/Code", "C:\Users\<name>\Documents\Code"
|
*/
'remote_sites_path' => env('IGNITION_REMOTE_SITES_PATH', base_path()),
'local_sites_path' => env('IGNITION_LOCAL_SITES_PATH', ''),
/*
|--------------------------------------------------------------------------
| Housekeeping Endpoint Prefix
|--------------------------------------------------------------------------
|
| Ignition registers a couple of routes when it is enabled. Below you may
| specify a route prefix that will be used to host all internal links.
|
*/
'housekeeping_endpoint_prefix' => '_ignition',
/*
|--------------------------------------------------------------------------
| Settings File
|--------------------------------------------------------------------------
|
| Ignition allows you to save your settings to a specific global file.
|
| If no path is specified, a file with settings will be saved to the user's
| home directory. The directory depends on the OS and its settings but it's
| typically `~/.ignition.json`. In this case, the settings will be applied
| to all of your projects where Ignition is used and the path is not
| specified.
|
| However, if you want to store your settings on a project basis, or you
| want to keep them in another directory, you can specify a path where
| the settings file will be saved. The path should be an existing directory
| with correct write access.
| For example, create a new `ignition` folder in the storage directory and
| use `storage_path('ignition')` as the `settings_file_path`.
|
| Default value: '' (empty string)
*/
'settings_file_path' => '',
/*
|--------------------------------------------------------------------------
| Recorders
|--------------------------------------------------------------------------
|
| Ignition registers a couple of recorders when it is enabled. Below you may
| specify a recorders will be used to record specific events.
|
*/
'recorders' => [
DumpRecorder::class,
JobRecorder::class,
LogRecorder::class,
QueryRecorder::class,
],
/*
* When a key is set, we'll send your exceptions to Open AI to generate a solution
*/
'open_ai_key' => env('IGNITION_OPEN_AI_KEY'),
/*
|--------------------------------------------------------------------------
| Include arguments
|--------------------------------------------------------------------------
|
| Ignition show you stack traces of exceptions with the arguments that were
| passed to each method. This feature can be disabled here.
|
*/
'with_stack_frame_arguments' => true,
/*
|--------------------------------------------------------------------------
| Argument reducers
|--------------------------------------------------------------------------
|
| Ignition show you stack traces of exceptions with the arguments that were
| passed to each method. To make these variables more readable, you can
| specify a list of classes here which summarize the variables.
|
*/
'argument_reducers' => [
\Spatie\Backtrace\Arguments\Reducers\BaseTypeArgumentReducer::class,
\Spatie\Backtrace\Arguments\Reducers\ArrayArgumentReducer::class,
\Spatie\Backtrace\Arguments\Reducers\StdClassArgumentReducer::class,
\Spatie\Backtrace\Arguments\Reducers\EnumArgumentReducer::class,
\Spatie\Backtrace\Arguments\Reducers\ClosureArgumentReducer::class,
\Spatie\Backtrace\Arguments\Reducers\DateTimeArgumentReducer::class,
\Spatie\Backtrace\Arguments\Reducers\DateTimeZoneArgumentReducer::class,
\Spatie\Backtrace\Arguments\Reducers\SymphonyRequestArgumentReducer::class,
\Spatie\LaravelIgnition\ArgumentReducers\ModelArgumentReducer::class,
\Spatie\LaravelIgnition\ArgumentReducers\CollectionArgumentReducer::class,
\Spatie\Backtrace\Arguments\Reducers\StringableArgumentReducer::class,
],
];

50
config/tinker.php Normal file
View File

@@ -0,0 +1,50 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Console Commands
|--------------------------------------------------------------------------
|
| This option allows you to add additional Artisan commands that should
| be available within the Tinker environment. Once the command is in
| this array you may execute the command in Tinker using its name.
|
*/
'commands' => [
// App\Console\Commands\ExampleCommand::class,
],
/*
|--------------------------------------------------------------------------
| Auto Aliased Classes
|--------------------------------------------------------------------------
|
| Tinker will not automatically alias classes in your vendor namespaces
| but you may explicitly allow a subset of classes to get aliased by
| adding the names of each of those classes to the following list.
|
*/
'alias' => [
//
],
/*
|--------------------------------------------------------------------------
| Classes That Should Not Be Aliased
|--------------------------------------------------------------------------
|
| Typically, Tinker automatically aliases classes as you require them in
| Tinker. However, you may wish to never alias certain classes, which
| you may accomplish by listing the classes in the following array.
|
*/
'dont_alias' => [
'App\Nova',
],
];

View File

@@ -26,11 +26,11 @@ class UserFactory extends Factory
'email_verified_at' => now(),
'remember_token' => Str::random(10),
'home_address' => fake()->streetAddress(),
'home_city' => fake()->city(),
'home_state' => '',
'home_postcode' => fake()->postcode(),
'home_country' => fake()->country(),
'shipping_address' => fake()->streetAddress(),
'shipping_city' => fake()->city(),
'shipping_state' => '',
'shipping_postcode' => fake()->postcode(),
'shipping_country' => fake()->country(),
'billing_address' => fake()->streetAddress(),
'billing_city' => fake()->city(),

View File

@@ -3,13 +3,13 @@
namespace Database\Factories;
use App\Models\Location;
use App\Models\Event;
use App\Models\Workshop;
use DateInterval;
use Illuminate\Database\Eloquent\Factories\Factory;
class WorkshopFactory extends Factory
{
protected $model = Event::class;
protected $model = Workshop::class;
public function definition(): array
{

View File

@@ -1,37 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::rename('workshops', 'events');
Schema::table('tickets', function (Blueprint $table) {
$table->dropForeign(['workshop_id']);
$table->renameColumn('workshop_id', 'event_id');
$table->foreign('event_id')->references('id')->on('events');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::rename('events', 'workshops');
Schema::table('tickets', function (Blueprint $table) {
$table->dropForeign(['event_id']);
$table->renameColumn('event_id', 'workshop_id');
$table->foreign('workshops_id')->references('id')->on('workshops');
});
}
};

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('tfa_secret')->nullable();
$table->boolean('agree_tos')->default(false);
});
DB::table('users')->update(['agree_tos' => true]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('agree_tos');
$table->dropColumn('tfa_secret');
});
}
};

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('user_backup_codes', function (Blueprint $table) {
$table->id();
$table->foreignUuid('user_id')->constrained()->onDelete('cascade');
$table->string('code', 256);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_backup_codes');
}
};

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->renameColumn('home_address', 'shipping_address');
$table->renameColumn('home_address2', 'shipping_address2');
$table->renameColumn('home_city', 'shipping_city');
$table->renameColumn('home_state', 'shipping_state');
$table->renameColumn('home_postcode', 'shipping_postcode');
$table->renameColumn('home_country', 'shipping_country');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->renameColumn('shipping_address', 'home_address');
$table->renameColumn('shipping_address2', 'home_address2');
$table->renameColumn('shipping_city', 'home_city');
$table->renameColumn('shipping_state', 'home_state');
$table->renameColumn('shipping_postcode', 'home_postcode');
$table->renameColumn('shipping_country', 'home_country');
});
}
};

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('sent_emails', function (Blueprint $table) {
$table->string('id', 15)->primary();
$table->string('recipient');
$table->string('mailable_class');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('sent_emails');
}
};

View File

@@ -7,7 +7,7 @@ use App\Models\Media;
use App\Models\Post;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use App\Models\Event;
use App\Models\Workshop;
use Database\Factories\LocationFactory;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Storage;

View File

@@ -0,0 +1,5 @@
<?php
return [
'clamav' => ':attribute contains virus.',
];

3445
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,27 +3,29 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
"build": "vite build",
"snyk:test": "snyk test --all-projects --exclude=vendor"
},
"devDependencies": {
"autoprefixer": "^10.4.19",
"axios": "^1.6.4",
"laravel-vite-plugin": "^1.0",
"@tailwindcss/vite": "^4.1.17",
"autoprefixer": "^10.4.24",
"axios": "^1.13.4",
"laravel-vite-plugin": "^2.0.1",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"vite": "^5.0"
"vite": "^7.3.0"
},
"dependencies": {
"@tiptap/core": "^2.3.0",
"@tiptap/extension-highlight": "^2.3.0",
"@tiptap/extension-image": "^2.3.0",
"@tiptap/extension-link": "^2.3.0",
"@tiptap/extension-subscript": "^2.3.0",
"@tiptap/extension-superscript": "^2.3.0",
"@tiptap/extension-text-align": "^2.3.0",
"@tiptap/extension-typography": "^2.3.0",
"@tiptap/extension-underline": "^2.3.0",
"@tiptap/pm": "^2.5.1",
"@tiptap/starter-kit": "^2.3.0"
"@tiptap/core": "^3.10.7",
"@tiptap/extension-highlight": "^3.18.0",
"@tiptap/extension-image": "^3.18.0",
"@tiptap/extension-link": "^3.18.0",
"@tiptap/extension-subscript": "^3.18.0",
"@tiptap/extension-superscript": "^3.18.0",
"@tiptap/extension-text-align": "^3.18.0",
"@tiptap/extension-typography": "^3.18.0",
"@tiptap/extension-underline": "^3.18.0",
"@tiptap/pm": "^3.10.7",
"@tiptap/starter-kit": "^3.18.0",
"tailwindcss": "^4.1.17"
}
}

8
pint.json Normal file
View File

@@ -0,0 +1,8 @@
{
"preset": "laravel",
"rules": {
"simplified_null_return": true,
"new_with_braces": true,
"no_unused_imports": true
}
}

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,3 +1,14 @@
<If "%{HTTP_HOST} =~ /(\.local|^localhost(:\d+)?$)/">
# Disable caching in dev
<IfModule mod_headers.c>
Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
Header set Pragma "no-cache"
Header set Expires "0"
Header unset ETag
</IfModule>
FileETag None
</If>
<IfModule mod_deflate.c>
# Enable on-the-fly compression for various file types.
AddOutputFilterByType DEFLATE application/javascript
@@ -54,10 +65,12 @@
# Block access to .git directory
RewriteRule .*\.git/.* - [L,R=404]
# Force HTTPS and www subdomain
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^ https://www.stemmechanics.com.au%{REQUEST_URI} [L,R=301]
# <If "%{HTTP_HOST} !~ /(\.local|^localhost(:\d+)?$)/">
# # Force HTTPS and www in prod
# RewriteCond %{HTTPS} off [OR]
# RewriteCond %{HTTP_HOST} !^www\. [NC]
# RewriteRule ^ https://www.stemmechanics.com.au%{REQUEST_URI} [L,R=301]
# </If>
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .

BIN
public/about.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,4 +1,26 @@
let SM = {
redirectIfSafe: (target) => {
if (typeof target !== 'string' || target === '') {
window.location.assign('/');
return;
}
let url;
try {
url = new URL(target, window.location.origin);
} catch (error) {
window.location.assign('/');
return;
}
if (url.origin !== window.location.origin) {
window.location.assign('/');
return;
}
window.location.assign(url.href);
},
alert: (title, text, type = 'info') =>{
const data = {
position: 'top-end',
@@ -16,6 +38,23 @@ let SM = {
Swal.fire(data);
},
confirm: (title, content, button, callback) => {
Swal.fire({
position: 'top',
icon: 'warning',
iconColor: '#b91c1c',
title: title,
html: content,
showCancelButton: true,
confirmButtonText: button,
confirmButtonColor: '#b91c1c',
cancelButtonText: 'Cancel',
reverseButtons: true
}).then((result) => {
callback(result.isConfirmed);
});
},
copyToClipboard: (text) => {
const copyContent = async () => {
try {
@@ -29,21 +68,21 @@ let SM = {
copyContent().then(() => { /* empty */});
},
updateBillingAddress: () => {
const checkboxElement = document.querySelector('input[name="billing_same_home"]');
updateShippingAddress: () => {
const checkboxElement = document.querySelector('input[name="shipping_same_billing"]');
if (checkboxElement) {
const itemNames = ['address', 'address2', 'city', 'state', 'postcode', 'country'];
if (checkboxElement.checked) {
itemNames.forEach((itemName) => {
const element = document.querySelector(`input[name="billing_${itemName}"]`);
element.value = document.querySelector(`input[name="home_${itemName}"]`).value;
const element = document.querySelector(`input[name="shipping_${itemName}"]`);
element.value = document.querySelector(`input[name="billing_${itemName}"]`).value;
element.setAttribute('readonly', 'true');
});
} else {
itemNames.forEach((itemName) => {
const element = document.querySelector(`input[name="billing_${itemName}"]`);
const element = document.querySelector(`input[name="shipping_${itemName}"]`);
element.removeAttribute('readonly');
});
}
@@ -67,7 +106,7 @@ let SM = {
axios.delete(url)
.then((response) => {
if(response.data.success){
window.location.href = response.data.redirect;
SM.redirectIfSafe(response.data.redirect);
}
})
.catch(() => {
@@ -119,7 +158,7 @@ let SM = {
}
}
const uploadFile = (file, start, title, idx, count) => {
const uploadFile = (file, start, title, idx, count, uploadToken = null) => {
const showPercentDecimals = (file.size > (1024 * 1024 * 40));
const chunkSize = 1024 * 1024 * 2;
const end = Math.min(file.size, start + chunkSize);
@@ -129,6 +168,9 @@ let SM = {
formData.append('file', chunk);
formData.append('filename', file.name);
formData.append('filesize', file.size);
if (uploadToken) {
formData.append('upload_token', uploadToken);
}
if (start === 0) {
formData.append('filestart', 'true');
@@ -166,6 +208,10 @@ let SM = {
}
}).then((response) => {
if (response.status === 200) {
if (response.data && response.data.upload_token) {
uploadToken = response.data.upload_token;
}
if (end >= file.size) {
uploadedFiles.push({ file: file, title: title, data: response.data });
@@ -188,12 +234,13 @@ let SM = {
} else {
start = 0;
idx += 1;
uploadToken = null;
}
} else {
start = end;
}
uploadFile(files[idx], start, titles[idx] || '', idx, files.length);
uploadFile(files[idx], start, titles[idx] || '', idx, files.length, uploadToken);
} else {
showError(response.data.message);
}
@@ -347,6 +394,6 @@ let SM = {
};
document.addEventListener('DOMContentLoaded', () => {
SM.updateBillingAddress();
SM.updateShippingAddress();
SM.updateAllThumbnails();
});

8
renovate-config.json Normal file
View File

@@ -0,0 +1,8 @@
{
"endpoint": "https://git.stemmechanics.com.au/api/v1",
"gitAuthor": "Renovate Bot <renovate-bot@stemmechanics.com.au>",
"platform": "gitea",
"onboardingConfigFileName": "renovate.json",
"autodiscover": true,
"optimizeForDisabled": true
}

24
renovate.json Normal file
View File

@@ -0,0 +1,24 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
],
"automerge": true,
"platformAutomerge": true,
"requiredStatusChecks": null,
"packageRules": [
{
"matchUpdateTypes": [
"patch"
],
"automerge": true
},
{
"matchUpdateTypes": [
"minor",
"major"
],
"automerge": false
}
]
}

View File

@@ -1,6 +1,24 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@source "../**/*.{blade.php,js,php,vue}";
@theme {
/* Color tokens */
--color-primary-color: #0284C7;
--color-primary-color-dark: #0370A1;
--color-primary-color-light: #0EA5E9;
--color-danger-color: #b91c1c;
--color-danger-color-dark: #991b1b;
--color-danger-color-light: #dc2626;
--color-success-color: #16a34a;
--color-success-color-dark: #22c55e;
--color-success-color-light: #4ade80;
/* Box shadows */
--shadow-deep: 0 10px 15px rgba(0, 0, 0, 0.5);
}
html, body {
@apply bg-gray-100;

View File

@@ -1,6 +1,7 @@
import Link from "@tiptap/extension-link";
import {Editor} from "@tiptap/core";
import StarterKit from "@tiptap/starter-kit";
import Underline from "@tiptap/extension-underline";
import Highlight from "@tiptap/extension-highlight";
import TextAlign from "@tiptap/extension-text-align";
import Typography from "@tiptap/extension-typography";
@@ -49,12 +50,16 @@ document.addEventListener('alpine:init', () => {
editor = new Editor({
element: this.$refs.element,
extensions: [
StarterKit,
StarterKit.configure({
link: false,
underline: false,
}),
Highlight,
CustomLink.configure({
rel: 'noopener noreferrer',
openOnClick: 'whenNotEditable',
}),
Underline,
TextAlign.configure({
types: ['heading', 'paragraph', 'small', 'extraSmall'],
}),
@@ -65,6 +70,11 @@ document.addEventListener('alpine:init', () => {
ExtraSmall,
Box
],
editorProps: {
attributes: {
class: 'tiptap content',
},
},
content: content,
onCreate({/* editor */}) {
_this.updatedAt = Date.now()
@@ -141,6 +151,9 @@ document.addEventListener('alpine:init', () => {
unsetAllMarks() {
editor.chain().focus().unsetAllMarks().run()
},
clearNodes() {
editor.chain().focus().clearNodes().run()
},
clearNotes() {
editor.chain().focus().clearNodes().run()
},

View File

@@ -13,11 +13,11 @@ return [
'surname_required' => 'A surname is required',
'phone_required' => 'A phone number is required',
'home_address_required' => 'A home address is required',
'home_city_required' => 'A home city is required',
'home_postcode_required' => 'A home postcode is required',
'home_country_required' => 'A home country is required',
'home_state_required' => 'A home state is required',
'shipping_address_required' => 'A shipping address is required',
'shipping_city_required' => 'A shipping city is required',
'shipping_postcode_required' => 'A shipping postcode is required',
'shipping_country_required' => 'A shipping country is required',
'shipping_state_required' => 'A shipping state is required',
'billing_address_required' => 'A billing address is required',
'billing_city_required' => 'A billing city is required',

View File

@@ -0,0 +1,19 @@
<x-layout>
<x-mast>About STEMMechanics</x-mast>
<div class="bg-no-repeat bg-cover h-96" style="background-image:url({{asset('about.webp')}})"></div>
<x-container class="pt-8">
<p class="mb-4">STEMMechanics is a hands on education studio based in Cairns, created and operated by James Collins. Drawing on years of experience delivering digital learning, STEM programs and creative technology workshops across Queensland, James built STEMMechanics to give communities practical, engaging ways to understand technology.</p>
<p class="mb-4">James' background includes work with State Library of Queensland, Education Queensland and a wide range of community organisations, where he delivered digital literacy programs, ICT support, eSports events, STEM initiatives and media workshops in both metropolitan and remote regions. This mix of technical, educational and community based experience shaped the approach that defines STEMMechanics today.</p>
<p class="mb-4">At its core, STEMMechanics exists because learners understand technology best when they can build, test, break, remake and explore it themselves. James believes in hands on, curiosity driven learning that develops confidence, problem solving skills and genuine interest in STEM. Every program is designed to be practical, creative and accessible.</p>
<p class="mb-4">STEMMechanics operates from a dedicated, private workshop in Cairns where James prototypes new ideas, experiments with electronics and mechanical builds, develops software tools and designs the kits used in his workshops. This workshop is the testing ground for everything delivered to schools and communities across the region.</p>
<x-heading>What STEMMechanics Does</x-heading>
<h3 class="ml-4 font-bold">STEM Workshops</h3>
<p class="ml-4 mb-4">STEMMechanics delivers a wide range of workshop programs including coding, robotics, Micro:bit activities, cardboard engineering, mechanical motion, paper circuits and hands on build projects. All sessions are tailored to the age group, learning outcomes and context of each community.</p>
<h3 class="ml-4 font-bold">Digital Media & Creative Tech</h3>
<p class="ml-4 mb-4">Workshops include stop motion animation, filmmaking, digital storytelling and introductory media production. These programs support students in developing both creative and technical skills.</p>
<h3 class="ml-4 font-bold">Community & School Programs</h3>
<p class="ml-4 mb-4">STEMMechanics partners with schools, regional councils, libraries and community groups to deliver project based STEM initiatives, themed workshop blocks and multi day programs.</p>
</x-container>
</x-layout>

View File

@@ -1,21 +1,21 @@
@php
$user = auth()->user();
$billing_same_home = $user->home_address === $user->billing_address
&& $user->home_address2 === $user->billing_address2
&& $user->home_city === $user->billing_city
&& $user->home_state === $user->billing_state
&& $user->home_postcode === $user->billing_postcode
&& $user->home_country === $user->billing_country;
$shipping_same_billing = $user->shipping_address === $user->billing_address
&& $user->shipping_address2 === $user->billing_address2
&& $user->shipping_city === $user->billing_city
&& $user->shipping_state === $user->billing_state
&& $user->shipping_postcode === $user->billing_postcode
&& $user->shipping_country === $user->billing_country;
@endphp
<x-layout>
<x-mast>Account Settings</x-mast>
<x-container>
<form method="POST" action="{{ route('account.update') }}" x-data x-on:submit.prevent="SM.updateBillingAddress(); $el.submit()">
<form method="POST" action="{{ route('account.update') }}" x-data x-on:submit.prevent="SM.updateShippingAddress(); $el.submit()">
@csrf
<h3 class="text-lg font-bold mt-4 mb-3">Contact Information</h3>
<div class="flex gap-8">
<div class="flex flex-col sm:gap-8 sm:flex-row">
<div class="flex-1">
<x-ui.input label="First name" name="firstname" value="{{ $user->firstname }}" />
</div>
@@ -23,7 +23,7 @@ $billing_same_home = $user->home_address === $user->billing_address
<x-ui.input label="Surname" name="surname" value="{{ $user->surname }}" />
</div>
</div>
<div class="flex gap-8">
<div class="flex flex-col sm:gap-8 sm:flex-row">
<div class="flex-1">
<x-ui.input type="email" label="Email" name="email" value="{{ $user->email }}" info="{{ $user->email_update_pending ? 'Pending request to change to ' . $user->email_update_pending : '' }}"/>
</div>
@@ -42,25 +42,77 @@ $billing_same_home = $user->home_address === $user->billing_address
</div>
</section>
<section x-data="{ open: true }">
<section x-data="{ open: false }">
<a href="#" class="flex items-center" @click.prevent="open = !open">
<i :class="{'transform': !open, '-rotate-90': !open, 'translate-y-0.5': true}" class="fa-solid fa-angle-down text-lg transition-transform mr-2"></i>
<h3 class="text-lg font-bold mt-4 mb-3">Home Address</h3>
<i :class="{'transform': !open, '-rotate-90': !open, 'translate-y-0.5': true}"
class="fa-solid fa-angle-down text-lg transition-transform mr-2"></i>
<h3 class="text-lg font-bold mt-4 mb-3">Two Factor Authentication</h3>
</a>
<div x-show="open">
<x-ui.input label="Address" name="home_address" value="{{ $user->home_address }}" />
<x-ui.input label="Address 2" name="home_address2" value="{{ $user->home_address2 }}" />
<x-ui.input label="City" name="home_city" value="{{ $user->home_city }}" />
<div class="flex gap-8">
<div class="flex-1">
<x-ui.input label="State" name="home_state" value="{{ $user->home_state }}" />
<div class="px-4 mb-4" x-show="open">
<div class="flex items-center border border-gray-300 rounded bg-white pl-2 pr-4 py-3 mb-4">
<div class="bg-gray-200 rounded-full w-14 h-14 flex items-center justify-center flex-shrink-0">
<i class="fa-solid fa-envelope text-2xl"></i>
</div>
<div class="flex-1">
<x-ui.input label="Postcode" name="home_postcode" value="{{ $user->home_postcode }}" />
<div class="mx-4 flex-grow">
<p class="flex mb-2">
<span class="text-sm font-bold mr-2">Use Email</span>
<span class="text-xs bg-green-500 text-white rounded px-2 py-0.5">Enabled</span>
</p>
<p class="text-xs">Use the security link sent to your email address as your two-factor authentication (2FA). The security link will be sent to the address associated with your account.</p>
</div>
</div>
<div class="border border-gray-300 rounded bg-white pl-2 pr-4 py-3">
<div class="flex items-center">
<div class="bg-gray-200 rounded-full w-14 h-14 flex items-center justify-center flex-shrink-0">
<i class="fa-solid fa-mobile-screen-button text-2xl"></i>
</div>
<div class="mx-4 flex-grow">
<p class="flex mb-2">
<span class="text-sm font-bold mr-2">Use Authenticator App</span>
<span x-cloak x-show="!$store.tfa.enabled" class="text-xs bg-red-500 text-white rounded px-2 py-0.5">Disabled</span>
<span x-cloak x-show="$store.tfa.enabled" class="text-xs bg-green-500 text-white rounded px-2 py-0.5">Enabled</span>
</p>
<p class="text-xs">Use an Authenticator App as your two-factor authenticator. When you sign in you'll be asked to use the security code provided by your Authenticator App.</p>
</div>
<div class="flex flex-col text-nowrap gap-2">
<x-ui.button x-show="!$store.tfa.enabled" id="tfa_button" type="button" color="primary-outline" x-data x-on:click.prevent="setupTFA()">Setup</x-ui.button>
<x-ui.button x-show="$store.tfa.enabled" type="button" color="danger-outline" x-data x-on:click.prevent="destroyTFA()">Disable</x-ui.button>
<a href="#" x-show="$store.tfa.enabled" x-on:click.prevent="resetBackupCodes($event)" class="text-xs link">Reset Backup Codes</a>
</div>
</div>
<div class="mt-4 pt-4 border-t flex items-center justify-center gap-4" x-cloak x-show="$store.tfa.show && !$store.tfa.loading">
<img src="/loading.gif" id="tfa_image_loader" alt="loading" width="100" height="100"/>
<img src="" id="tfa_image" alt="QR Code" width="150" height="150" style="display:none" onload="this.style.display='block';document.getElementById('tfa_image_loader').style.display='none';"/>
<div>
<p class="text-xs mb-2">Scan the QR Code into your Authenticator App and enter the code provided below</p>
<div class="flex items-center gap-4 justify-center">
<x-ui.input name="code" id="code" class="mb-0" />
<x-ui.button class="mt-1" type="button" color="primary-outline" x-on:click.prevent="linkTFA()">Link</x-ui.button>
</div>
</div>
</div>
<div class="mt-4 pt-4 border-t flex justify-center" x-cloak x-show="$store.tfa.loading">
<img src="/loading.gif" alt="loading" width="100" height="100"/>
</div>
<div class="mt-4 pt-4 border-t flex justify-center" x-cloak x-show="$store.tfa.codes && !$store.tfa.loading">
<div class="w-[34rem] flex items-center gap-4">
<div class="w-[18rem] mx-auto">
<p class="text-sm font-bold mb-1">Save your Backup Codes</p>
<ul class="ml-6 mb-4 text-xs list-disc">
<li>Keep these backup codes safe</li>
<li>You can only use each one once</li>
<li>They will not be shown again</li>
<li>Any existing codes can no longer be used</li>
</ul>
</div>
<div class="w-[16rem] bg-gray-200 p-4 text-sm font-mono flex flex-wrap justify-center">
<template x-for="(code, idx) in $store.tfa.codes" :key="idx">
<p class="mx-4" x-text="code"></p>
</template>
</div>
</div>
</div>
</div>
<x-ui.input label="Country" name="home_country" value="{{ $user->home_country }}" />
</div>
</section>
@@ -70,19 +122,40 @@ $billing_same_home = $user->home_address === $user->billing_address
<h3 class="text-lg font-bold mt-4 mb-3">Billing Address</h3>
</a>
<div x-show="open">
<x-ui.checkbox label="Same as home address" name="billing_same_home" checked="{{ $billing_same_home }}" x-data x-on:click="SM.updateBillingAddress" />
<x-ui.input label="Address" name="billing_address" value="{{ $user->billing_address }}" readonly="{{ $billing_same_home }}" />
<x-ui.input label="Address 2" name="billing_address2" value="{{ $user->billing_address2 }}" readonly="{{ $billing_same_home }}" />
<x-ui.input label="City" name="billing_city" value="{{ $user->billing_city }}" readonly="{{ $billing_same_home }}" />
<div class="flex gap-8">
<x-ui.input label="Address" name="billing_address" value="{{ $user->billing_address }}" />
<x-ui.input label="Address 2" name="billing_address2" value="{{ $user->billing_address2 }}" />
<x-ui.input label="City" name="billing_city" value="{{ $user->billing_city }}" />
<div class="flex flex-col sm:gap-8 sm:flex-row">
<div class="flex-1">
<x-ui.input label="State" name="billing_state" value="{{ $user->billing_state }}" readonly="{{ $billing_same_home }}" />
<x-ui.input label="State" name="billing_state" value="{{ $user->billing_state }}" />
</div>
<div class="flex-1">
<x-ui.input label="Postcode" name="billing_postcode" value="{{ $user->billing_postcode }}" readonly="{{ $billing_same_home }}" />
<x-ui.input label="Postcode" name="billing_postcode" value="{{ $user->billing_postcode }}" />
</div>
</div>
<x-ui.input label="Country" name="billing_country" value="{{ $user->billing_country }}" readonly="{{ $billing_same_home }}" />
<x-ui.input label="Country" name="billing_country" value="{{ $user->billing_country }}" />
</div>
</section>
<section x-data="{ open: true }">
<a href="#" class="flex items-center" @click.prevent="open = !open">
<i :class="{'transform': !open, '-rotate-90': !open, 'translate-y-0.5': true}" class="fa-solid fa-angle-down text-lg transition-transform mr-2"></i>
<h3 class="text-lg font-bold mt-4 mb-3">Shipping Address</h3>
</a>
<div x-show="open">
<x-ui.checkbox label="Same as billing address" name="shipping_same_billing" checked="{{ $shipping_same_billing }}" x-data x-on:click="SM.updateShippingAddress" />
<x-ui.input label="Address" name="shipping_address" value="{{ $user->shipping_address }}" readonly="{{ $shipping_same_billing }}" />
<x-ui.input label="Address 2" name="shipping_address2" value="{{ $user->shipping_address2 }}" readonly="{{ $shipping_same_billing }}" />
<x-ui.input label="City" name="shipping_city" value="{{ $user->shipping_city }}" readonly="{{ $shipping_same_billing }}" />
<div class="flex flex-col sm:gap-8 sm:flex-row">
<div class="flex-1">
<x-ui.input label="State" name="shipping_state" value="{{ $user->shipping_state }}" readonly="{{ $shipping_same_billing }}" />
</div>
<div class="flex-1">
<x-ui.input label="Postcode" name="shipping_postcode" value="{{ $user->shipping_postcode }}" readonly="{{ $shipping_same_billing }}" />
</div>
</div>
<x-ui.input label="Country" name="shipping_country" value="{{ $user->shipping_country }}" readonly="{{ $shipping_same_billing }}" />
</div>
</section>
@@ -97,3 +170,106 @@ $billing_same_home = $user->home_address === $user->billing_address
</form>
</x-container>
</x-layout>
{{ $codes ?? '' }}
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('tfa', {
show: false,
secret: null,
enabled: {{ $user->tfa_secret !== null ? 'true' : 'false'}},
codes: null,
loading: false
});
});
function setupTFA() {
document.getElementById('tfa_button').disabled = true;
axios.get('/account/2fa')
.then(response => {
if(response.data.secret) {
Alpine.store('tfa').show = true;
Alpine.store('tfa').secret = response.data.secret;
document.getElementById('tfa_image').src = '/account/2fa/image?secret=' + response.data.secret;
} else {
SM.alert('2FA Error', 'An error occurred while setting up two-factor authentication. Please try again later', 'danger');
}
})
.catch(() => {
SM.alert('2FA Error', 'An error occurred while setting up two-factor authentication. Please try again later', 'danger');
});
}
function linkTFA() {
Alpine.store('tfa').loading = true;
axios.post('/account/2fa', {
code: document.getElementById('code').value,
secret: Alpine.store('tfa').secret,
})
.then(response => {
console.log(response.data);
if(response.data.success) {
SM.alert('2FA Linked', 'Two-factor authentication has been successfully linked to your account', 'success');
document.getElementById('tfa_button').disabled = false;
document.getElementById('code').value = '';
document.getElementById('tfa_image').src = '';
Alpine.store('tfa').show = false;
Alpine.store('tfa').enabled = true;
Alpine.store('tfa').codes = response.data.codes;
} else {
SM.alert('2FA Error', 'An error occurred while linking two-factor authentication. Please try again later', 'danger');
}
})
.catch(() => {
SM.alert('2FA Error', 'An error occurred while linking two-factor authentication. Please try again later', 'danger');
})
.finally(() => {
Alpine.store('tfa').loading = false;
});
}
function resetBackupCodes(event) {
event.target.classList.add('disabled');
Alpine.store('tfa').codes = null;
Alpine.store('tfa').loading = true;
axios.post('/account/2fa/reset-backup-codes')
.then(response => {
if(response.data.success) {
Alpine.store('tfa').codes = response.data.codes;
} else {
SM.alert('2FA Error', 'An error occurred while resetting your backup codes. Please try again later', 'danger');
}
})
.catch(() => {
SM.alert('2FA Error', 'An error occurred while resetting your backup codes. Please try again later', 'danger');
})
.finally(() => {
event.target.classList.remove('disabled');
Alpine.store('tfa').loading = false;
});
}
function destroyTFA() {
SM.confirm('Disable 2FA', 'Are you sure you want to remove two-factor authentication from your account?', 'Disable', (confirm) => {
if(confirm) {
axios.delete('/account/2fa')
.then(response => {
if (response.data.success) {
SM.alert('2FA Disabled', 'Two-factor authentication has been successfully disabled on your account', 'success');
Alpine.store('tfa').enabled = false;
Alpine.store('tfa').codes = null;
} else {
SM.alert('2FA Error', 'An error occurred while disabling two-factor authentication. Please try again later', 'danger');
}
})
.catch(() => {
SM.alert('2FA Error', 'An error occurred while disabling two-factor authentication. Please try again later', 'danger');
});
}
}, {
confirmButtonText: 'Disable'
});
}
</script>

View File

@@ -2,7 +2,7 @@
<x-mast backRoute="admin.user.index" backTitle="Users">Create User</x-mast>
<x-container>
<form method="POST" action="{{ route('admin.user.store') }}" x-data x-on:submit.prevent="SM.updateBillingAddress(); $el.submit()">
<form method="POST" action="{{ route('admin.user.store') }}" x-data x-on:submit.prevent="SM.updateShippingAddress(); $el.submit()">
@csrf
<h3 class="text-lg font-bold mt-4 mb-3">Contact Information</h3>
<div class="flex gap-8">
@@ -22,34 +22,12 @@
</div>
</div>
<section x-data="{ open: true }">
<a href="#" class="flex items-center" @click.prevent="open = !open">
<i :class="{'transform': !open, '-rotate-90': !open, 'translate-y-0.5': true}" class="fa-solid fa-angle-down text-lg transition-transform mr-2"></i>
<h3 class="text-lg font-bold mt-4 mb-3">Home Address</h3>
</a>
<div x-show="open">
<x-ui.input label="Address" name="home_address" />
<x-ui.input label="Address 2" name="home_address2" />
<x-ui.input label="City" name="home_city" />
<div class="flex gap-8">
<div class="flex-1">
<x-ui.input label="State" name="home_state" />
</div>
<div class="flex-1">
<x-ui.input label="Postcode" name="home_postcode" />
</div>
</div>
<x-ui.input label="Country" name="home_country" />
</div>
</section>
<section x-data="{ open: true }">
<a href="#" class="flex items-center" @click.prevent="open = !open">
<i :class="{'transform': !open, '-rotate-90': !open, 'translate-y-0.5': true}" class="fa-solid fa-angle-down text-lg transition-transform mr-2"></i>
<h3 class="text-lg font-bold mt-4 mb-3">Billing Address</h3>
</a>
<div x-show="open">
<x-ui.checkbox label="Same as home address" name="billing_same_home" checked="true" x-data x-on:click="SM.updateBillingAddress" />
<x-ui.input label="Address" name="billing_address" />
<x-ui.input label="Address 2" name="billing_address2" />
<x-ui.input label="City" name="billing_city" />
@@ -65,6 +43,28 @@
</div>
</section>
<section x-data="{ open: true }">
<a href="#" class="flex items-center" @click.prevent="open = !open">
<i :class="{'transform': !open, '-rotate-90': !open, 'translate-y-0.5': true}" class="fa-solid fa-angle-down text-lg transition-transform mr-2"></i>
<h3 class="text-lg font-bold mt-4 mb-3">Shipping Address</h3>
</a>
<div x-show="open">
<x-ui.checkbox label="Same as billing address" name="shipping_same_billing" checked="true" x-data x-on:click="SM.updateShippingAddress" />
<x-ui.input label="Address" name="shipping_address" />
<x-ui.input label="Address 2" name="shipping_address2" />
<x-ui.input label="City" name="shipping_city" />
<div class="flex gap-8">
<div class="flex-1">
<x-ui.input label="State" name="shipping_state" />
</div>
<div class="flex-1">
<x-ui.input label="Postcode" name="shipping_postcode" />
</div>
</div>
<x-ui.input label="Country" name="shipping_country" />
</div>
</section>
<div class="flex justify-end mt-8">
<x-ui.button type="submit">Create</x-ui.button>
</div>

View File

@@ -1,19 +1,19 @@
@props(['user'])
@php
$billing_same_home = $user->home_address === $user->billing_address
&& $user->home_address2 === $user->billing_address2
&& $user->home_city === $user->billing_city
&& $user->home_state === $user->billing_state
&& $user->home_postcode === $user->billing_postcode
&& $user->home_country === $user->billing_country;
$shipping_same_billing = $user->shipping_address === $user->billing_address
&& $user->shipping_address2 === $user->billing_address2
&& $user->shipping_city === $user->billing_city
&& $user->shipping_state === $user->billing_state
&& $user->shipping_postcode === $user->billing_postcode
&& $user->shipping_country === $user->billing_country;
@endphp
<x-layout>
<x-mast backRoute="admin.user.index" backTitle="Users">Edit User</x-mast>
<x-container>
<form method="POST" action="{{ route('admin.user.update', $user) }}" x-data x-on:submit.prevent="SM.updateBillingAddress(); $el.submit()">
<form method="POST" action="{{ route('admin.user.update', $user) }}" x-data x-on:submit.prevent="SM.updateShippingAddress(); $el.submit()">
@method('PUT')
@csrf
<h3 class="text-lg font-bold mt-4 mb-3">Contact Information</h3>
@@ -34,14 +34,13 @@
</div>
</div>
{{ $user }}
<section x-data="{ open: true }">
<a href="#" class="flex items-center" @click.prevent="open = !open">
<i :class="{'transform': !open, '-rotate-90': !open, 'translate-y-0.5': true}" class="fa-solid fa-angle-down text-lg transition-transform mr-2"></i>
<h3 class="text-lg font-bold mt-4 mb-3">Email Subscriptions</h3>
</a>
<div x-show="open">
<x-ui.checkbox label="Upcoming Workshops" name="billing_same_home" checked="{{ $billing_same_home }}" />
<x-ui.checkbox label="Upcoming Workshops" name="subscribed" checked="{{ $user->subscribed }}" />
</div>
</section>
@@ -51,18 +50,18 @@
<h3 class="text-lg font-bold mt-4 mb-3">Home Address</h3>
</a>
<div x-show="open">
<x-ui.input label="Address" name="home_address" value="{{ $user->home_address }}" />
<x-ui.input label="Address 2" name="home_address2" value="{{ $user->home_address2 }}" />
<x-ui.input label="City" name="home_city" value="{{ $user->home_city }}" />
<x-ui.input label="Address" name="billing_address" value="{{ $user->billing_address }}" />
<x-ui.input label="Address 2" name="billing_address2" value="{{ $user->billing_address2 }}" />
<x-ui.input label="City" name="billing_city" value="{{ $user->billing_city }}" />
<div class="flex gap-8">
<div class="flex-1">
<x-ui.input label="State" name="home_state" value="{{ $user->home_state }}" />
<x-ui.input label="State" name="billing_state" value="{{ $user->billing_state }}" />
</div>
<div class="flex-1">
<x-ui.input label="Postcode" name="home_postcode" value="{{ $user->home_postcode }}" />
<x-ui.input label="Postcode" name="billing_postcode" value="{{ $user->billing_postcode }}" />
</div>
</div>
<x-ui.input label="Country" name="home_country" value="{{ $user->home_country }}" />
<x-ui.input label="Country" name="billing_country" value="{{ $user->billing_country }}" />
</div>
</section>
@@ -72,19 +71,19 @@
<h3 class="text-lg font-bold mt-4 mb-3">Billing Address</h3>
</a>
<div x-show="open">
<x-ui.checkbox label="Same as home address" name="billing_same_home" checked="{{ $billing_same_home }}" x-data x-on:click="SM.updateBillingAddress" />
<x-ui.input label="Address" name="billing_address" value="{{ $user->billing_address }}" readonly="{{ $billing_same_home }}" />
<x-ui.input label="Address 2" name="billing_address2" value="{{ $user->billing_address2 }}" readonly="{{ $billing_same_home }}" />
<x-ui.input label="City" name="billing_city" value="{{ $user->billing_city }}" readonly="{{ $billing_same_home }}" />
<x-ui.checkbox label="Same as billing address" name="shipping_same_billing" checked="{{ $shipping_same_billing }}" x-data x-on:click="SM.updateShippingAddress" />
<x-ui.input label="Address" name="shipping_address" value="{{ $user->shipping_address }}" readonly="{{ $shipping_same_billing }}" />
<x-ui.input label="Address 2" name="shipping_address2" value="{{ $user->shipping_address2 }}" readonly="{{ $shipping_same_billing }}" />
<x-ui.input label="City" name="shipping_city" value="{{ $user->shipping_city }}" readonly="{{ $shipping_same_billing }}" />
<div class="flex gap-8">
<div class="flex-1">
<x-ui.input label="State" name="billing_state" value="{{ $user->billing_state }}" readonly="{{ $billing_same_home }}" />
<x-ui.input label="State" name="shipping_state" value="{{ $user->shipping_state }}" readonly="{{ $shipping_same_billing }}" />
</div>
<div class="flex-1">
<x-ui.input label="Postcode" name="billing_postcode" value="{{ $user->billing_postcode }}" readonly="{{ $billing_same_home }}" />
<x-ui.input label="Postcode" name="shipping_postcode" value="{{ $user->shipping_postcode }}" readonly="{{ $shipping_same_billing }}" />
</div>
</div>
<x-ui.input label="Country" name="billing_country" value="{{ $user->billing_country }}" readonly="{{ $billing_same_home }}" />
<x-ui.input label="Country" name="shipping_country" value="{{ $user->shipping_country }}" readonly="{{ $shipping_same_billing }}" />
</div>
</section>

View File

@@ -1,33 +1,33 @@
@php
$eventContent = isset($event) ? $event->content : '';
$workshopContent = isset($workshop) ? $workshop->content : '';
@endphp
<x-layout>
<x-mast backRoute="admin.event.index" backTitle="Workshops">{{ isset($event) ? 'Edit' : 'Create' }} Workshop</x-mast>
<x-mast backRoute="admin.workshop.index" backTitle="Workshops">{{ isset($workshop) ? 'Edit' : 'Create' }} Workshop</x-mast>
<x-container class="mt-4">
<form x-data="{type:'physical',registration:'{{old('registration', $event->registration ?? 'none')}}'}" method="POST" action="{{ route('admin.event.' . (isset($event) ? 'update' : 'store'), $event ?? []) }}">
@isset($event)
<form x-data="{type:'physical',registration:'{{old('registration', $workshop->registration ?? 'none')}}'}" method="POST" action="{{ route('admin.workshop.' . (isset($workshop) ? 'update' : 'store'), $workshop ?? []) }}">
@isset($workshop)
@method('PUT')
@endisset
@csrf
<div class="mb-4">
<x-ui.input label="Title" name="title" value="{!! isset($event) ? $event->title : '' !!}" />
<x-ui.input label="Title" name="title" value="{!! isset($workshop) ? $workshop->title : '' !!}" />
</div>
<div class="mb-4">
<x-ui.media label="Image" name="hero_media_name" value="{{ $event->hero_media_name ?? '' }}" allow_uploads="true" />
<x-ui.media label="Image" name="hero_media_name" value="{{ $workshop->hero_media_name ?? '' }}" allow_uploads="true" />
</div>
<div class="flex flex-col sm:flex-row sm:gap-8">
<div class="flex-1">
<x-ui.select label="Type" name="type" x-model="type">
<option value="physical" {{ ($event->location_id ?? '') !== '' || !isset($event) ? 'selected' : '' }}>Physical</option>
<option value="online" {{ ($event->location_id ?? '') === null ? 'selected' : '' }}>Online</option>
<option value="physical" {{ ($workshop->location_id ?? '') !== '' || !isset($workshop) ? 'selected' : '' }}>Physical</option>
<option value="online" {{ ($workshop->location_id ?? '') === null ? 'selected' : '' }}>Online</option>
</x-ui.select>
</div>
<div class="flex-1">
<span x-show="type==='physical'">
<x-ui.select label="Location" name="location_id">
@foreach(\App\Models\Location::orderByRaw("name = 'Online' DESC, name ASC")->get() as $location)
<option value="{{ $location->id }}" {{ ($event->location_id ?? '') === $location->id ? 'selected' : '' }}>{{ $location->name }}</option>
<option value="{{ $location->id }}" {{ ($workshop->location_id ?? '') === $location->id ? 'selected' : '' }}>{{ $location->name }}</option>
@endforeach
</x-ui.select>
</span>
@@ -35,26 +35,26 @@
</div>
<div class="flex flex-col sm:flex-row sm:gap-8">
<div class="flex-1">
<x-ui.input type="datetime-local" label="Start Date" name="starts_at" value="{{ \App\Helpers::timestampNoSeconds($event->starts_at ?? '') }}" onchange="updatedStartsAt()"/>
<x-ui.input type="datetime-local" label="Start Date" name="starts_at" value="{{ \App\Helpers::timestampNoSeconds($workshop->starts_at ?? '') }}" onchange="updatedStartsAt()"/>
</div>
<div class="flex-1">
<x-ui.input type="datetime-local" label="End Date" name="ends_at" value="{{ \App\Helpers::timestampNoSeconds($event->ends_at ?? '') }}" />
<x-ui.input type="datetime-local" label="End Date" name="ends_at" value="{{ \App\Helpers::timestampNoSeconds($workshop->ends_at ?? '') }}" />
</div>
</div>
<div class="flex flex-col sm:flex-row sm:gap-8">
<div class="flex-1">
<x-ui.select label="Status" name="status">
<option value="draft" {{ ($event->status ?? '') === 'draft' ? 'selected' : '' }}>Draft</option>
<option value="open" {{ ($event->status ?? '') === 'open' ? 'selected' : '' }}>Open</option>
<option value="private" {{ ($event->status ?? '') === 'private' ? 'selected' : '' }}>Private</option>
<option value="full" {{ ($event->status ?? '') === 'full' ? 'selected' : '' }}>Full</option>
<option value="scheduled" {{ ($event->status ?? '') === 'scheduled' ? 'selected' : '' }}>Scheduled</option>
<option value="closed" {{ ($event->status ?? '') === 'closed' ? 'selected' : '' }}>Closed</option>
<option value="cancelled" {{ ($event->status ?? '') === 'cancelled' ? 'selected' : '' }}>Cancelled</option>
<option value="draft" {{ ($workshop->status ?? '') === 'draft' ? 'selected' : '' }}>Draft</option>
<option value="open" {{ ($workshop->status ?? '') === 'open' ? 'selected' : '' }}>Open</option>
<option value="private" {{ ($workshop->status ?? '') === 'private' ? 'selected' : '' }}>Private</option>
<option value="full" {{ ($workshop->status ?? '') === 'full' ? 'selected' : '' }}>Full</option>
<option value="scheduled" {{ ($workshop->status ?? '') === 'scheduled' ? 'selected' : '' }}>Scheduled</option>
<option value="closed" {{ ($workshop->status ?? '') === 'closed' ? 'selected' : '' }}>Closed</option>
<option value="cancelled" {{ ($workshop->status ?? '') === 'cancelled' ? 'selected' : '' }}>Cancelled</option>
</x-ui.select>
</div>
<div class="flex-1">
<x-ui.input type="datetime-local" label="Publish Date" name="publish_at" value="{{ \App\Helpers::timestampNoSeconds($event->publish_at ?? '') }}" onchange="updatedPublishAt()" />
<x-ui.input type="datetime-local" label="Publish Date" name="publish_at" value="{{ \App\Helpers::timestampNoSeconds($workshop->publish_at ?? '') }}" onchange="updatedPublishAt()" />
</div>
</div>
<div class="flex flex-col sm:flex-row sm:gap-8">
@@ -62,44 +62,44 @@
&nbsp;
</div>
<div class="flex-1">
<x-ui.input type="datetime-local" label="Closes Date" name="closes_at" value="{{ \App\Helpers::timestampNoSeconds($event->closes_at ?? '') }}" />
<x-ui.input type="datetime-local" label="Closes Date" name="closes_at" value="{{ \App\Helpers::timestampNoSeconds($workshop->closes_at ?? '') }}" />
</div>
</div>
<div class="flex flex-col sm:flex-row sm:gap-8">
<div class="flex-1">
<x-ui.input label="Price" name="price" info="Leave blank to hide from public. Also supports Free, TBD or TBC" value="{{ $event->price ?? '' }}" />
<x-ui.input label="Price" name="price" info="Leave blank to hide from public. Also supports Free, TBD or TBC" value="{{ $workshop->price ?? '' }}" />
</div>
<div class="flex-1">
<x-ui.input label="Ages" name="ages" info="Leave blank to hide from public" value="{{ $event->ages ?? '8+' }}" />
<x-ui.input label="Ages" name="ages" info="Leave blank to hide from public" value="{{ $workshop->ages ?? '8+' }}" />
</div>
</div>
<div class="flex flex-col sm:flex-row sm:gap-8">
<div class="flex-1">
<x-ui.select label="Registration" name="registration" x-model="registration" onchange="document.getElementsByName('registration_data').forEach((e)=>e.value='')">
<option value="none" {{ (old('registration', $event->registration ?? '')) === 'none' ? 'selected' : '' }}>None</option>
<option value="link" {{ (old('registration', $event->registration ?? '')) === 'link' ? 'selected' : '' }}>External Link</option>
<option value="email" {{ (old('registration', $event->registration ?? '')) === 'email' ? 'selected' : '' }}>External Email</option>
<option value="message" {{ (old('registration', $event->registration ?? '')) === 'message' ? 'selected' : '' }}>Custom Message</option>
<option value="none" {{ (old('registration', $workshop->registration ?? '')) === 'none' ? 'selected' : '' }}>None</option>
<option value="link" {{ (old('registration', $workshop->registration ?? '')) === 'link' ? 'selected' : '' }}>External Link</option>
<option value="email" {{ (old('registration', $workshop->registration ?? '')) === 'email' ? 'selected' : '' }}>External Email</option>
<option value="message" {{ (old('registration', $workshop->registration ?? '')) === 'message' ? 'selected' : '' }}>Custom Message</option>
</x-ui.select>
</div>
<div class="flex-1">
<span x-show="registration==='link'">
<x-ui.input label="Registration URL" name="registration_url" id="registration_url" value="{{ $event->registration_data ?? '' }}" error="{{ $errors->first('registration_data') }}" />
<x-ui.input label="Registration URL" name="registration_url" id="registration_url" value="{!! isset($workshop) ? $workshop->registration_data : '' !!}" error="{{ $errors->first('registration_data') }}" />
</span>
<span x-show="registration==='email'">
<x-ui.input label="Registration Email" name="registration_email" id="registration_email" value="{{ $event->registration_data ?? '' }}" error="{{ $errors->first('registration_data') }}" />
<x-ui.input label="Registration Email" name="registration_email" id="registration_email" value="{{ $workshop->registration_data ?? '' }}" error="{{ $errors->first('registration_data') }}" />
</span>
<span x-show="registration==='message'">
<x-ui.input label="Registration Message" name="registration_message" id="registration_message" value="{{ $event->registration_data ?? '' }}" error="{{ $errors->first('registration_data') }}" />
<x-ui.input label="Registration Message" name="registration_message" id="registration_message" value="{{ $workshop->registration_data ?? '' }}" error="{{ $errors->first('registration_data') }}" />
</span>
<input type="hidden" name="registration_data" id="registration_data" value="{{ $event->registration_data ?? '' }}">
<input type="hidden" name="registration_data" id="registration_data" value="{{ $workshop->registration_data ?? '' }}">
</div>
</div>
<div class="mb-4">
<x-ui.editor
label="Content"
name="content"
value="{!! $eventContent !!}"
value="{!! $workshopContent !!}"
></x-ui.editor>
</div>
<div class="mb-4">
@@ -107,14 +107,14 @@
label="Files"
name="files"
editor="true"
value="{!! isset($event) ? $event->files()->orderBy('name')->get() : '' !!}"
value="{!! isset($workshop) ? $workshop->files()->orderBy('name')->get() : '' !!}"
></x-ui.filelist>
</div>
<div class="flex justify-end gap-4 mt-8">
@isset($event)
<x-ui.button type="button" color="danger" x-data x-on:click.prevent="SM.confirmDelete('{{ csrf_token() }}', 'Delete workshop?', 'Are you sure you want to delete this workshop? This action cannot be undone', '{{ route('admin.event.destroy', $event) }}')">Delete</x-ui.button>
@isset($workshop)
<x-ui.button type="button" color="danger" x-data x-on:click.prevent="SM.confirmDelete('{{ csrf_token() }}', 'Delete workshop?', 'Are you sure you want to delete this workshop? This action cannot be undone', '{{ route('admin.workshop.destroy', $workshop) }}')">Delete</x-ui.button>
@endisset
<x-ui.button type="submit">{{ isset($event) ? 'Save' : 'Create' }}</x-ui.button>
<x-ui.button type="submit">{{ isset($workshop) ? 'Save' : 'Create' }}</x-ui.button>
</div>
</form>
</x-container>

View File

@@ -4,14 +4,14 @@
<x-container>
<div class="flex my-4 items-center">
<div class="flex-1">
<x-ui.button type="link" href="{{ route('admin.event.create') }}">Create</x-ui.button>
<x-ui.button type="link" href="{{ route('admin.workshop.create') }}">Create</x-ui.button>
</div>
<div class="flex-1">
<x-ui.search name="search" label="Search" />
</div>
</div>
@if($events->isEmpty())
@if($workshops->isEmpty())
<x-none-found item="workshops" search="{{ request()->get('search') }}" />
@else
<x-ui.table>
@@ -23,24 +23,24 @@
<th>Action</th>
</x-slot:header>
<x-slot:body>
@foreach ($events as $event)
@foreach ($workshops as $workshop)
<tr>
<td class="flex items-center">
<img src="{{ $event->hero->thumbnail }}" class="max-h-12 max-w-12 -ml-2 -my-3 mr-3 inline rounded" alt="{{ $event->hero->title }}" />
<img src="{{ $workshop->hero->thumbnail }}" class="max-h-12 max-w-12 -ml-2 -my-3 mr-3 inline rounded" alt="{{ $workshop->hero->title }}" />
<div>
<div class="whitespace-normal">{{ $event->title }}</div>
<div class="lg:hidden text-xs text-gray-500">{{ $event->location->name }} ({{ ucwords($event->status) }})</div>
<div class="md:hidden text-xs text-gray-500">{{ \Carbon\Carbon::parse($event->starts_at)->format('j/m/Y g:i a') }}</div>
<div class="whitespace-normal">{{ $workshop->title }}</div>
<div class="lg:hidden text-xs text-gray-500">{{ $workshop->location->name }} ({{ ucwords($workshop->status) }})</div>
<div class="md:hidden text-xs text-gray-500">{{ \Carbon\Carbon::parse($workshop->starts_at)->format('j/m/Y g:i a') }}</div>
</div>
</td>
<td class="hidden lg:table-cell">{{ ucwords($event->status) }}</td>
<td class="hidden lg:table-cell">{{ $event->location->name }}</td>
<td class="hidden md:table-cell">{{ \Carbon\Carbon::parse($event->starts_at)->format('M j Y, g:i a') }}</td>
<td class="hidden lg:table-cell">{{ ucwords($workshop->status) }}</td>
<td class="hidden lg:table-cell">{{ $workshop->location->name }}</td>
<td class="hidden md:table-cell">{{ \Carbon\Carbon::parse($workshop->starts_at)->format('M j Y, g:i a') }}</td>
<td>
<div class="flex justify-center gap-3">
<a href="{{ route('admin.event.edit', $event) }}" class="hover:text-primary-color" title="Edit"><i class="fa-solid fa-pen-to-square"></i></a>
<a href="{{ route('admin.event.duplicate', $event) }}" class="hover:text-primary-color" title="Duplicate"><i class="fa-regular fa-copy"></i></a>
<a href="#" class="hover:text-red-600" x-data x-on:click.prevent="SM.confirmDelete('{{ csrf_token() }}', 'Delete workshop?', 'Are you sure you want to delete this workshop? This action cannot be undone', '{{ route('admin.event.destroy', $event) }}')" title="Delete"><i class="fa-solid fa-trash"></i></a>
<a href="{{ route('admin.workshop.edit', $workshop) }}" class="hover:text-primary-color" title="Edit"><i class="fa-solid fa-pen-to-square"></i></a>
<a href="{{ route('admin.workshop.duplicate', $workshop) }}" class="hover:text-primary-color" title="Duplicate"><i class="fa-regular fa-copy"></i></a>
<a href="#" class="hover:text-red-600" x-data x-on:click.prevent="SM.confirmDelete('{{ csrf_token() }}', 'Delete workshop?', 'Are you sure you want to delete this workshop? This action cannot be undone', '{{ route('admin.workshop.destroy', $workshop) }}')" title="Delete"><i class="fa-solid fa-trash"></i></a>
</div>
</td>
</tr>
@@ -48,7 +48,7 @@
</x-slot:body>
</x-ui.table>
{{ $events->appends(request()->query())->links() }}
{{ $workshops->appends(request()->query())->links() }}
@endif
</x-container>

View File

@@ -0,0 +1,68 @@
@php
if(!isset($email)) {
$email = '';
if(isset($user)) {
$email = $user->email;
}
}
@endphp
<x-layout :bodyClass="'image-background'">
<div x-data="{show:'{{ $method ?? 'tfa' }}'}">
<x-dialog x-cloak x-show="show==='tfa'" formaction="{{ route('login.store') }}">
<x-slot:title>
<a class="link absolute left-0" href="{{ route('login') }}"><i class="fa-solid fa-angle-left"></i></a>
Please enter 2FA code
</x-slot:title>
<x-slot:header>
<p class="text-sm">Two-factor authentication (2FA) is enabled for your account. Please enter a code to log in.</p>
</x-slot:header>
<input type="hidden" name="email" value="{{ $email }}"/>
<x-ui.input type="text" name="code" label="Code" floating autofocus error="{{ $errors->first('code') }}"/>
<x-slot:footer>
<div class="text-xs">
Having trouble? <a class="link" href="#" x-on:click.prevent="show='other'">Sign in another way</a>
</div>
<x-ui.button type="submit">Verify</x-ui.button>
</x-slot:footer>
</x-dialog>
<x-dialog x-cloak x-show="show==='other'">
@captcha
<x-slot:title>
<a class="link absolute left-0" href="#" x-on:click.prevent="show='tfa'"><i class="fa-solid fa-angle-left"></i></a>
Sign in another way
</x-slot:title>
<x-slot:header>Select the method to sign in to your account</x-slot:header>
<div class="flex flex-col gap-4 mb-4">
<form method="post" action="{{ route('login.store') }}">
@csrf
@captcha
<input type="hidden" name="email" value="{{ $email }}" />
<input type="hidden" name="method" value="email" />
<x-ui.button type="submit" class="w-full">Email Link</x-ui.button>
</form>
<x-ui.button type="button" x-on:click.prevent="show='backup'">Enter Backup Code</x-ui.button>
</div>
<x-slot:footer>
<div class="text-xs">If you need support for accessing your account, please contact STEMMechanics support at <a href="mailto:hello@stemmechanics.com.au" class="link">hello@stemmechanics.com.au</a></div>
</x-slot:footer>
</x-dialog>
<x-dialog x-cloak x-show="show==='backup'" formaction="{{ route('login.store') }}">
<x-slot:title>
<a class="link absolute left-0" href="#" x-on:click.prevent="show='other'"><i class="fa-solid fa-angle-left"></i></a>
Please enter a backup code
</x-slot:title>
<x-slot:header>
<p class="text-sm">Enter one of your backup codes below to log in. Once a backup codes are a 1 time use only.</p>
</x-slot:header>
@captcha
<input type="hidden" name="email" value="{{ $email }}"/>
<x-ui.input type="text" name="backup_code" label="Backup Code" floating autofocus error="{{ $errors->first('backup_code') }}" />
<x-slot:footer center>
<x-ui.button class="self-end" type="submit">Verify</x-ui.button>
</x-slot:footer>
</x-dialog>
</div>
</x-layout>

View File

@@ -0,0 +1,14 @@
<x-layout :bodyClass="'image-background'">
<x-dialog formaction="{{ route('login.store') }}">
@captcha
<x-slot:title>Sign in another way</x-slot:title>
<x-slot:header>Select the method to sign in to your account</x-slot:header>
<div class="flex flex-col gap-4 mb-4">
<x-ui.button type="button" onclick="loginUsingEmail()">Email Link</x-ui.button>
<x-ui.button type="button" onclick="loginUsingEmail()">Enter Backup Code</x-ui.button>
</div>
<x-slot:footer>
<div class="text-xs">If you need support for accessing your account, please contact STEMMechanics support at <a href="mailto:hello@stemmechanics.com.au" class="link">hello@stemmechanics.com.au</a></div>
</x-slot:footer>
</x-dialog>
</x-layout>

View File

@@ -1,5 +1,6 @@
<x-layout :bodyClass="'image-background'">
<x-dialog formaction="{{ route('login.store') }}">
@captcha
@if(session('status') == 'not-found')
<x-slot:title>Sorry, we didn't recognize that email</x-slot:title>
<x-slot:header>
@@ -8,7 +9,7 @@
@else
<x-slot:title>Sign in with email</x-slot:title>
<x-slot:header>
<p>Enter the email address associated with your account, and we'll send a magic link to your inbox.</p>
<p>Enter the email address associated with your account</p>
</x-slot:header>
@endif
<x-ui.input type="email" name="email" label="Email" floating autofocus />

View File

@@ -1,7 +1,7 @@
<div class="flex items-center justify-center flex-grow py-24">
<div class="flex items-center justify-center flex-grow py-24" {{ $attributes }}>
<div class="w-full mx-2 max-w-lg p-8 pb-6 bg-white rounded-md shadow-deep">
@isset($title)
<h2 class="text-2xl font-bold mb-4 text-center">{{ $title }}</h2>
<h2 class="text-2xl font-bold mb-4 text-center relative">{{ $title }}</h2>
@endisset
@isset($header)
<div class="flex items-center gap-4 mb-4">

View File

@@ -7,10 +7,12 @@
<li><a href="https://discord.gg/yNzk4x7mpD" class="text-sm hover:text-primary-color">Discord</a></li>
<li><a href="https://www.facebook.com/stemmechanics" class="text-sm hover:text-primary-color">Facebook</a></li>
<li><a href="https://www.stemcraft.com.au/" class="text-sm hover:text-primary-color">STEMCraft (Minecraft)</a></li>
<li><a href="https://jenkins.stemmechanics.com.au/" class="text-sm hover:text-primary-color">Jenkins</a></li>
<li><a href="https://youtube.com/@STEMMechanics" class="text-sm hover:text-primary-color">YouTube</a></li>
</ul>
<ul class="sm:w-1/3 flex flex-col gap-0.5 text-center sm:text-left">
<li><h3 class="font-bold mb-2">STEMMechanics</h3></li>
<li><a href="{{ route('about') }}" class="text-sm hover:text-primary-color">About</a></li>
<li><a href="{{ route('contact') }}" class="text-sm hover:text-primary-color">Contact Us</a></li>
<li><a href="{{ route('code-of-conduct') }}" class="text-sm hover:text-primary-color">Code of Conduct</a></li>
<li><a href="{{ route('terms-conditions') }}" class="text-sm hover:text-primary-color">Terms & Conditions</a></li>

View File

@@ -31,6 +31,7 @@
</script>
@endif
@stack('scripts')
@captchaScripts
@livewireScripts
</body>
</html>

View File

@@ -1,6 +1,16 @@
<x-container class="bg-primary-color-light text-white py-10">
<h1 class="font-bold text-4xl">{{ $slot }}</h1>
<h1 class="font-bold text-4xl">{{ $title ?? $slot }}</h1>
@if(isset($description))
<div class="text-lg">{{ $description }}</div>
@endif
@if(isset($backRoute) && isset($backTitle))
<a href="{{ route($backRoute) }}" class="text-lg hover:text-gray-300"><i class="fa-solid fa-angle-left mr-3"></i>{{ $backTitle }}</a>
@endif
@isset($tabs)
<div class="mt-4 -mb-10 flex justify-end">
@foreach($tabs as $tab)
<a href="{{ $tab['route'] }}" class="rounded-t-md px-4 py-2 {{ ('/' . request()->path() === parse_url($tab['route'], PHP_URL_PATH) ? 'bg-gray-100 text-primary-color-dark' : 'text-white hover:bg-primary-color-dark') }} transition-colors">{{ $tab['title'] }}</a>
@endforeach
</div>
@endisset
</x-container>

View File

@@ -1,60 +1,137 @@
<nav class="shadow bg-white">
<div class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8 relative" x-data="{open:false}">
<nav class="shadow bg-white" x-data="{showSearch:false}" x-init="
document.addEventListener('keydown', (event) => {
if ((event.metaKey || event.ctrlKey) && event.key === 'f') {
event.preventDefault();
$data.showSearch = true;
}
})
">
<div class="mx-auto max-w-7xl px-2 relative" x-data="{pageMenuOpen:false,userMenuOpen:false}">
<div class="relative flex h-16 items-center justify-between">
<div class="absolute inset-y-0 left-0 flex items-center sm:hidden">
<!-- Mobile menu button-->
<button type="button" class="relative inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" aria-controls="mobile-menu" aria-expanded="false">
<span class="absolute -inset-0.5"></span>
<span class="sr-only">Open main menu</span>
{{-- <img src="/assets/logo.svg" alt="STEMMechanics" onload="SVGInject(this)" style="color:purple"/>--}}
<div class="ml-4 mr-2 {{ !auth()->user()?->admin ? 'sm:hidden' : '' }}">
<button type="button" @click="pageMenuOpen=!pageMenuOpen" @keydown.escape="pageMenuOpen=false" class="relative flex w-6 text-gray-400 hover:text-white" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
<span class="sr-only">Open page menu</span>
<i class="fa fa-bars text-gray-800 hover:text-sky-500 transition"></i>
</button>
</div>
<div class="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
<div class="flex flex-1 items-center justify-center sm:justify-start ml-2">
<div class="flex flex-shrink-0 items-center">
<a href="{{ route('index') }}">
@includeSVG('logo.svg', 'width:14rem;margin-top:-0.2rem;color:black')
</a>
</div>
</div>
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
<div class="flex items-center">
<div class="hidden sm:ml-6 sm:block mr-4">
<div class="flex space-x-2">
<a href="{{ route('post.index') }}" class="text-gray-900 hover:text-sky-500 px-3 py-2 text-sm font-medium transition duration-300 ease-in-out transform hover:-translate-y-0.5" aria-current="page">Blog</a>
<a href="{{ route('event.index') }}" class="text-gray-900 hover:text-sky-500 px-3 py-2 text-sm font-medium transition duration-300 ease-in-out transform hover:-translate-y-0.5">Workshops</a>
</div>
</div>
<div class="ml-3">
<div>
<button type="button" @click="open=!open" @keydown.escape="open=false" class="relative flex w-6 text-gray-400 hover:text-white" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
<span class="sr-only">Open user menu</span>
<svg class="w-6 h-6 text-gray-800 hover:text-sky-500 transition" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M5 7h14M5 12h14M5 17h14"/>
</svg>
{{-- <a href="{{ route('post.index') }}" class="text-gray-900 hover:text-sky-500 px-3 py-2 text-sm font-medium transition duration-300 ease-in-out" aria-current="page">Blog</a>--}}
<a href="{{ route('about') }}" class="text-gray-900 hover:text-sky-500 px-1 md:px-3 py-2 text-sm font-medium transition duration-300 ease-in-out">About</a>
<a href="{{ route('workshop.index') }}" class="text-gray-900 hover:text-sky-500 px-1 md:px-3 py-2 text-sm font-medium transition duration-300 ease-in-out">Workshops</a>
<a href="{{ route('contact') }}" class="text-gray-900 hover:text-sky-500 px-1 md:px-3 py-2 text-sm font-medium transition duration-300 ease-in-out">Contact</a>
<button type="button" class="text-gray-900 hover:text-sky-500 text-sm md:pl-1 font-medium transition duration-300 ease-in-out" @click.prevent="showSearch=true">
<i class="fa fa-search"></i>
</button>
</div>
</div>
<div class="mr-3 md:mx-3">
<button type="button" @click="userMenuOpen=!userMenuOpen" @keydown.escape="userMenuOpen=false" class="relative flex w-6 text-gray-400 hover:text-white" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
<span class="sr-only">Open user menu</span>
<i class="fa-regular fa-user-circle text-gray-800 hover:text-sky-500 transition"></i>
</button>
</div>
</div>
</div>
<div x-show="open" @click.away="open=false" x-cloak class="absolute w-full right-0 sm:right-5 sm:top-9 z-50 sm:mt-2 sm:w-48 origin-top-right sm:rounded-md bg-white py-3 px-2 shadow-lg border-t sm:ring-1 ring-black ring-opacity-25 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
<a href="{{ route('post.index') }}" class="sm:hidden block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-regular fa-newspaper w-4 mr-2"></i>Blog</a>
<a href="{{ route('event.index') }}" class="sm:hidden block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-bullhorn w-4 mr-2"></i>Workshops</a>
<div class="sm:hidden border-t border-gray-200 my-2"></div>
@if(auth()->guest())
<a href="{{ route('register') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-pen-to-square w-4 mr-2"></i>Register</a>
<a href="{{ route('login') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-right-to-bracket w-4 mr-2"></i>Log in</a>
@else
<div x-show="pageMenuOpen" @click.away="pageMenuOpen=false" x-cloak class="fixed left-0 top-0 h-full w-full z-20" role="menu" aria-labelledby="page-menu-button" tabindex="-1">
<div x-show="pageMenuOpen" @click="pageMenuOpen=false" class="absolute inset-0 bg-black/40 backdrop-blur-sm"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"></div>
<div x-show="pageMenuOpen" class="relative h-full left-0 top-0 w-96 max-w-full bg-white z-50 shadow-lg p-4"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform -translate-x-full"
x-transition:enter-end="opacity-100 transform translate-x-0"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100 transform translate-x-0"
x-transition:leave-end="opacity-0 transform -translate-x-full">
<div class="flex justify-between mb-4">
<div>
@includeSVG('logo.svg', 'width:10em;color:black')
</div>
<button @click="pageMenuOpen=false" class="hover:text-red-500">
<i class="fa fa-times"></i>
</button>
</div>
<div class="sm:hidden block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1" @click.prevent="showSearch=true">
<i class="fa fa-search w-4 mr-2"></i>Search
</div>
{{-- <a href="{{ route('post.index') }}" class="sm:hidden block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-regular fa-newspaper w-4 mr-2"></i>Blog</a>--}}
<a href="{{ route('about') }}" class="sm:hidden block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-circle-info w-4 mr-2"></i>About</a>
<a href="{{ route('workshop.index') }}" class="sm:hidden block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-bullhorn w-4 mr-2"></i>Workshops</a>
<a href="{{ route('contact') }}" class="sm:hidden block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-regular fa-envelope w-4 mr-2"></i>Contact</a>
@if(auth()->user()?->admin)
<div class="sm:hidden border-t border-gray-200 my-2"></div>
<div class="block text-xs font-semibold text-gray-500 px-2 py-1">Admin</div>
<a href="{{ route('admin.location.index') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-location-dot w-4 mr-2"></i>Locations</a>
<a href="{{ route('admin.media.index') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-photo-film w-4 mr-2"></i>Media</a>
<a href="{{ route('admin.post.index') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-regular fa-newspaper w-4 mr-2"></i>Posts</a>
{{-- <a href="{{ route('admin.post.index') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-regular fa-newspaper w-4 mr-2"></i>Posts</a>--}}
<a href="{{ route('admin.user.index') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-users w-4 mr-2"></i>Users</a>
<a href="{{ route('admin.event.index') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-regular fa-calendar w-4 mr-2"></i>Events</a>
<div class="border-t border-gray-200 my-2"></div>
<a href="{{ route('admin.workshop.index') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-bullhorn w-4 mr-2"></i>Workshops</a>
@endif
<a href="{{ route('account.show') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-user-pen w-4 mr-2"></i>Account</a>
<a href="{{ route('logout') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-right-from-bracket w-4 mr-2"></i>Log out</a>
@endif
</div>
</div>
<div
x-show="userMenuOpen"
@click.away="userMenuOpen=false"
x-cloak
>
<div x-show="userMenuOpen" @click="userMenuOpen=false" class="fixed left-0 w-screen z-20 h-screen bg-black/40 backdrop-blur-sm"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"></div>
<div
x-show="userMenuOpen"
class="absolute w-full right-0 sm:right-5 sm:top-12 z-50 sm:mt-2 sm:w-64 origin-top-right sm:rounded-md bg-white py-3 px-2 shadow-lg border-t border-gray-200 sm:ring-1 ring-black/25 focus:outline-none">
@if(auth()->guest())
<a href="{{ route('register') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-pen-to-square w-4 mr-2"></i>Register</a>
<a href="{{ route('login') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-right-to-bracket w-4 mr-2"></i>Log in</a>
@else
<div class="text-lg font-semibold px-4 py-1 text-gray-700">Welcome {{ auth()->user()->firstname ?? strstr(auth()->user()->email, '@', true) }}</div>
<div class="border-t border-gray-200 my-2"></div>
<a href="{{ route('account.show') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-user-pen w-4 mr-2"></i>Account</a>
<a href="{{ route('logout') }}" class="block px-4 py-2 text-sm text-gray-700 rounded transition hover:bg-sky-600 hover:text-white" role="menuitem" tabindex="-1"><i class="fa-solid fa-right-from-bracket w-4 mr-2"></i>Log out</a>
@endif
</div>
</div>
</div>
<div class="fixed inset-0 z-50 flex items-center justify-center" x-cloak x-show="showSearch" x-on:click="showSearch=false" x-on:keydown.escape.window="showSearch=false" x-init="$watch('showSearch', value => {
if(value) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const el = document.getElementsByName('q')[0];
if (!el) return;
el.focus({ preventScroll: true });
if (typeof el.select === 'function') el.select();
// iOS fallback:
if (el.setSelectionRange) el.setSelectionRange(0, el.value.length);
})
})
}
})">
<div class="absolute inset-0 backdrop-blur-sm bg-black/40"></div>
<div class="relative w-full mx-8 max-w-2xl bg-gray-50 p-2 rounded-lg shadow-lg" x-on:click.stop>
<form action="{{ route('search.index') }}" method="GET">
<x-ui.search type="text" name="q" label="Search..." />
</form>
</div>
</div>
</nav>

View File

@@ -0,0 +1,19 @@
@props(['item' => 'results', 'search', 'message', 'title'])
@php
if(!isset($message)) {
if(!isset($search) || $search == '')
$message = "We couldn't find any $item";
else
$message = "We couldn't find any $item matching \"$search\"";
}
if(!isset($title)) {
$title = "No results found";
}
@endphp
<div class="flex flex-col items-center my-8 w-full">
<i class="text-gray-300 mb-6 text-8xl fa-solid fa-cat"></i>
<p class="text-gray-500 mt-2">No workshops coming up. Were on a short break, mostly by playing with the cat.</p>
</div>

View File

@@ -1,45 +0,0 @@
@props(['event'])
@php
$statusClass = $event->status;
$statusTitle = $event->status;
if($event->status === 'scheduled') {
$statusClass = 'soon';
$statusTitle = 'Open soon';
}
@endphp
<a href="{{ route('event.show', $event) }}" class="flex flex-col bg-white border rounded-lg overflow-hidden hover:shadow-lg hover:scale-[101%] transition-all relative {{ $attributes->get('class') }}">
<div class="shadow border rounded px-3 py-2 absolute top-2 left-2 flex flex-col justify-center items-center bg-white">
<div class="text-gray-600 font-bold leading-none">{{ $event->starts_at->format('j') }}</div>
<div class="text-gray-600 text-xs uppercase">{{ $event->starts_at->format('M') }}</div>
</div>
<div class="border border-white border-opacity-50 absolute flex items-center justify-center top-5 -right-9 bg-gray-500 w-36 text-sm text-white font-bold uppercase py-1 rotate-45 h-8 sm-banner-{{ strtolower($statusClass) }}">{{ $statusTitle }}</div>
<img src="{{ $event->hero?->url }}?md" alt="{{ $event->title }}" class="w-full h-64 object-cover object-center">
<div class="flex-grow p-4 flex flex-col">
<h2 class="flex-grow {{ strlen($event->title) > 25 ? 'text-lg' : 'text-xl' }} font-bold mb-2">{{ $event->title }}</h2>
<div class="text-gray-600 text-sm mb-1 flex gap-2">
<div class="w-6 flex items-center justify-center">
<i class="fa-regular fa-calendar"></i>
</div>{{ $event->starts_at->format('j/m/Y @ g:i a') }}
</div>
<div class="text-gray-600 text-sm mb-1 flex gap-2">
<div class="w-6 flex items-center justify-center">
<i class="fa-solid fa-location-dot"></i>
</div>{{ $event->location->name }}
</div>
@if($event->ages)
<div class="text-gray-600 text-sm mb-1 flex gap-2">
<div class="w-6 flex items-center justify-center">
<i class="fa-regular fa-face-smile"></i>
</div>{{ isset($event->ages) && $event->ages !== '' ? 'Ages ' . $event->ages : 'All ages' }}
</div>
@endif
<div class="text-gray-600 text-sm mb-1 flex gap-2">
<div class="w-6 flex items-center justify-center">
<i class="fa-solid fa-dollar-sign"></i>
</div>{{ isset($event->price) && $event->price !== '' && $event->price !== '0' ? $event->price : 'Free' }}
</div>
</div>
</a>

View File

@@ -0,0 +1,46 @@
@props(['workshop'])
@php
$statusClass = $workshop->status;
$statusTitle = $workshop->status;
if($workshop->status === 'scheduled') {
$statusClass = 'soon';
$statusTitle = 'Open soon';
}
@endphp
<a href="{{ route('workshop.show', $workshop) }}" class="flex flex-col bg-white border border-gray-200 rounded-lg overflow-hidden hover:shadow-lg hover:scale-[101%] transition-all relative {{ $attributes->get('class') }}">
<div class="shadow border border-gray-200 rounded px-3 py-2 absolute top-2 left-2 flex flex-col justify-center items-center bg-white">
<div class="text-gray-600 font-bold leading-none">{{ $workshop->starts_at->format('j') }}</div>
<div class="text-gray-600 text-xs uppercase">{{ $workshop->starts_at->format('M') }}</div>
</div>
<div class="border border-white/50 absolute flex items-center justify-center top-5 -right-9 bg-gray-500 w-36 text-sm text-white font-bold uppercase py-1 rotate-45 h-8 sm-banner-{{ strtolower($statusClass) }}">{{ $statusTitle }}</div>
<img src="{{ $workshop->hero?->url }}?md" alt="{{ $workshop->title }}" class="w-full h-64 object-cover object-center">
<div class="flex-grow p-4 flex flex-col">
<h2 class="flex-grow {{ strlen($workshop->title) > 25 ? 'text-lg' : 'text-xl' }} font-bold mb-2">{{ $workshop->title }}</h2>
<div class="text-gray-600 text-sm mb-1 flex gap-2">
<div class="w-6 flex items-center justify-center">
<i class="fa-regular fa-calendar"></i>
</div>{{ $workshop->starts_at->format('j/m/Y @ g:i a') }}
</div>
<div class="text-gray-600 text-sm mb-1 flex gap-2">
<div class="w-6 flex items-center justify-center">
<i class="fa-solid fa-location-dot"></i>
</div>{{ $workshop->location->name }}
</div>
@if($workshop->ages)
<div class="text-gray-600 text-sm mb-1 flex gap-2">
<div class="w-6 flex items-center justify-center">
<i class="fa-regular fa-face-smile"></i>
</div>{{ $workshop->ages ? 'Ages ' . $workshop->ages : 'All ages' }}
</div>
@endif
<div class="text-gray-600 text-sm mb-1 flex gap-2">
<div class="w-6 flex items-center justify-center">
<i class="fa-solid fa-dollar-sign"></i>
</div>
{{ $workshop->price && $workshop->price !== '0' ? number_format((float)$workshop->price, 2) : 'Free' }}
</div>
</div>
</a>

View File

@@ -4,16 +4,19 @@
$colorClasses = [
'outline' => 'hover:bg-gray-500 focus-visible:outline-primary-color text-gray-800 border border-gray-400 bg-white hover:text-white',
'primary' => 'hover:bg-primary-color-dark focus-visible:outline-primary-color bg-primary-color text-white',
'primary-sm' => '!font-normal !text-xs !px-4 !py-1 hover:bg-primary-color-dark focus-visible:outline-primary-color bg-primary-color text-white',
'primary-outline' => 'hover:bg-primary-color-dark focus-visible:outline-primary-color text-primary-color border border-primary-color bg-white hover:text-white',
'primary-outline-sm' => '!font-normal !text-xs !px-4 !py-1 hover:bg-primary-color-dark focus-visible:outline-primary-color text-primary-color border border-primary-color bg-white hover:text-white',
'danger' => 'hover:bg-danger-color-dark focus-visible:outline-danger-color bg-danger-color text-white',
'success' => 'hover:bg-success-color-dark focus-visible:outline-success-color bg-success-color text-white'
'danger-outline' => 'hover:bg-danger-color-dark focus-visible:outline-danger-color text-danger-color border border-danger-color bg-white hover:text-white',
'success' => 'hover:bg-success-color-dark focus-visible:outline-success-color bg-success-color text-white',
'dark' => 'hover:bg-gray-900 focus-visible:outline-gray-800 bg-gray-800 text-white'
][$color];
$commonClasses = @twMerge(['whitespace-nowrap', 'text-center','justify-center','rounded-md','px-8','py-1.5','text-sm','font-semibold','leading-6','shadow-sm','focus-visible:outline','focus-visible:outline-2','focus-visible:outline-offset-2','transition'], ($class ?? ''));
@endphp
@if($type == 'submit' || $type == 'button')
@if($type === 'submit' || $type === 'button')
<button type="{{ $type }}" class="{{ $colorClasses . ' ' . $commonClasses }}" {{ $attributes }}>{{ $slot }}</button>
@elseif($type == 'link')
@elseif($type === 'link')
<a href="{{ $href ?? '#' }}" target="{{ $target ?? '_self' }}" class="{{ $colorClasses . ' ' . $commonClasses }}" {{ $attributes }}">{{ $slot }}</a>
@endif

View File

@@ -1,4 +1,4 @@
@props(['type' => 'text', 'name', 'label' => '', 'value' => '', 'floating' => false, 'noLabel' => false, 'readonly' => false, 'info', 'error' => null, 'labelNotice' => null])
@props(['type' => 'text', 'name', 'label' => '', 'value' => '', 'floating' => false, 'noLabel' => false, 'readonly' => false, 'info', 'error' => null, 'labelNotice' => null, 'placeholder' => '', 'fieldClasses' => '' ])
@php
if($error === null) {
@@ -6,7 +6,7 @@
}
$hasError = $error !== '';
$classes = 'disabled:bg-gray-100 bg-white block px-2.5 pb-2.5 w-full text-sm text-gray-900 rounded-lg border appearance-nonefocus:outline-none focus:ring-0 focus:border-blue-600 ' . ($hasError ? 'border-red-600 ring-red-600 focus:border-red-600 focus:ring-red-600' : 'border-gray-300 focus:border-indigo-300 focus:ring-indigo-300');
$classes = 'disabled:bg-gray-100 bg-white block px-2.5 pb-2.5 w-full text-sm text-gray-900 rounded-lg border appearance-none focus:outline-none focus:ring-0 focus:border-blue-600 ' . ($hasError ? 'border-red-600 ring-red-600 focus:border-red-600 focus:ring-red-600' : 'border-gray-300 focus:border-indigo-300 focus:ring-indigo-300');
$value = old($name, $value);
@endphp
@@ -14,27 +14,27 @@
@if($floating)
<div class="relative">
@if($type === 'textarea')
<textarea class="{{ twMerge(['pt-4'], $classes) }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }}>{{ $value }}</textarea>
<textarea class="{{ twMerge(['pt-4'], $classes, $attributes->get('fieldClasses')) }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }}>{{ $value }}</textarea>
@else
<input class="{{ twMerge(['pt-4'], $classes) }}" autocomplete="off" placeholder=" " value="{{ $value }}" type="{{ $type }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }} />
<input class="{{ twMerge(['pt-4'], $classes, $attributes->get('fieldClasses')) }}" autocomplete="off" placeholder=" " value="{{ $value }}" type="{{ $type }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }} />
@endif
<label for="{{ $name }}" class="absolute text-sm text-gray-500 duration-300 transform -translate-y-4 scale-75 top-2 z-10 origin-[0] bg-white px-2 peer-focus:px-2 peer-focus:text-blue-600 peer-placeholder-shown:scale-100 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-1/2 peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4 rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto start-1">{{ $label }}</label>
</div>
@elseif($noLabel)
<div class="relative">
@if($type === 'textarea')
<textarea class="{{ twMerge(['pt-2.5'], $classes) }}" name="{{ $name }}" placeholder="{{ $label }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }}>{{ $value }}</textarea>
<textarea class="{{ twMerge(['pt-2.5'], $classes, $fieldClasses) }}" name="{{ $name }}" placeholder="{{ $label }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }}>{{ $value }}</textarea>
@else
<input class="{{ twMerge(['pt-2.5'], $classes) }}" autocomplete="off" placeholder="{{ $label }}" value="{{ $value }}" type="{{ $type }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }} />
<input class="{{ twMerge(['pt-2.5'], $classes, $fieldClasses) }}" autocomplete="off" placeholder="{{ $label }}" value="{{ $value }}" type="{{ $type }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }} />
@endif
</div>
@else
<div>
<label for="{{ $name }}" class="block text-sm pl-1">{{ $label }}{!! isset($labelNotice) && $labelNotice !== '' ? '<i class="fa-solid fa-triangle-exclamation ml-1 text-gray-500 hover:text-black" data-tooltip="' . $labelNotice . '"></i>' : '' !!}</label>
@if($type === 'textarea')
<textarea class="{{ twMerge(['pt-2.5','mt-1','h-96'], $classes) }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes->whereDoesntStartWith('x-') }}>{{ $value }}</textarea>
<textarea class="{{ twMerge(['pt-2.5','mt-1','h-96'], $classes, $fieldClasses) }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes->whereDoesntStartWith('x-') }}>{{ $value }}</textarea>
@else
<input class="{{ twMerge(['pt-2.5','mt-1'], $classes) }}" autocomplete="off" placeholder=" " value="{{ $value }}" type="{{ $type }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }} />
<input class="{{ twMerge(['pt-2.5','mt-1'], $classes, $fieldClasses) }}" autocomplete="off" placeholder=" " value="{{ $value }}" type="{{ $type }}" name="{{ $name }}" {{ $readonly ? 'readonly' : '' }} {{ $attributes }} />
@endif
</div>
@endif

View File

@@ -36,7 +36,7 @@
imgElement.src = details.thumbnail;
imgElement.classList.remove('hidden');
placeholderElement.classList.add('hidden');
placeholderElement.classList.add('hidden!');
document.getElementById(name).value = value;
});

View File

@@ -5,7 +5,7 @@
@endphp
<form method="GET" action="{{ url()->current() }}" class="{{ $attributes->get('class') }}">
<div class="flex relative" x-data="{search:'{{request()->get('search')}}'}">
<div class="flex relative" x-data="{search:'{{request()->get('q')}}'}">
<input class="{{ $classes }}" autocomplete="off" placeholder="{{ $label }}" x-model="search" type="{{ $type }}" name="{{ $name }}" />
<x-ui.button type="submit" class="rounded-l-none px-6"><i class="fa-solid fa-magnifying-glass"></i></x-ui.button>
<i x-show="search" cloak class="absolute z-10 top-1/2 right-[4.5rem] transform -translate-y-1/2 text-gray-300 hover:text-gray-400 cursor-pointer fa-solid fa-circle-xmark" x-data x-on:click="search='';$nextTick(()=>{if('{{request()->get('search')}}'!==''){$el.closest('form').submit();}})"></i>

View File

@@ -12,8 +12,8 @@
<p class="mb-2">We do not have a physical address as our workshops are delivered across Queensland. Visit the workshops page for each specific location.</p>
<p class="mb-4">Official mail can be sent to the following postal address:</p>
<p class="mb-2 text-center">STEMMechanics<br />
1/4 Jordan Street<br />
Edmonton, QLD, 4869<br />
63 Dalton Street<br />
Westcourt, QLD, 4870<br />
Australia</p>
<p class="mb-2 text-center"><strong class="font-semibold">ABN</strong>: 15 772 281 735</p>
</x-container>

View File

@@ -0,0 +1,13 @@
@component('mail::message', ['email' => $email])
<p>Hey there!</p>
<p>We just wanted to let you know that someone just logged in using a backup code.</p>
<p>If this was you, then it is all good!</p>
<p>If it's not, we recommend you let us know by replying to this email and resetting your backup codes by:</p>
<ul>
<li>Logging into your account on STEMMechanics</li>
<li>Visit your account page</li>
<li>Under <strong>Two Factor Authentication</strong> - Click <i>Reset Backup Codes</i></li>
</ul>
<p>Warm regards,</p>
<p>—James 😁</p>
@endcomponent

View File

@@ -0,0 +1,8 @@
@component('mail::message', ['email' => $email])
<p>Hey there!</p>
<p>We just wanted to let you know that using an <strong>Authenticator App</strong> to log in to your account on STEMMechanics has been <strong>Disabled</strong>.</p>
<p>If this was you, then it is all good! - Any previous <i>Backup TFA Codes</i> can no longer be used.</p>
<p>If it's not, we recommend you let us know by replying to this email.</p>
<p>Regards,</p>
<p>—James 😁</p>
@endcomponent

View File

@@ -0,0 +1,8 @@
@component('mail::message', ['email' => $email])
<p>Hey there!</p>
<p>We just wanted to let you know that using an <strong>Authenticator App</strong> to log in to your account on STEMMechanics has been <strong>Enabled</strong>.</p>
<p>If this was you, then it is all good!</p>
<p>If it's not, we recommend you let us know by replying to this email.</p>
<p>Regards,</p>
<p>—James 😁</p>
@endcomponent

View File

@@ -0,0 +1,29 @@
@component('mail::message', ['email' => $email])
<p>Hey there!</p>
<p>Check out our exciting workshops coming up in the next few weeks:</p>
<p class="center">
@php
$currentLocation = null;
@endphp
@foreach($workshops as $workshop)
@if($workshop->location->name !== $currentLocation)
<h2 style="margin-top: 32px; margin-bottom: 6px">{{ $workshop->location->name }}</h2>
@php
$currentLocation = $workshop->location->name;
@endphp
@endif
<p style="margin-bottom: 6px">{{ $workshop->starts_at->format('D, j M, g:i A') . ' - ' }}<a href="{{ route('workshop.show', $workshop->slug) }}">{{ $workshop->title }}</a> ({{ ($workshop->price && is_numeric($workshop->price) && $workshop->price != '0' ? '$' . number_format((float)$workshop->price, 2) : 'Free') . ( $workshop->status === 'scheduled' ? ' / Opens soon' : '') }})</p>
@endforeach
<p class="tall center" style="margin-top: 32px">
@component('mail::button', ['url' => 'https://stemmechanics.com.au/workshops'])
View All Workshops
@endcomponent
</p>
<p>We hope to see you at one of our upcoming workshops!</p>
<p>Warm regards,</p>
<p> James 😁</p>
@slot('subcopy')
<h4>Why did I get this email?</h4>
<p class="sub">You received this email as you are subscribed to our upcoming workshop email list. If you wish no longer receive this email, you can <a href="{{ $unsubscribeLink }}">unsubscribe here</a>.</p>
@endslot
@endcomponent

View File

@@ -1,12 +1,12 @@
@component('mail::message', ['email' => $email])
@component('mail::message', ['email' => $email, 'unsubscribe' => $unsubscribeLink])
<p>Welcome to the community!</p>
<p>Really glad to have you here and can't wait to see you at one of our workshops.</p>
<p>You'll get information about upcoming workshops as it comes out.</p>
<p>Even though this is (of course) an automated email, just wanted to say thanks for registering and intro myself.</p>
<p>Even though this is (of course) an automated email, just wanted to say thanks and intro myself.</p>
<p>If you didn't know, I'm James and I'm the founder of STEMMechanics. I promise not to spam you, sell your data, or send you anything I don't think is absolutely necessary.</p>
<p>You know a bit about me but I don't know really anything about you...</p>
<p><strong>If you're up for it</strong>, reply to this email and tell me a bit about yourself and also let me know what workshops you are interested in?</p>
<p><strong>If you're up for it</strong>, reply to this email or join us in <a href="https://discord.gg/yNzk4x7mpD">Discord</a> and tell me a bit about yourself and also let me know what workshops you are interested in?</p>
<p>I read and reply to every one 😁</p>
<p>Talk soon</p>
<p>—James</p>
<p> James</p>
@endcomponent

View File

@@ -0,0 +1,5 @@
@extends('errors::minimal')
@section('title', __('Unauthorized'))
@section('code', '401')
@section('message', __('Unauthorized'))

View File

@@ -0,0 +1,5 @@
@extends('errors::minimal')
@section('title', __('Payment Required'))
@section('code', '402')
@section('message', __('Payment Required'))

View File

@@ -0,0 +1,5 @@
@extends('errors::minimal')
@section('title', __('Page Expired'))
@section('code', '419')
@section('message', __('Page Expired'))

View File

@@ -0,0 +1,5 @@
@extends('errors::minimal')
@section('title', __('Too Many Requests'))
@section('code', '429')
@section('message', __('Too Many Requests'))

View File

@@ -0,0 +1,5 @@
@extends('errors::minimal')
@section('title', __('Server Error'))
@section('code', '500')
@section('message', __('Server Error'))

View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title')</title>
<!-- Styles -->
<style>
html, body {
background-color: #fff;
color: #636b6f;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-weight: 100;
height: 100vh;
margin: 0;
}
.full-height {
height: 100vh;
}
.flex-center {
align-items: center;
display: flex;
justify-content: center;
}
.position-ref {
position: relative;
}
.content {
text-align: center;
}
.title {
font-size: 36px;
padding: 20px;
}
</style>
</head>
<body>
<div class="flex-center position-ref full-height">
<div class="content">
<div class="title">
@yield('message')
</div>
</div>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -1,30 +0,0 @@
<x-layout>
<x-slot name="title">Workshops</x-slot>
<x-mast>Workshops</x-mast>
<section class="bg-gray-100">
<x-container class="my-4">
<x-ui.search class="md:hidden" name="search" label="Search" value="{{ request()->get('search') }}" />
<form class="hidden md:flex gap-4" method="GET" action="{{ request()->url() }}">
<x-ui.input no-label class="my-0 flex-1" type="text" name="search" label="Keywords" value="{{ request()->get('search') }}"/>
<x-ui.input no-label class="my-0 flex-1" type="text" name="location" label="Location" value="{{ request()->get('location') }}"/>
<x-ui.input no-label class="my-0 flex-1" type="text" name="date" label="Date Range" value="{{ request()->get('date') }}"/>
<x-ui.button type="submit"><i class="fa-solid fa-magnifying-glass"></i></x-ui.button>
</form>
</x-container>
@if($events->isEmpty())
<x-container class="mt-8">
<x-none-found item="workshops" search="{{ request()->get('search') }}" />
</x-container>
@else
<x-container class="mt-4" inner-class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
@foreach ($events as $event)
<x-panel-event :event="$event" />
@endforeach
</x-container>
<x-container>
{{ $events->appends(request()->query())->links() }}
</x-container>
@endif
</section>
</x-layout>

View File

@@ -1,64 +0,0 @@
<x-layout>
<x-container>
<x-ui.image-hero :image="$event->hero?->url" class="my-8" />
<div class="flex sm:gap-16 gap-4 flex-col sm:flex-row">
<div class="flex flex-col flex-1">
<h1 class="text-3xl font-bold mb-6">{!! $event->title !!}</h1>
<article class="content mb-4">{!! $event->content !!}</article>
<x-ui.filelist class="mt-16" value="{!! $event->files()->orderBy('name')->get() !!}" />
</div>
<div class="flex flex-col sm:pt-8 basis-64 flex-grow-0 flex-shrink-0">
@if($event->status === 'closed')
<div class="sm-registration-closed">Registration for this event has closed.</div>
@elseif($event->status === 'full')
<div class="sm-registration-full">This workshop is currently full.</div>
@elseif($event->status === 'private')
<div class="sm-registration-private">This is a private event. Please contact the organiser for details.</div>
@elseif($event->status === 'scheduled')
<div class="sm-registration-scheduled">Registration for this workshop will open soon.</div>
@elseif($event->status === 'cancelled')
<div class="sm-registration-cancelled">This workshop has been cancelled.</div>
@elseif($event->registration === 'none')
<div class="sm-registration-none">Registration not required for this event. Arrive early to avoid disappointment as seating maybe limited.</div>
@elseif($event->registration === 'link')
<x-ui.button href="{{ $event->registration_data }}" class="my-4">Register for Event</x-ui.button>
@elseif($event->registration === 'email')
<div class="sm-registration-email">Registration for this event by emailing <a href="mailto:{{ $event->registration_data }}" class="link">{{ $event->registration_data }}</a>.</div>
@elseif($event->registration === 'message')
<div class="sm-registration-message">{{ $event->registration_data }}</div>
@endif
@if(auth()->user()?->admin)
<x-ui.button class="mb-4" color="primary-outline" href="{{ route('admin.event.edit', $event) }}">Edit Workshop</x-ui.button>
@endif
<h2 class="text-gray-600 text-lg font-bold mt-4 mb-2"><i class="mr-1 fa-regular fa-calendar"></i> Date/Time</h2>
<p class="text-gray-600 text-sm pl-6 mb-6">{!! implode('<br />', \App\Helpers::createTimeDurationStr($event->starts_at, $event->ends_at)) !!}</p>
<h2 class="text-gray-600 text-lg font-bold mb-2"><i class="mr-1 fa-solid fa-location-dot"></i> Location</h2>
<div class="text-gray-600 text-sm pl-6 mb-6">
@if($event->location->url)
<a href="{{ $event->location->url }}" class="link">
@endif
<p>{{ $event->location->name }}</p>
@if($event->location->url)
</a>
@endif
@if($event->location->address_url)
<a href="{{ $event->location->address_url }}" class="link" target="_blank">
@endif
<p class="text-xs">{{ $event->location->address }}</p>
@if($event->location->address_url)
</a>
@endif
</div>
<h2 class="text-gray-600 text-lg font-bold mb-2"><i class="mr-1 fa-regular fa-face-smile"></i> {{ isset($event->ages) && $event->ages !== '' ? 'Ages ' . $event->ages : 'All ages' }}</h2>
@if(\App\Helpers::isUnderAge($event->ages))
<p class="text-gray-600 text-xs pl-3 ml-2 mb-6 border-l-4 border-l-yellow-400">Parental supervision may be required for children 8 years of age and under.</p>
@endif
<h2 class="text-gray-600 text-lg font-bold mb-2"><i class="mr-1 fa-solid fa-dollar-sign"></i> {{ isset($event->price) && $event->price !== '' && $event->price !== '0' ? $event->price : 'Free' }}</h2>
{{-- @if(isset($event->price) && $event->price !== '' && $event->price !== '0' && strtolower($event->price) !== 'free')--}}
{{-- <p class="text-gray-600 text-xs pl-3 ml-2 mb-6 border-l-4 border-l-green-500">Payment by cash or EFTPOS accepted. Please ensure correct change.</p>--}}
{{-- @endif--}}
</div>
</div>
</x-container>
</x-layout>

View File

@@ -1,6 +1,6 @@
<x-layout id="home">
<x-slot name="title">Home</x-slot>
<section id="banner" class="bg-center bg-no-repeat bg-cover" style="background-image:linear-gradient(to right, rgba(0,0,0,.7),rgba(0,0,0,.2)),url('/home-hero.webp')">
<section id="banner" class="bg-center bg-no-repeat bg-cover" style="background-image:linear-gradient(to right, rgba(0,0,0,.7),rgba(0,0,0,.2)),url({{asset('home-hero.webp')}})">
<x-container class="py-32 relative">
<h2 class="text-3xl text-white font-bold mb-4">Join the fun!</h2>
<p class="text-white max-w-[42rem] mb-3">To keep up with our ever-changing world, it's important to encourage and support a new generation of curious minds who love science, engineering, art, and leadership.</p>
@@ -8,24 +8,38 @@
<p class="absolute bottom-3 right-5 bg-black bg-opacity-75 text-white text-xs px-3 py-1 rounded">Steady Hand Game in Ravenshoe</p>
</x-container>
</section>
<section id="news" class="py-12">
<section id="events" class="py-12">
<x-container>
<h2 class="text-2xl font-bold mb-6">Latest Posts</h2>
@if($posts->isEmpty())
<x-none-found item="posts" message="No posts have been published at this time" title="" />
<h2 class="text-2xl font-bold mb-6">Upcoming workshops</h2>
@if($workshops->isEmpty())
<x-on-holiday />
@else
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
@foreach($posts as $index => $post)
<x-panel-post :post="$post" class="{{ $index === 3 ? 'lg:hidden' : '' }}" />
@foreach($workshops as $index => $workshop)
<x-panel-workshop :workshop="$workshop" class="{{ $index === 3 ? 'lg:hidden' : '' }}" />
@endforeach
</div>
@endif
</x-container>
</section>
{{-- <section id="news" class="py-12">--}}
{{-- <x-container>--}}
{{-- <h2 class="text-2xl font-bold mb-6">Latest Posts</h2>--}}
{{-- @if($posts->isEmpty())--}}
{{-- <x-none-found item="posts" message="No posts have been published at this time" title="" />--}}
{{-- @else--}}
{{-- <div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">--}}
{{-- @foreach($posts as $index => $post)--}}
{{-- <x-panel-post :post="$post" class="{{ $index === 3 ? 'lg:hidden' : '' }}" />--}}
{{-- @endforeach--}}
{{-- </div>--}}
{{-- @endif--}}
{{-- </x-container>--}}
{{-- </section>--}}
<section id="skills">
<x-container class="bg-gray-200 py-32 my-8" inner-class="flex flex-row gap-16">
<x-container class="bg-gray-200 py-32" inner-class="flex flex-row gap-16">
<div class="flex-1 min-h-72 hidden md:block">
<div class="h-full bg-no-repeat bg-center bg-cover rounded-lg" style="background-image:url('/home-green-screen.webp')"></div>
<div class="h-full bg-no-repeat bg-center bg-cover rounded-lg" style="background-image:url({{asset('home-green-screen.webp')}})"></div>
</div>
<div class="flex flex-col flex-1 text-center">
<h2 class="text-3xl mb-4 text-center md:text-left">Build skills while having a great time</h2>
@@ -33,47 +47,33 @@
<div class="self-center">
<p class="mb-6 text-left">To keep up with our ever-changing world, it's important to encourage and support a new generation of curious minds who love science, engineering, art, and leadership.</p>
<div class="flex flex-grow justify-center items-center">
<x-ui.button color="success" href="{{ route('event.index') }}" class="font-normal">Explore Workshops</x-ui.button>
<x-ui.button color="success" href="{{ route('workshop.index') }}" class="font-normal">Explore Workshops</x-ui.button>
</div>
</div>
<div class="ml-8 hidden sm:block md:hidden">
<div class="h-48 w-48 bg-no-repeat bg-center bg-cover rounded-full" style="background-image:url('/home-green-screen.webp')"></div>
<div class="h-48 w-48 bg-no-repeat bg-center bg-cover rounded-full" style="background-image:url({{asset('home-green-screen.webp')}})"></div>
</div>
</div>
</div>
</x-container>
</section>
<section id="events" class="pt-4 pb-8">
<x-container>
<h2 class="text-2xl font-bold mb-6">Upcoming workshops</h2>
@if($events->isEmpty())
<x-none-found item="workshops" message="No workshops have been scheduled at this time" title="" />
@else
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
@foreach($events as $index => $event)
<x-panel-event :event="$event" class="{{ $index === 3 ? 'lg:hidden' : '' }}" />
@endforeach
</div>
@endif
</x-container>
</section>
<section id="minecraft" class="bg-center bg-no-repeat bg-cover" style="background-image:url('/home-minecraft.webp')">
<section id="minecraft" class="bg-center bg-no-repeat bg-cover" style="background-image:url({{asset('home-minecraft.webp')}})">
<x-container class="text-white py-32">
<h2 class="text-3xl mb-4">Play Minecraft with us</h2>
<p class="mb-4">We invite you to join us on our <a href="https://stemcraft.com.au/" class="link">Minecraft server</a> where you can participate in weekly challenges and mini-games.</p>
<div class="mb-4 flex gap-4">
<img src="/home-minecraft-edu.webp" class="h-12" />
<img src="{{ asset('home-minecraft-edu.webp') }}" alt="Minecraft Education" class="h-12" />
<p>We also run workshops on our minecraft server, both online and offline, where you ca learn to make it rain rabbits, or grow flowers wherever you walk!</p>
</div>
<div class="flex justify-center">
<img src="/home-minecraft-address.webp" class="h-12" />
<img src="{{ asset('home-minecraft-address.webp') }}" alt="play.stemcraft.com.au" class="h-12" />
</div>
</x-container>
</section>
<section id="support">
<x-container class="bg-gray-200 py-32 -mb-12" inner-class="flex flex-row gap-16">
<div class="hidden sm:block flex-1">
<div class="h-full bg-no-repeat bg-center bg-cover rounded-lg" style="background-image:url('/home-discord.webp')"></div>
<div class="h-full bg-no-repeat bg-center bg-cover rounded-lg" style="background-image:url({{ asset('home-discord.webp') }})"></div>
</div>
<div class="flex-1 text-center">
<h2 class="text-3xl mb-4 text-left">And the support doesn't stop!</h2>
@@ -85,4 +85,13 @@
</div>
</x-container>
</section>
<section id="subscribe">
<x-container class="bg-primary-color-dark py-24 -mb-12" inner-class="flex justify-center">
<div class="max-w-[52rem]">
<h2 class="text-3xl mb-0 text-white">Want to know whats coming up?</h2>
<p class="mb-6 text-left text-white">Sign up and well send you updates on new workshops, special sessions and whats happening around STEMMechanics.</p>
<livewire:email-subscribe />
</div>
</x-container>
</section>
</x-layout>

View File

@@ -0,0 +1,39 @@
<div>
<form wire:submit.prevent="subscribe" class="flex flex-row justify-center">
<input
type="text"
name="name"
wire:model.defer="trap"
autocomplete="off"
tabindex="-1"
class="hidden"
/>
<x-ui.input
type="email"
name="email"
label="Email"
no-label
wire:model.defer="email"
class="m-0"
field-classes="rounded-r-none sm:w-96"
/>
{{-- Submit button --}}
<x-ui.button color="dark" type="submit" class="rounded-l-none">
Subscribe
</x-ui.button>
</form>
@if($message)
@if($success)
<p class="mt-4 text-sm text-green-600 mx-auto border-green-800 bg-green-100 py-1 px-4 w-fit">
<i class="fa fa-check mr-2"></i>{{ $message }}
</p>
@else
<p class="mt-4 text-sm text-red-600 mx-auto border-red-800 bg-red-100 py-1 px-4 w-fit">
<i class="fa fa-exclamation-triangle mr-2"></i>{{ $message }}
</p>
@endif
@endif
</div>

View File

@@ -2,15 +2,9 @@
<x-slot name="title">Blog</x-slot>
<x-mast>Blog</x-mast>
<section class="bg-gray-100">
<x-container class="pt-4">
<form method="GET" action="{{ request()->url() }}">
<x-ui.search name="search" label="Search" value="{{ request()->get('search') }}" />
</form>
</x-container>
@if($posts->isEmpty())
<x-container class="mt-8">
<x-none-found item="posts" search="{{ request()->get('search') }}" />
<x-none-found item="posts" />
</x-container>
@else
<x-container class="mt-4" inner-class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">

View File

@@ -0,0 +1,40 @@
<x-layout>
<x-mast title="Search" description='Results for "{{ $search }}"' />
<x-container>
{{-- <section class="bg-gray-100">--}}
{{-- <h2 class="text-2xl font-bold my-6">Posts</h2>--}}
{{-- @if($posts->isEmpty())--}}
{{-- <x-container class="mt-8">--}}
{{-- <x-none-found item="posts" search="{{ request()->get('search') }}" />--}}
{{-- </x-container>--}}
{{-- @else--}}
{{-- <x-container class="mt-4" inner-class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">--}}
{{-- @foreach ($posts as $post)--}}
{{-- <x-panel-post :post="$post" />--}}
{{-- @endforeach--}}
{{-- </x-container>--}}
{{-- <x-container>--}}
{{-- {{ $posts->appends(request()->except('post'))->links('', ['pageName' => 'post']) }}--}}
{{-- </x-container>--}}
{{-- @endif--}}
{{-- </section>--}}
<section class="bg-gray-100">
<h2 class="text-2xl font-bold my-6">Workshops</h2>
@if($workshops->isEmpty())
<x-container class="mt-8">
<x-none-found item="workshops" search="{{ request()->get('search') }}" />
</x-container>
@else
<x-container class="mt-4" inner-class="grid md:grid-cols-2 lg:grid-cols-3 gap-8 w-full">
@foreach ($workshops as $workshop)
<x-panel-workshop :workshop="$workshop" />
@endforeach
</x-container>
<x-container>
{{ $workshops->appends(request()->except('workshop'))->links('', ['pageName' => 'workshop']) }}
</x-container>
@endif
</section>
</x-container>
</x-layout>

View File

@@ -18,7 +18,7 @@
<x-slot:footer>
<x-mail::footer>
<p>This email was sent to <a href="mailto:{{ $email }}">{{ $email }}</a><br />
<a href="{{ route('index') }}">{{ config('app.name') }}</a> | 1/4 Jordan Street | Edmonton, QLD 4869 Australia<br />
<a href="{{ route('index') }}">{{ config('app.name') }}</a> | 63 Dalton Street | Westcourt, QLD 4870 Australia<br />
© {{ date('Y') }} {{ config('app.name') }}. {{ __('All rights reserved.') }}<br />
<a href="{{ route('privacy') }}">Privacy Policy</a> | <a href="{{ route('terms-conditions') }}">Terms & Conditions</a> @isset($unsubscribe) | <a href="{{ $unsubscribe }}">Unsubscribe</a>@endisset
</p>

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