This commit is contained in:
2023-04-19 22:31:47 +10:00
parent fb9944ef14
commit 5ae6e02ce8
32 changed files with 475 additions and 348 deletions

View File

@@ -43,6 +43,8 @@ class UserConductor extends Conductor
if ($user === null || ($user->hasPermission('admin/users') === false && strcasecmp($user->id, $model->id) !== 0)) {
$fields = ['id', 'username', 'display_name'];
$data = arrayLimitKeys($data, $fields);
} else {
$data['permissions'] = $user->permissions;
}
return $data;

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -1,56 +1,51 @@
<template>
<SMContainer class="sm-attachments">
<h3 v-if="props.attachments && props.attachments.length > 0">Files</h3>
<table class="sm-attachment-list">
<tbody>
<tr
v-for="file of props.attachments"
:key="file.id"
class="sm-attachment-row">
<td class="sm-attachment-file-icon">
<img
:src="getFileIconImagePath(file.title || file.name)"
height="48"
width="48" />
</td>
<td class="sm-attachment-file-name">
<a :href="file.url" target="_blank">{{
file.title || file.name
}}</a>
</td>
<td class="sm-attachment-download">
<a :href="file.url + '?download=1'"
><svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M12 10V20M12 20L9.5 17.5M12 20L14.5 17.5"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.3218 7.05726C7.12925 4.69709 9.36551 3 12 3C14.6345 3 16.8708 4.69709 17.6782 7.05726C19.5643 7.37938 21 9.02203 21 11C21 13.2091 19.2091 15 17 15H16C15.4477 15 15 14.5523 15 14C15 13.4477 15.4477 13 16 13H17C18.1046 13 19 12.1046 19 11C19 9.89543 18.1046 9 17 9C16.9776 9 16.9552 9.00037 16.9329 9.0011C16.4452 9.01702 16.0172 8.67854 15.9202 8.20023C15.5502 6.37422 13.9345 5 12 5C10.0655 5 8.44979 6.37422 8.07977 8.20023C7.98284 8.67854 7.55482 9.01702 7.06706 9.0011C7.04476 9.00037 7.02241 9 7 9C5.89543 9 5 9.89543 5 11C5 12.1046 5.89543 13 7 13H8C8.55228 13 9 13.4477 9 14C9 14.5523 8.55228 15 8 15H7C4.79086 15 3 13.2091 3 11C3 9.02203 4.43567 7.37938 6.3218 7.05726Z"
fill="currentColor" />
</svg>
</a>
</td>
<td class="sm-attachment-file-size">
({{ bytesReadable(file.size) }})
</td>
</tr>
</tbody>
</table>
</SMContainer>
<h3 v-if="props.attachments && props.attachments.length > 0">Files</h3>
<table class="attachment-list">
<tbody>
<tr
v-for="file of props.attachments"
:key="file.id"
class="attachment-row">
<td class="attachment-file-icon">
<img
:src="getFileIconImagePath(file.name || file.title)"
height="48"
width="48" />
</td>
<td class="attachment-file-name">
<a :href="file.url">{{ file.title || file.name }}</a>
</td>
<td class="attachment-download">
<a :href="file.url + '?download=1'"
><svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M12 10V20M12 20L9.5 17.5M12 20L14.5 17.5"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.3218 7.05726C7.12925 4.69709 9.36551 3 12 3C14.6345 3 16.8708 4.69709 17.6782 7.05726C19.5643 7.37938 21 9.02203 21 11C21 13.2091 19.2091 15 17 15H16C15.4477 15 15 14.5523 15 14C15 13.4477 15.4477 13 16 13H17C18.1046 13 19 12.1046 19 11C19 9.89543 18.1046 9 17 9C16.9776 9 16.9552 9.00037 16.9329 9.0011C16.4452 9.01702 16.0172 8.67854 15.9202 8.20023C15.5502 6.37422 13.9345 5 12 5C10.0655 5 8.44979 6.37422 8.07977 8.20023C7.98284 8.67854 7.55482 9.01702 7.06706 9.0011C7.04476 9.00037 7.02241 9 7 9C5.89543 9 5 9.89543 5 11C5 12.1046 5.89543 13 7 13H8C8.55228 13 9 13.4477 9 14C9 14.5523 8.55228 15 8 15H7C4.79086 15 3 13.2091 3 11C3 9.02203 4.43567 7.37938 6.3218 7.05726Z"
fill="currentColor" />
</svg>
</a>
</td>
<td class="attachment-file-size">
({{ bytesReadable(file.size) }})
</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
import { bytesReadable } from "../helpers/types";
import { getFileIconImagePath } from "../helpers/utils";
import SMContainer from "./SMContainer.vue";
const props = defineProps({
attachments: {
@@ -61,92 +56,84 @@ const props = defineProps({
</script>
<style lang="scss">
.sm-attachments {
display: block;
.attachment-list {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
max-width: 580px;
margin-top: 12px;
h3 {
margin-top: map-get($spacer, 4);
margin-bottom: map-get($spacer, 3);
}
.attachment-row {
td {
border-bottom: 1px solid $secondary-background-color;
padding: 8px 0;
}
.sm-attachment-list {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
max-width: 580px;
&:last-child td {
border-bottom: 0;
}
.sm-attachment-row {
td {
border-bottom: 1px solid $secondary-background-color;
padding: 8px 0;
.attachment-file-icon {
width: 64px;
img {
display: block;
}
}
&:last-child td {
border-bottom: 0;
}
.attachment-file-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sm-attachment-file-icon {
width: 64px;
.attachment-download {
width: 28px;
text-align: center;
img {
display: block;
}
}
.sm-attachment-file-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sm-attachment-download {
width: 28px;
text-align: center;
a {
display: block;
color: $secondary-color-dark;
transition: color 0.2s ease-in-out;
&:hover {
color: $primary-color-dark;
}
svg {
margin-top: 4px;
width: 24px;
height: 24px;
}
}
}
.sm-attachment-file-size {
width: 84px;
font-size: 75%;
a {
display: block;
color: $secondary-color-dark;
white-space: nowrap;
text-align: right;
transition: color 0.2s ease-in-out;
&:hover {
color: $primary-color-dark;
}
svg {
margin-top: 4px;
width: 24px;
height: 24px;
}
}
}
.attachment-file-size {
width: 64px;
font-size: 75%;
color: $secondary-color-dark;
white-space: nowrap;
text-align: right;
}
}
}
@media only screen and (max-width: 640px) {
.sm-attachments .sm-attachment-list {
.sm-attachment-file-icon img {
.attachment-list {
.attachment-file-icon img {
margin: 0 4px;
}
.sm-attachment-download a,
.sm-attachment-file-size {
.attachment-download a,
.attachment-file-size {
padding-left: 0.25rem;
}
}
}
@media only screen and (max-width: 440px) {
.sm-attachments .sm-attachment-list {
.sm-attachment-file-icon {
.attachment-list {
.attachment-file-icon {
display: none;
}
}

View File

@@ -42,7 +42,7 @@ const slots = useSlots();
flex-direction: column;
padding: 0 16px;
max-width: 1200px;
align-items: center;
// align-items: center;
margin: 0 auto;
&.full {

View File

@@ -30,7 +30,8 @@
v-model="value"
@focus="handleFocus"
@blur="handleBlur"
@input="handleInput" />
@input="handleInput"
@keyup="handleKeyup" />
</div>
<div v-if="slots.append" class="input-control-append">
<slot name="append"></slot>
@@ -50,7 +51,7 @@ import { inject, watch, ref, useSlots } from "vue";
import { isEmpty } from "../helpers/utils";
import { toTitleCase } from "../helpers/string";
const emits = defineEmits(["update:modelValue", "change"]);
const emits = defineEmits(["update:modelValue", "blur", "keyup"]);
const props = defineProps({
form: {
type: Object,
@@ -198,7 +199,7 @@ const handleFocus = () => {
const handleBlur = async () => {
active.value = value.value?.length ?? 0 > 0;
focused.value = false;
emits("change");
emits("blur");
if (control) {
await control.validate();
@@ -217,6 +218,10 @@ const handleInput = (event: Event) => {
}
};
const handleKeyup = (event: Event) => {
emits("keyup", event);
};
const handleClear = () => {
value.value = "";
emits("update:modelValue", "");
@@ -357,7 +362,7 @@ const handleClear = () => {
&.input-active {
.input-control-item .input-label {
transform: translate(16px, 6px) scale(0.8);
transform: translate(16px, 6px) scale(0.7);
}
}

View File

@@ -10,6 +10,7 @@
><ion-icon name="chevron-back-outline"></ion-icon>
{{ props.backTitle }}</router-link
>
<p class="info" v-if="slots.default"><slot></slot></p>
</div>
<div v-if="tabs().length > 0" class="tabs">
<router-link
@@ -26,6 +27,7 @@
</template>
<script setup lang="ts">
import { useSlots } from "vue";
import { useRoute } from "vue-router";
const props = defineProps({
@@ -47,6 +49,8 @@ const props = defineProps({
},
});
const slots = useSlots();
const tabGroups = [
[
{ title: "Contact", to: "/contact" },
@@ -107,6 +111,17 @@ const tabs = () => {
transition: margin 0.1s linear;
}
}
.info {
margin-top: -24px;
color: rgb(255, 255, 255, 0.74);
max-width: 500px;
a {
color: rgb(255, 255, 255);
text-decoration: none;
}
}
}
.tabs {

View File

@@ -76,7 +76,7 @@ const menuItems = [
{
name: "community",
label: "Community",
to: { name: "blog" },
to: { name: "community" },
},
{
name: "contact",
@@ -87,7 +87,6 @@ const menuItems = [
name: "register",
label: "Register",
to: { name: "register" },
icon: "person-add-outline",
show: () => !userStore.id,
},
{
@@ -100,14 +99,12 @@ const menuItems = [
name: "dashboard",
label: "Dashboard",
to: { name: "dashboard" },
icon: "grid-outline",
show: () => userStore.id,
},
{
name: "logout",
label: "Log out",
to: { name: "logout" },
icon: "log-out-outline",
show: () => userStore.id,
},
];

View File

@@ -32,7 +32,7 @@ import SMButton from "../SMButton.vue";
import SMFormCard from "../SMFormCard.vue";
import SMForm from "../SMForm.vue";
import SMFormFooter from "../SMFormFooter.vue";
import SMInput from "../../depreciated/SMInput-old.vue";
import SMInput from "../SMInput.vue";
const form: FormObject = reactive(
Form({

View File

@@ -19,6 +19,20 @@ export const isEmpty = (objOrString: unknown): boolean => {
return false;
};
/**
* Returns the file extension
*
* @param {string} fileName The filename with extension.
* @returns {string} The file extension.
*/
export const getFileExtension = (fileName: string): string => {
if (fileName.includes(".")) {
return fileName.split(".").pop();
}
return "";
};
/**
* Returns a url to a file type icon based on file name.
*
@@ -26,8 +40,12 @@ export const isEmpty = (objOrString: unknown): boolean => {
* @returns {string} The url to the file type icon.
*/
export const getFileIconImagePath = (fileName: string): string => {
const ext = fileName.split(".").pop();
return `/img/fileicons/${ext}.png`;
const ext = getFileExtension(fileName);
if (ext.length > 0) {
return `/img/fileicons/${ext}.png`;
}
return "/img/fileicons/unknown.png";
};
/**
@@ -37,8 +55,8 @@ export const getFileIconImagePath = (fileName: string): string => {
* @returns {string} The url to the file preview icon.
*/
export const getFilePreview = (url: string): string => {
const ext = url.split(".").pop();
if (ext) {
const ext = getFileExtension(fileName);
if (ext.length > 0) {
if (/(gif|jpe?g|png)/i.test(ext)) {
return `${url}?size=thumb`;
}

View File

@@ -94,12 +94,12 @@ export const routes = [
component: () => import("@/views/Rules.vue"),
},
{
path: "/unsubscribe",
name: "unsubscribe",
path: "/community",
name: "community",
meta: {
title: "Unsubscribe",
title: "Community",
},
component: () => import("@/views/Unsubscribe.vue"),
component: () => import("@/views/Community.vue"),
},
{
path: "/minecraft",
@@ -344,24 +344,6 @@ export const routes = [
},
component: () => import("@/views/ForgotPassword.vue"),
},
{
path: "/error/internal",
name: "error-internal",
meta: {
title: "Server error",
hideInEditor: true,
},
component: () => import("@/components/errors/Internal.vue"),
},
{
path: "/error/forbidden",
name: "forbidden",
meta: {
title: "Forbidden",
hideInEditor: true,
},
component: () => import("@/components/errors/Forbidden.vue"),
},
{
path: "/:catchAll(.*)",
name: "not-found",
@@ -369,7 +351,7 @@ export const routes = [
title: "Page not found",
hideInEditor: true,
},
component: () => import("@/components/errors/NotFound.vue"),
component: () => import("@/views/404.vue"),
},
];
@@ -392,26 +374,14 @@ router.beforeEach(async (to, from, next) => {
// Check Token
if (userStore.id) {
let redirect = false;
try {
let res = await api.get("/me");
userStore.setUserDetails(res.json.user);
} catch (err) {
if (err.status == 401) {
userStore.clearUser();
redirect = true;
}
}
if (
redirect &&
to.path.startsWith("/error/") === false &&
to.path.startsWith("/login") === false
) {
next({ name: "login", query: { redirect: to.fullPath } });
return;
}
}
const getMetaValue = (tag, defaultValue = "") => {

View File

@@ -5,6 +5,7 @@ export interface UserDetails {
username: string;
first_name: string;
last_name: string;
display_name: string;
email: string;
phone: string;
permissions: string[];
@@ -16,6 +17,7 @@ export interface UserState {
username: string;
firstName: string;
lastName: string;
displayName: string;
email: string;
phone: string;
permissions: string[];
@@ -29,6 +31,7 @@ export const useUserStore = defineStore({
username: "",
firstName: "",
lastName: "",
displayName: "",
email: "",
phone: "",
permissions: [],
@@ -40,6 +43,7 @@ export const useUserStore = defineStore({
this.$state.username = user.username;
this.$state.firstName = user.first_name;
this.$state.lastName = user.last_name;
this.$state.displayName = user.display_name;
this.$state.email = user.email;
this.$state.phone = user.phone;
this.$state.permissions = user.permissions || [];
@@ -55,9 +59,13 @@ export const useUserStore = defineStore({
this.$state.username = null;
this.$state.firstName = null;
this.$state.lastName = null;
this.$state.displayName = null;
this.$state.email = null;
this.$state.phone = null;
this.$state.permissions = [];
this.$reset();
localStorage.removeItem(this.$id);
},
},

View File

@@ -0,0 +1,5 @@
<template>
<SMPage :page-error="404" />
</template>
<script setup lang="ts"></script>

View File

@@ -5,7 +5,8 @@
type="text"
label="Search articles"
v-model="searchInput"
show-clear>
@keyup.enter="handleClickSearch"
@blur="handleClickSearch">
<template #append
><SMButton
type="primary"
@@ -14,9 +15,8 @@
@click="handleClickSearch"
/></template>
</SMInput>
<template v-if="pageLoading">
<SMLoading large />
</template>
<SMLoading v-if="pageLoading" large />
<SMNoItems v-else-if="posts.length == 0" text="No Articles Found" />
<template v-else>
<SMPagination
v-if="postsTotal > postsPerPage"
@@ -63,6 +63,7 @@ import SMInput from "../components/SMInput.vue";
import SMButton from "../components/SMButton.vue";
import { excerpt } from "../helpers/string";
import SMLoading from "../components/SMLoading.vue";
import SMNoItems from "../components/SMNoItems.vue";
const message = ref("");
const pageLoading = ref(true);
@@ -75,7 +76,8 @@ let postsTotal = ref(0);
let searchInput = ref("");
const handleClickSearch = () => {
alert(searchInput.value);
postsPage.value = 1;
handleLoad();
};
/**
@@ -84,13 +86,22 @@ const handleClickSearch = () => {
const handleLoad = () => {
message.value = "";
pageLoading.value = true;
posts.value = [];
let params = {
limit: postsPerPage,
page: postsPage.value,
};
if (searchInput.value.length > 0) {
params[
"filter"
] = `(title:${searchInput.value},OR,content:${searchInput.value})`;
}
api.get({
url: "/posts",
params: {
limit: postsPerPage,
page: postsPage.value,
},
params: params,
})
.then((result) => {
const data = result.data as PostCollection;

View File

@@ -0,0 +1,131 @@
<template>
<SMMastHead title="Community"
>STEMMechanics has an active community across multiple channels. By
joining our communities, you agree to follow the
<router-link :to="{ name: 'code-of-conduct' }"
>Code of Conduct</router-link
>.</SMMastHead
>
<SMContainer>
<div class="communities">
<a
:href="community.url"
class="community-card"
v-for="(community, index) in communities"
:key="index">
<div
class="thumbnail"
:style="{
backgroundImage: `url(${community.thumbnail})`,
}"></div>
<h3 class="title">{{ community.title }}</h3>
<p class="content">
{{ community.content }}
</p>
</a>
</div>
</SMContainer>
</template>
<script setup lang="ts">
import SMMastHead from "../components/SMMastHead.vue";
const communities = [
{
thumbnail: "/img/community-discord.png",
url: "https://someone.com/",
title: "Discord",
content:
"A vibrant community for discussion, user support, showcases... and custom emoji!",
},
{
thumbnail: "/img/community-discord.png",
url: "/minecraft",
title: "Minecraft",
content:
"Our usual hang-out to kill zombies and build redstone contraptions.",
},
{
thumbnail: "/img/community-discord.png",
url: "https://github.com/stemmechanics",
title: "GitHub",
content: "All our open-source projects. Send bug reports here.",
},
{
thumbnail: "/img/community-discord.png",
url: "https://youtube.com/stemmechanics",
title: "YouTube",
content: "Channel for official STEMMechanics videos.",
},
{
thumbnail: "/img/community-discord.png",
url: "https://facebook.com/stemmechanics",
title: "Facebook",
content: "Community for discussions and showcasing workshops.",
},
{
thumbnail: "/img/community-discord.png",
url: "https://facebook.com/stemmechanics",
title: "Mastodon",
content: "Connect with us in the Fediverse.",
},
];
</script>
<style lang="scss">
.page-community {
.communities {
display: grid;
grid-template-columns: 1fr;
gap: 30px;
.community-card {
text-decoration: none;
color: var(--base-color-text);
background-color: var(--base-color-light);
box-shadow: var(--base-shadow);
&:hover {
filter: none;
.thumbnail {
filter: brightness(115%);
}
}
.thumbnail {
aspect-ratio: 16 / 9;
border-radius: 7px;
background-position: center;
background-size: cover;
background-color: var(--card-background-color);
margin-bottom: 24px;
}
.title {
margin: 0;
padding: 0 16px;
word-break: break-word;
color: var(--primary-color);
}
.content {
font-size: 90%;
padding: 0 16px 16px 16px;
}
}
}
}
@media (min-width: 640px) {
.page-community .communities {
grid-template-columns: 1fr 1fr;
}
}
@media (min-width: 768px) {
.page-community .communities {
grid-template-columns: 1fr 1fr 1fr;
}
}
</style>

View File

@@ -47,7 +47,7 @@ import SMButton from "../components/SMButton.vue";
import SMFormCard from "../components/SMFormCard.vue";
import SMForm from "../components/SMForm.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../depreciated/SMInput-old.vue";
import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import { And, Max, Min, Required } from "../helpers/validate";

View File

@@ -53,7 +53,7 @@ import SMButton from "../components/SMButton.vue";
import SMFormCard from "../components/SMFormCard.vue";
import SMForm from "../components/SMForm.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../depreciated/SMInput-old.vue";
import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import { And, Min, Required } from "../helpers/validate";

View File

@@ -52,7 +52,7 @@ import SMButton from "../components/SMButton.vue";
import SMFormCard from "../components/SMFormCard.vue";
import SMForm from "../components/SMForm.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../depreciated/SMInput-old.vue";
import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import { And, Email, Required } from "../helpers/validate";

View File

@@ -1,7 +1,7 @@
<template>
<SMHero class="hero-offset" />
<SMContainer class="about">
<SMContainer class="about align-items-center">
<template #inner>
<h2>Join the Fun!</h2>
<p></p>
@@ -23,7 +23,7 @@
</p>
</template>
</SMContainer>
<SMContainer class="workshops">
<SMContainer class="workshops align-items-center">
<template #inner>
<SMRow>
<SMColumn class="align-items-center flex-basis-55">
@@ -49,9 +49,11 @@
<SMContainer>
<h2>Play Minecraft with us</h2>
<p>
We invite you to join us on our Minecraft servers, supporting
both Bedrock and Java clients, where you can participate in
weekly challenges and mini-games.
We invite you to join us on our
<router-link :to="{ name: 'minecraft' }"
>Minecraft server</router-link
>
where you can participate in weekly challenges and mini-games.
</p>
<p class="minecraft-education">
<img
@@ -75,7 +77,7 @@
</p>
</SMContainer>
</SMContainer>
<SMContainer class="support">
<SMContainer class="support align-items-center">
<template #inner>
<h2>And the support doesn't stop!</h2>
<SMRow>

View File

@@ -1,12 +1,10 @@
<template>
<SMPage>
<SMLoader :loading="true" />
</SMPage>
<SMLoading large />
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
import SMLoader from "../components/SMLoader.vue";
import SMLoading from "../components/SMLoading.vue";
import { api } from "../helpers/api";
import { useToastStore } from "../store/ToastStore";
import { useUserStore } from "../store/UserStore";

View File

@@ -1,61 +1,102 @@
<template>
<SMPage class="sm-page-minecraft">
<template #container>
<h1>Connecting to our Minecraft Server</h1>
<ol>
<li>
Open up your Minecraft on your computer and make sure you
are using version 1.19.3
</li>
<li>Click Multiplayer</li>
<li>Click Add Server</li>
<li>Enter Server Name STEMMechanics</li>
<li>Enter Server Address mc.stemmech.com.au</li>
<li>
We have a custom resourcepack which you can enable before
joining
</li>
<li>Click Done</li>
<li>Join the Server!</li>
</ol>
<h2>Java or Bedrock</h2>
<p>
Regrettably, our support is exclusively for the Java edition of
Minecraft. The reason for this is that we rely on the features
unique to the Java edition, such as Resource Packs, Custom
Items, and Modelling, which are not available in Bedrock
(Tablet) edition.
</p>
<p>
It's worth noting that if you have the Bedrock version, you may
be eligible to acquire the Java version for free from
<a href="https://minecraft.net">Minecraft.net</a>. However,
please keep in mind that the Java version is solely compatible
with Windows or Mac operating systems.
</p>
<h2>Goodbye Drustcraft</h2>
<p>
STEMMechanics launched the Drustcraft server three years ago and
since then, players have had countless enjoyable experiences.
Cities were built, bosses defeated, and most importantly, a
tight-knit community formed.
</p>
<p>
Maintaining the server design became overwhelming and took away
the fun of playing Minecraft. Hence, in January, the decision
was made to shut down Drustcraft and offer a more
straightforward Minecraft server, retaining the beloved elements
of Drustcraft like mini-games, bosses, and survival. Join us on
the new STEMMechanics Minecraft server, where the Drustcraft
community awaits.
</p>
</template>
</SMPage>
<SMMastHead title="Minecraft Server" />
<SMContainer narrow>
<h3>Connecting to our Minecraft Server</h3>
<ol>
<li>
Open up your Minecraft on your computer and make sure you are
using version 1.19.3
</li>
<li>Click Multiplayer</li>
<li>Click Add Server</li>
<li>Enter Server Name STEMMechanics</li>
<li>Enter Server Address mc.stemmech.com.au</li>
<li>
We have a custom resourcepack which you can enable before
joining
</li>
<li>Click Done</li>
<li>Join the Server!</li>
</ol>
<h3>Java or Bedrock</h3>
<p>
Regrettably, our support is exclusively for the Java edition of
Minecraft. The reason for this is that we rely on the features
unique to the Java edition, such as Resource Packs, Custom Items,
and Modelling, which are not available in Bedrock (Tablet) edition.
</p>
<p>
It's worth noting that if you have the Bedrock version, you may be
eligible to acquire the Java version for free from
<a href="https://minecraft.net">Minecraft.net</a>. However, please
keep in mind that the Java version is solely compatible with Windows
or Mac operating systems.
</p>
<h3>Goodbye Drustcraft</h3>
<p>
STEMMechanics launched the Drustcraft server three years ago and
since then, players have had countless enjoyable experiences. Cities
were built, bosses defeated, and most importantly, a tight-knit
community formed.
</p>
<p>
Maintaining the server design became overwhelming and took away the
fun of playing Minecraft. Hence, in January, the decision was made
to shut down Drustcraft and offer a more straightforward Minecraft
server, retaining the beloved elements of Drustcraft like
mini-games, bosses, and survival. Join us on the new STEMMechanics
Minecraft server, where the Drustcraft community awaits.
</p>
<h3>So long Cairns Minecraft</h3>
<p>
After seven incredible years of operation, the Cairns Minecraft
server officially closed its virtual doors in May 2022. This
close-knit online community, which brought together gamers from
around the region, renowned for its fantastic builds, lively
competitions, and unique events. Throughout its existence, players
forged genuine friendships, collaborated on awe-inspiring projects,
and pushed the boundaries of creativity in the world of Minecraft.
Although the server's closure marked the end of an era, the
cherished memories and invaluable experiences shared by its members
will forever remain etched in the hearts of the Cairns Minecraft
community.
</p>
<SMAttachments :attachments="downloads" />
</SMContainer>
</template>
<script setup lang="ts">
import SMAttachments from "../components/SMAttachments.vue";
import SMMastHead from "../components/SMMastHead.vue";
const downloads = [
{
id: "1",
title: "Cairns Minecraft - Survival",
name: "2103-cm-survival.zip",
url: "https://cdn.stemmechanics.com.au/2103-cm-survival.zip",
size: 6098565968,
},
{
id: "2",
title: "Cairns Minecraft - Creative",
name: "2205-cm-creative-complete.zip",
url: "https://cdn.stemmechanics.com.au/2205-cm-creative-complete.zip",
size: 6712439230,
},
{
id: "3",
title: "Cairns Minecraft - Creative (Worlds Only)",
name: "2205-cm-creative.zip",
url: "https://cdn.stemmechanics.com.au/2205-cm-creative.zip",
size: 3585899092,
},
];
</script>
<style lang="scss">
.sm-page-minecraft {
h2 {
h3 {
margin-bottom: 0.5rem;
}
li {

View File

@@ -49,7 +49,7 @@ import SMButton from "../components/SMButton.vue";
import SMFormCard from "../components/SMFormCard.vue";
import SMForm from "../components/SMForm.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../depreciated/SMInput-old.vue";
import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import { Required } from "../helpers/validate";

View File

@@ -49,7 +49,7 @@ import SMButton from "../components/SMButton.vue";
import SMFormCard from "../components/SMFormCard.vue";
import SMForm from "../components/SMForm.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../depreciated/SMInput-old.vue";
import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import { And, Max, Min, Password, Required } from "../helpers/validate";

View File

@@ -1,90 +0,0 @@
<template>
<SMPage>
<SMRow>
<SMFormCard narrow>
<template v-if="!formDone">
<h1>Unsubscribe</h1>
<p>
If you would like to unsubscribe from our mailing list,
you have come to the right page!
</p>
<SMForm v-model="form" @submit="handleSubmit">
<SMInput control="email" />
<SMFormFooter>
<template #right>
<SMButton type="submit" label="Unsubscribe" />
</template>
</SMFormFooter>
</SMForm>
</template>
<template v-else>
<h1>Unsubscribed</h1>
<p class="text-center">
You have now been unsubscribed from our newsletter.
</p>
<SMRow class="pb-2">
<SMColumn class="justify-content-center">
<SMButton :to="{ name: 'home' }" label="Home" />
</SMColumn>
</SMRow>
</template>
</SMFormCard>
</SMRow>
</SMPage>
</template>
<script setup lang="ts">
import { reactive, ref } from "vue";
import { useReCaptcha } from "vue-recaptcha-v3";
import { useRoute } from "vue-router";
import SMButton from "../components/SMButton.vue";
import SMFormCard from "../components/SMFormCard.vue";
import SMForm from "../components/SMForm.vue";
import SMFormFooter from "../components/SMFormFooter.vue";
import SMInput from "../depreciated/SMInput-old.vue";
import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import { And, Email, Required } from "../helpers/validate";
const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const formDone = ref(false);
let form = reactive(
Form({
email: FormControl("", And([Required(), Email()])),
})
);
const handleSubmit = async () => {
form.loading(true);
try {
await recaptchaLoaded();
const captcha = await executeRecaptcha("submit");
await api.delete({
url: "/subscriptions",
body: {
email: form.controls.email.value,
captcha_token: captcha,
},
});
formDone.value = true;
} catch (error) {
form.apiErrors(error);
} finally {
form.loading(false);
}
};
if (useRoute().query.email !== undefined) {
let queryEmail = useRoute().query.email;
if (Array.isArray(queryEmail)) {
queryEmail = queryEmail[0];
}
form.controls.email.value = queryEmail;
handleSubmit();
}
</script>

View File

@@ -5,20 +5,17 @@
<SMInput
v-model="filterKeywords"
label="Keywords"
:show-clear="true"
@change="handleFilter" />
@blur="handleFilter" />
<SMInput
v-model="filterLocation"
label="Location"
:show-clear="true"
@change="handleFilter" />
@blur="handleFilter" />
<SMInput
v-model="filterDateRange"
type="daterange"
label="Date Range"
:feedback-invalid="dateRangeError"
:show-clear="true"
@change="handleFilter" />
@blur="handleFilter" />
</SMToolbar>
<SMPagination
v-if="postsTotal > postsPerPage"
@@ -32,10 +29,8 @@
:message="formMessage"
class="mt-5" />
<template v-if="pageLoading">
<SMLoading large />
</template>
<SMNoItems v-else-if="postsTotal == 0" />
<SMLoading v-if="pageLoading" large />
<SMNoItems v-else-if="events.length == 0" text="No Workshops Found" />
<div v-else class="events">
<router-link
class="event-card"
@@ -97,12 +92,6 @@ import SMContainer from "../components/SMContainer.vue";
import SMNoItems from "../components/SMNoItems.vue";
import SMLoading from "../components/SMLoading.vue";
interface EventData {
event: Event;
banner: string;
bannerType: string;
}
const pageLoading = ref(true);
let events: Event[] = reactive([]);
const dateRangeError = ref("");
@@ -253,6 +242,7 @@ const handleLoad = async () => {
};
const handleFilter = async () => {
postsPage.value = 1;
handleLoad();
};

View File

@@ -23,7 +23,7 @@ import { reactive } from "vue";
import { useRoute } from "vue-router";
import SMButton from "../../components/SMButton.vue";
import SMForm from "../../components/SMForm.vue";
import SMInput from "../../depreciated/SMInput-old.vue";
import SMInput from "../../components/SMInput.vue";
import { api } from "../../helpers/api";
import { Form, FormControl } from "../../helpers/form";

View File

@@ -132,7 +132,7 @@ import { useRoute, useRouter } from "vue-router";
import SMButton from "../../components/SMButton.vue";
import SMEditor from "../../components/SMEditor.vue";
import SMFormFooter from "../../components/SMFormFooter.vue";
import SMInput from "../../depreciated/SMInput-old.vue";
import SMInput from "../../components/SMInput.vue";
import { api } from "../../helpers/api";
import { SMDate } from "../../helpers/datetime";
import { Form, FormControl } from "../../helpers/form";

View File

@@ -70,7 +70,7 @@ import SMHeading from "../../components/SMHeading.vue";
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
import SMMessage from "../../components/SMMessage.vue";
import SMToolbar from "../../components/SMToolbar.vue";
import SMInput from "../../depreciated/SMInput-old.vue";
import SMInput from "../../components/SMInput.vue";
import { api } from "../../helpers/api";
import { SMDate } from "../../helpers/datetime";
import { debounce } from "../../helpers/debounce";

View File

@@ -67,7 +67,7 @@ import { useRoute, useRouter } from "vue-router";
import SMButton from "../../components/SMButton.vue";
import SMFormCard from "../../components/SMFormCard.vue";
import SMForm from "../../components/SMForm.vue";
import SMInput from "../../depreciated/SMInput-old.vue";
import SMInput from "../../components/SMInput.vue";
import { api } from "../../helpers/api";
import { Form, FormControl } from "../../helpers/form";

View File

@@ -71,7 +71,7 @@ import SMButton from "../../components/SMButton.vue";
import SMEditor from "../../components/SMEditor.vue";
import SMForm from "../../components/SMForm.vue";
import SMFormFooter from "../../components/SMFormFooter.vue";
import SMInput from "../../depreciated/SMInput-old.vue";
import SMInput from "../../components/SMInput.vue";
import SMInputAttachments from "../../components/SMInputAttachments.vue";
import { api } from "../../helpers/api";
import { PostResponse, UserCollection } from "../../helpers/api.types";

View File

@@ -63,7 +63,7 @@ import { openDialog } from "../../components/SMDialog";
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import SMButton from "../../components/SMButton.vue";
import SMHeading from "../../components/SMHeading.vue";
import SMInput from "../../depreciated/SMInput-old.vue";
import SMInput from "../../components/SMInput.vue";
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
import SMMessage from "../../components/SMMessage.vue";
import SMToolbar from "../../components/SMToolbar.vue";

View File

@@ -85,8 +85,10 @@ const loadData = async () => {
const data = result.data as UserResponse;
if (data && data.user) {
form.controls.username.value = data.user.username;
form.controls.first_name.value = data.user.first_name;
form.controls.last_name.value = data.user.last_name;
form.controls.display_name.value = data.user.display_name;
form.controls.phone.value = data.user.phone;
form.controls.email.value = data.user.email;
}
@@ -99,6 +101,7 @@ const loadData = async () => {
form.controls.username.value = userStore.username;
form.controls.first_name.value = userStore.firstName;
form.controls.last_name.value = userStore.lastName;
form.controls.display_name.value = userStore.displayName;
form.controls.phone.value = userStore.phone;
form.controls.email.value = userStore.email;
}
@@ -118,6 +121,7 @@ const handleSubmit = async () => {
body: {
first_name: form.controls.first_name.value,
last_name: form.controls.last_name.value,
display_name: form.controls.display_name.value,
email: form.controls.email.value,
phone: form.controls.phone.value,
},