lots o updates

This commit is contained in:
2023-04-18 21:47:44 +10:00
parent b53fca9648
commit 36c71da4bb
29 changed files with 656 additions and 547 deletions

View File

@@ -0,0 +1,35 @@
<?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.
*
* @return void
*/
public function up()
{
DB::table('users')->whereNull('phone')->update(['phone' => '']);
Schema::table('users', function (Blueprint $table) {
$table->string('phone')->default("")->nullable(false)->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->string('phone')->nullable(true)->change();
});
}
};

View File

@@ -0,0 +1,40 @@
<?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.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('display_name')->default("");
});
// Update existing rows with display_name
DB::table('users')->select('id', 'username')->orderBy('id')->chunk(100, function ($users) {
foreach ($users as $user) {
DB::table('users')->where('id', $user->id)->update(['display_name' => $user->username]);
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('display_name');
});
}
};

View File

@@ -79,6 +79,13 @@ a:visited {
} }
} }
p,
li,
.html {
text-rendering: optimizeLegibility;
line-height: 1.5;
}
p { p {
margin: 1rem 0; margin: 1rem 0;
} }
@@ -91,30 +98,6 @@ li {
} }
} }
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 16px;
}
.btn {
cursor: pointer;
position: relative;
font-family: var(--header-font-family);
font-weight: 800;
padding: 16px 32px 16px 32px;
border: 0;
background-color: var(--base-color-light);
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
color: var(--link-color);
&:hover {
filter: brightness(85%);
}
}
// Who knows why ion-icon randomally sets this to hidden..... // Who knows why ion-icon randomally sets this to hidden.....
// ion-icon { // ion-icon {
// visibility: visible; // visibility: visible;
@@ -190,23 +173,23 @@ li {
// } // }
// } // }
// /* SM Dialog */ /* SM Dialog */
// .sm-dialog-outer { .dialog-outer {
// position: fixed; position: fixed;
// display: flex; display: flex;
// top: 0; top: 0;
// left: 0; left: 0;
// bottom: 0; bottom: 0;
// right: 0; right: 0;
// flex-direction: column; flex-direction: column;
// align-items: center; align-items: center;
// justify-content: center; justify-content: center;
// z-index: 1000; z-index: 1000;
// padding: 1rem; padding: 1rem;
// } }
// .sm-dialog-outer:last-of-type { .dialog-outer:last-of-type {
// background-color: rgba(0, 0, 0, 0.4); background-color: rgba(0, 0, 0, 0.4);
// backdrop-filter: blur(2px); backdrop-filter: blur(2px);
// -webkit-backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);
// } }

View File

@@ -3,12 +3,14 @@
:root { :root {
// yes // yes
--default-font-size: 18px; --default-font-size: 18px;
--default-font-family: "Nunito", "Nunito override", "Arial", "Helvetica", --default-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif; Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
// --default-font-family: "Nunito", "Nunito override", "Arial", "Helvetica",
// sans-serif;
// yes // yes
--base-color: #eee; --base-color: #eee;
--base-color-text: rgba(0, 0, 0, 0.8); --base-color-text: #456;
--base-color-border: #999; --base-color-border: #999;
--base-color-light: #fff; --base-color-light: #fff;
@@ -25,6 +27,7 @@
// yes // yes
--primary-color: #35a5f1; --primary-color: #35a5f1;
--primary-color-hover: #f1fdff;
--primary-color-dark: #0e80ce; --primary-color-dark: #0e80ce;
--primary-color-darker: #095589; --primary-color-darker: #095589;

View File

@@ -1,59 +0,0 @@
<template>
<router-link :to="props.to" :class="['sm-article-card', `${props.type}`]">
<div
class="thumbnail"
:style="[{ backgroundImage: `url('${props.image}')` }]"></div>
<div class="content">
<h3>{{ props.title }}</h3>
<p class="excerpt">{{ props.excerpt }}</p>
</div>
</router-link>
</template>
<script setup lang="ts">
const props = defineProps({
to: {
type: Object,
required: true,
},
image: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
type: {
type: String,
default: "",
required: false,
},
excerpt: {
type: String,
required: true,
},
});
</script>
<style lang="scss">
.sm-article-card {
display: flex;
height: 100%;
flex-direction: column;
gap: 20px;
.thumbnail {
aspect-ratio: 16 / 9;
border-radius: 7px;
background-position: center;
background-size: cover;
background-color: var(--card-background-color);
box-shadow: var(--box-shadow);
}
&.row {
flex-direction: row;
}
}
</style>

View File

@@ -184,7 +184,7 @@ const handleClickItem = (item: string) => {
display: inline-block; display: inline-block;
font-family: var(--header-font-family); font-family: var(--header-font-family);
font-weight: 800; font-weight: 800;
padding: 16px 32px 16px 32px; padding: 12px 32px 12px 32px;
border: 0; border: 0;
background-color: var(--base-color-light); background-color: var(--base-color-light);
text-decoration: none; text-decoration: none;
@@ -195,7 +195,11 @@ const handleClickItem = (item: string) => {
user-select: none; user-select: none;
.button-label { .button-label {
display: inline-block;
padding: 2px 0 3px 0;
ion-icon { ion-icon {
display: inline-block;
width: 28px; width: 28px;
height: 28px; height: 28px;
margin: -8px 0; margin: -8px 0;
@@ -240,7 +244,7 @@ const handleClickItem = (item: string) => {
background-color: #fff; background-color: #fff;
&:hover { &:hover {
background-color: rgba(0, 0, 255, 0.5); background-color: var(--primary-color-hover);
} }
} }
} }

View File

@@ -1,6 +1,6 @@
<template> <template>
<div <div
:class="['sm-column', { 'flex-fill': fill && width == '' }]" :class="['column', { 'flex-fill': fill && width == '' }]"
:style="styles"> :style="styles">
<slot></slot> <slot></slot>
</div> </div>
@@ -28,13 +28,13 @@ if (props.width != "") {
</script> </script>
<style lang="scss"> <style lang="scss">
.sm-column { .column {
display: flex; display: flex;
margin: map-get($spacer, 2); margin: 0 12px;
flex-direction: column; flex-direction: column;
} }
.sm-row .sm-row .sm-column { .row .row .column {
&:first-of-type { &:first-of-type {
margin-left: 0; margin-left: 0;
} }

View File

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

View File

@@ -19,8 +19,8 @@ const dialogRefs = shallowReactive<DialogInstance[]>([]);
export default defineComponent({ export default defineComponent({
name: "SMDialogList", name: "SMDialogList",
template: ` template: `
<div class="sm-dialog-list"> <div class="dialog-list">
<div v-for="(dialogRef, index) in dialogRefList" :key="index" class="sm-dialog-outer"> <div v-for="(dialogRef, index) in dialogRefList" :key="index" class="dialog-outer">
<component <component
:is="dialogRef.dialog" :is="dialogRef.dialog"
v-if="dialogRef && dialogRef.wrapper === name" v-if="dialogRef && dialogRef.wrapper === name"

View File

@@ -90,7 +90,12 @@
> >
</li> </li>
<li> <li>
<router-link :to="{ name: 'terms' }" <router-link :to="{ name: 'code-of-conduct' }"
>Code of Conduct</router-link
>
</li>
<li>
<router-link :to="{ name: 'terms-and-conditions' }"
>Terms &amp; Conditions</router-link >Terms &amp; Conditions</router-link
> >
</li> </li>
@@ -99,9 +104,6 @@
>Privacy Policy</router-link >Privacy Policy</router-link
> >
</li> </li>
<li>
<router-link :to="{ name: 'rules' }">Rules</router-link>
</li>
</ul> </ul>
</SMColumn> </SMColumn>
</SMRow> </SMRow>

View File

@@ -65,7 +65,7 @@ const computedContent = computed(() => {
); );
return { return {
template: `<div class="sm-content">${html}</div>`, template: `<div class="html">${html}</div>`,
components: { components: {
SMImageGallery, SMImageGallery,
}, },

View File

@@ -17,7 +17,7 @@
name="alert-circle-outline"></ion-icon> name="alert-circle-outline"></ion-icon>
<ion-icon <ion-icon
v-if=" v-if="
props.showClear && value.length > 0 && !feedbackInvalid props.showClear && value?.length > 0 && !feedbackInvalid
" "
class="clear-icon" class="clear-icon"
name="close-outline" name="close-outline"
@@ -96,6 +96,11 @@ const props = defineProps({
default: false, default: false,
required: false, required: false,
}, },
feedbackInvalid: {
type: String,
default: "",
required: false,
},
}); });
const slots = useSlots(); const slots = useSlots();
@@ -132,14 +137,22 @@ const id = ref(
? props.control ? props.control
: "" : ""
); );
const feedbackInvalid = ref(""); const feedbackInvalid = ref(props.feedbackInvalid);
const active = ref(value.value.length > 0); const active = ref(value.value?.length ?? 0 > 0);
const focused = ref(false);
const disabled = ref(props.disabled); const disabled = ref(props.disabled);
watch( watch(
() => value.value, () => value.value,
(newValue) => { (newValue) => {
active.value = newValue.length > 0; active.value = newValue.length > 0 || focused.value == true;
}
);
watch(
() => props.feedbackInvalid,
(newValue) => {
feedbackInvalid.value = newValue;
} }
); );
@@ -174,10 +187,13 @@ if (form) {
const handleFocus = () => { const handleFocus = () => {
active.value = true; active.value = true;
focused.value = true;
}; };
const handleBlur = async () => { const handleBlur = async () => {
active.value = value.value != ""; active.value = value.value?.length ?? 0 > 0;
focused.value = false;
emits("change");
if (control) { if (control) {
await control.validate(); await control.validate();
@@ -295,7 +311,7 @@ const handleClear = () => {
background-color: #ccc; background-color: #ccc;
border-radius: 50%; border-radius: 50%;
font-size: 80%; font-size: 80%;
padding: 1px; padding: 1px 1px 1px 0px;
&:hover { &:hover {
color: #fff; color: #fff;
@@ -310,13 +326,18 @@ const handleClear = () => {
border-radius: 8px; border-radius: 8px;
background-color: var(--base-color-light); background-color: var(--base-color-light);
color: var(--base-color-text); color: var(--base-color-text);
&:disabled {
background-color: hsl(0, 0%, 92%);
cursor: not-allowed;
}
} }
} }
} }
.input-help { .input-help {
display: block; display: block;
font-size: 85%; font-size: 70%;
margin-bottom: 8px; margin-bottom: 8px;
.input-invalid { .input-invalid {

View File

@@ -51,10 +51,8 @@ const tabGroups = [
[ [
{ title: "Contact", to: "/contact" }, { title: "Contact", to: "/contact" },
{ title: "Code of Conduct", to: "/code-of-conduct" }, { title: "Code of Conduct", to: "/code-of-conduct" },
{ title: "Privacy", to: "/page" }, { title: "Terms and Conditions", to: "/terms-and-conditions" },
{ title: "Governance", to: "/page" }, { title: "Privacy", to: "/privacy" },
{ title: "Teams", to: "/login" },
{ title: "License", to: "/page" },
], ],
]; ];
@@ -115,16 +113,19 @@ const tabs = () => {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
width: 100%; width: 100%;
white-space: nowrap;
.tab-item { .tab-item {
display: inline-block;
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
font-family: var(--header-font-family); font-family: var(--header-font-family);
font-weight: 800; font-weight: 800;
font-size: 18px; font-size: 18px;
text-decoration: none; text-decoration: none;
padding: 16px 24px; padding: 16px 24px;
white-space: nowrap;
&:hover { &:hover:not(.active) {
color: rgba(255, 255, 255); color: rgba(255, 255, 255);
background-color: hsla(0, 0%, 100%, 0.1); background-color: hsla(0, 0%, 100%, 0.1);
} }
@@ -132,10 +133,23 @@ const tabs = () => {
&.active { &.active {
background-color: var(--base-color); background-color: var(--base-color);
color: var(--primary-color); color: var(--primary-color);
&:hover {
filter: none;
} }
} }
} }
} }
}
@media (max-width: 900px) {
.masthead .tabs {
display: block;
overflow-x: auto;
scroll-behavior: smooth;
scrollbar-width: none;
}
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.masthead { .masthead {

View File

@@ -266,7 +266,10 @@ onUnmounted(() => {
margin: 0; margin: 0;
padding: 0 0 12px 0; padding: 0 0 12px 0;
li a { li {
margin-bottom: 0;
a {
color: var(--base-color-text); color: var(--base-color-text);
display: block; display: block;
padding: 12px 0; padding: 12px 0;
@@ -285,6 +288,7 @@ onUnmounted(() => {
} }
} }
} }
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.sm-navbar #sm-nav-head #sm-nav-toggle { .sm-navbar #sm-nav-head #sm-nav-toggle {

View File

@@ -0,0 +1,32 @@
<template>
<div class="no-items">
<ion-icon name="alert-circle-outline" />
<p>{{ props.text }}</p>
</div>
</template>
<script setup lang="ts">
const props = defineProps({
text: {
type: String,
default: "No items found",
required: false,
},
});
</script>
<style lang="scss">
.no-items {
margin-top: 32px;
text-align: center;
ion-icon {
font-size: 400%;
}
p {
margin: 0;
font-size: 130%;
}
}
</style>

View File

@@ -128,16 +128,15 @@ const handleClickPage = (page: number): void => {
font-family: var(--header-font-family); font-family: var(--header-font-family);
font-size: 90%; font-size: 90%;
font-weight: 600; font-weight: 600;
margin-bottom: 24px; margin: 24px auto;
box-shadow: var(--base-shadow);
.item { .item {
display: flex; display: flex;
cursor: pointer; cursor: pointer;
background-color: var(--base-color-light); background-color: var(--base-color-light);
padding: 12px 16px; padding: 12px 16px;
border-right: 1px solid rgba(0, 0, 0, 0.2); border-right: 1px solid rgba(0, 0, 0, 0.1);
border-left: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: var(--base-shadow);
&.active { &.active {
background-color: var(--primary-color); background-color: var(--primary-color);
@@ -159,8 +158,8 @@ const handleClickPage = (page: number): void => {
padding-left: 12px; padding-left: 12px;
} }
&:hover { &:hover:not(.active) {
filter: brightness(115%); background-color: var(--primary-color-hover);
} }
} }
} }

View File

@@ -1,10 +1,6 @@
<template> <template>
<div <div
:class="[ :class="['row', { 'row-break-lg': breakLarge }, { 'flex-fill': fill }]">
'sm-row',
{ 'row-break-lg': breakLarge },
{ 'flex-fill': fill },
]">
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
@@ -25,7 +21,7 @@ defineProps({
</script> </script>
<style lang="scss"> <style lang="scss">
.sm-row { .row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin: 0 auto; margin: 0 auto;
@@ -35,7 +31,7 @@ defineProps({
} }
@media screen and (max-width: 992px) { @media screen and (max-width: 992px) {
.sm-row.row-break-lg { .row.row-break-lg {
flex-direction: column; flex-direction: column;
} }
} }

View File

@@ -80,7 +80,7 @@ const handleRowClick = (item) => {
tr { tr {
&:hover { &:hover {
td { td {
background-color: rgba(0, 0, 255, 0.1) !important; background-color: var(--primary-color-hover);
cursor: pointer; cursor: pointer;
} }
} }

View File

@@ -1,69 +1,15 @@
<template> <template>
<div class="sm-toolbar"> <div class="toolbar">
<div v-if="slots.left" class="sm-toolbar-column sm-toolbar-column-left">
<slot name="left"></slot>
</div>
<div v-if="slots.default" class="sm-toolbar-column">
<slot></slot> <slot></slot>
</div> </div>
<div
v-if="slots.right"
class="sm-toolbar-column sm-toolbar-column-right">
<slot name="right"></slot>
</div>
</div>
</template> </template>
<script setup lang="ts">
import { useSlots } from "vue";
const slots = useSlots();
</script>
<style lang="scss"> <style lang="scss">
.sm-toolbar { .toolbar {
display: flex; display: flex;
justify-content: space-between;
margin-bottom: map-get($spacer, 2);
width: 100%; width: 100%;
justify-content: space-between;
.sm-toolbar-column { align-items: center;
display: flex; gap: 20px;
flex: 1;
flex-direction: row;
align-items: flex-start;
&.sm-toolbar-column-left {
justify-content: flex-start;
}
& > * {
margin: 0 map-get($spacer, 1);
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
&.sm-toolbar-column-right {
justify-content: flex-end;
}
}
}
@media screen and (max-width: 768px) {
.sm-toolbar {
.sm-toolbar-column {
flex-direction: column;
& > * {
margin: 0;
}
}
}
} }
</style> </style>

View File

@@ -136,7 +136,7 @@ export class SMDate {
parsedMonth: number = 0, parsedMonth: number = 0,
parsedYear: number = 0; parsedYear: number = 0;
if (year.length == 3 || year.length >= 5) { if (year == undefined || year.length == 3 || year.length >= 5) {
return this; return this;
} }

View File

@@ -101,14 +101,6 @@ export const routes = [
}, },
component: () => import("@/views/Unsubscribe.vue"), component: () => import("@/views/Unsubscribe.vue"),
}, },
{
path: "/terms",
name: "terms",
meta: {
title: "Terms and Conditions",
},
component: () => import("@/views/Terms.vue"),
},
{ {
path: "/minecraft", path: "/minecraft",
name: "minecraft", name: "minecraft",
@@ -142,6 +134,10 @@ export const routes = [
}, },
component: () => import("@/views/Contact.vue"), component: () => import("@/views/Contact.vue"),
}, },
{
path: "/conduct",
redirect: { name: "code-of-conduct" },
},
{ {
path: "/code-of-conduct", path: "/code-of-conduct",
name: "code-of-conduct", name: "code-of-conduct",
@@ -150,6 +146,18 @@ export const routes = [
}, },
component: () => import("@/views/CodeOfConduct.vue"), component: () => import("@/views/CodeOfConduct.vue"),
}, },
{
path: "/terms",
redirect: { name: "terms-and-conditions" },
},
{
path: "/terms-and-conditions",
name: "terms-and-conditions",
meta: {
title: "Terms and Conditions",
},
component: () => import("@/views/TermsAndConditions.vue"),
},
{ {
path: "/register", path: "/register",
name: "register", name: "register",

View File

@@ -1,19 +1,14 @@
<template> <template>
<SMPage class="sm-page-post-view" :page-error="pageError"> <div
<SMContainer> class="thumbnail"
<div class="sm-post-hero" :style="backgroundStyle"></div> :style="{ backgroundImage: `url('${backgroundImageUrl}')` }"></div>
<div class="sm-heading-info"> <SMContainer narrow>
<h1>{{ post.title }}</h1> <h1 class="title">{{ post.title }}</h1>
<div class="sm-date-author small"> <div class="author">By {{ post.user.username }}</div>
<ion-icon name="calendar-outline" /> <div class="date">{{ formattedDate(post.publish_at) }}</div>
{{ formattedPublishAt(post.publish_at) }}, by <SMHTML :html="post.content" class="content" />
{{ post.user.username }}
</div>
</div>
<SMHTML :html="post.content" />
<SMAttachments :attachments="post.attachments || []" /> <SMAttachments :attachments="post.attachments || []" />
</SMContainer> </SMContainer>
</SMPage>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -52,7 +47,10 @@ let pageLoading = ref(false);
*/ */
let postUser: User | null = null; let postUser: User | null = null;
let backgroundStyle = {}; /**
* Thumbnail image URL.
*/
let backgroundImageUrl = ref("");
/** /**
* Load the page data. * Load the page data.
@@ -81,12 +79,7 @@ const handleLoad = async () => {
utc: true, utc: true,
}).format("yyyy/MM/dd HH:mm:ss"); }).format("yyyy/MM/dd HH:mm:ss");
backgroundStyle = { backgroundImageUrl.value = mediaGetVariantUrl(post.value.hero);
backgroundImage: `url('${mediaGetVariantUrl(
post.value.hero
)}')`,
};
applicationStore.setDynamicTitle(post.value.title); applicationStore.setDynamicTitle(post.value.title);
} else { } else {
pageError.value = 404; pageError.value = 404;
@@ -101,7 +94,13 @@ const handleLoad = async () => {
} }
}; };
const formattedPublishAt = (dateStr) => { /**
* Format Date
*
* @param dateStr Date string.
* @returns Formatted date.
*/
const formattedDate = (dateStr) => {
return new SMDate(dateStr, { format: "yMd" }).format("MMMM d, yyyy"); return new SMDate(dateStr, { format: "yMd" }).format("MMMM d, yyyy");
}; };
@@ -109,46 +108,34 @@ handleLoad();
</script> </script>
<style lang="scss"> <style lang="scss">
.sm-page-post-view { .page-article {
.sm-container { .thumbnail {
width: 70%;
padding: 64px 0;
}
.sm-post-hero {
display: block;
width: 100%;
height: 480px;
border-radius: 6px;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
aspect-ratio: 16 / 9;
max-height: 640px;
width: 100%;
} }
.sm-heading-info { .title {
padding: 0 map-get($spacer, 3); margin-top: 64px;
margin-bottom: map-get($spacer, 4);
h1 {
text-align: left; text-align: left;
margin-bottom: 0.5rem;
text-overflow: ellipsis;
overflow: hidden;
word-wrap: break-word;
} }
.date-author { .author {
font-size: 80%; margin-top: 16px;
font-weight: 700;
svg {
margin-right: 0.5rem;
}
}
} }
.sm-content { .date {
padding: 0 map-get($spacer, 3); margin-top: 16px;
line-height: 1.5rem; font-weight: 700;
filter: brightness(175%);
}
.content {
margin-top: 24px;
} }
} }

View File

@@ -1,7 +1,11 @@
<template> <template>
<SMMastHead title="Blog" /> <SMMastHead title="Blog" />
<SMContainer> <SMContainer>
<SMInput type="text" label="Search articles" v-model="searchInput"> <SMInput
type="text"
label="Search articles"
v-model="searchInput"
show-clear>
<template #append <template #append
><SMButton ><SMButton
type="primary" type="primary"
@@ -11,11 +15,13 @@
/></template> /></template>
</SMInput> </SMInput>
<SMPagination <SMPagination
v-if="postsTotal > postsPerPage"
v-model="postsPage" v-model="postsPage"
:total="postsTotal" :total="postsTotal"
:per-page="postsPerPage" /> :per-page="postsPerPage" />
<div class="posts"> <div class="posts">
<article <router-link
:to="{ name: 'article', params: { slug: post.slug } }"
class="article-card" class="article-card"
v-for="(post, idx) in posts" v-for="(post, idx) in posts"
:key="idx"> :key="idx">
@@ -27,28 +33,30 @@
'medium' 'medium'
)})`, )})`,
}"></div> }"></div>
<div class="content"> <div class="info">
{{ post.content }} {{ post.user.username }} -
{{ computedDate(post.publish_at) }}
</div> </div>
</article> <h3 class="title">{{ post.title }}</h3>
<p class="content">
{{ excerpt(post.content) }}
</p>
</router-link>
</div> </div>
</SMContainer> </SMContainer>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Ref, ref, watch } from "vue"; import { Ref, ref, watch } from "vue";
import SMMessage from "../components/SMMessage.vue";
import SMPagination from "../components/SMPagination.vue"; import SMPagination from "../components/SMPagination.vue";
import SMPanel from "../components/SMPanel.vue";
import SMPanelList from "../components/SMPanelList.vue";
import { api } from "../helpers/api"; import { api } from "../helpers/api";
import { Post, PostCollection } from "../helpers/api.types"; import { Post, PostCollection } from "../helpers/api.types";
import { SMDate } from "../helpers/datetime"; import { SMDate } from "../helpers/datetime";
import { mediaGetVariantUrl } from "../helpers/media"; import { mediaGetVariantUrl } from "../helpers/media";
import SMMastHead from "../components/SMMastHead.vue"; import SMMastHead from "../components/SMMastHead.vue";
import SMInput from "../components/SMInput.vue"; import SMInput from "../components/SMInput.vue";
import SMForm from "../components/SMForm.vue";
import SMButton from "../components/SMButton.vue"; import SMButton from "../components/SMButton.vue";
import { excerpt } from "../helpers/string";
const message = ref(""); const message = ref("");
const pageLoading = ref(true); const pageLoading = ref(true);
@@ -102,6 +110,10 @@ const handleLoad = () => {
}); });
}; };
const computedDate = (date) => {
return new SMDate(date, { format: "yMd" }).format("d MMMM yyyy");
};
watch( watch(
() => postsPage.value, () => postsPage.value,
() => { () => {
@@ -113,31 +125,58 @@ handleLoad();
</script> </script>
<style lang="scss"> <style lang="scss">
.page-blog {
.posts { .posts {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 30px; gap: 30px;
.article-card { .article-card {
text-decoration: none;
color: var(--base-color-text);
&:hover {
filter: none;
.thumbnail {
filter: brightness(115%);
}
}
.thumbnail { .thumbnail {
aspect-ratio: 16 / 9; aspect-ratio: 16 / 9;
border-radius: 7px; border-radius: 7px;
background-position: center; background-position: center;
background-size: cover; background-size: cover;
background-color: var(--card-background-color); background-color: var(--card-background-color);
box-shadow: 0 5px 10px -3px #00000078; box-shadow: var(--base-shadow);
margin-bottom: 24px;
}
.info {
font-size: 80%;
}
.title {
margin: 16px 0;
word-break: break-all;
}
.content {
font-size: 90%;
}
} }
} }
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.posts { .page-blog .posts {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
.posts { .page-blog .posts {
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
} }
} }

View File

@@ -1,7 +1,7 @@
<template> <template>
<SMMastHead title="Code of Conduct" /> <SMMastHead title="Code of Conduct" />
<SMContainer> <SMContainer narrow>
<div class="container-text"> <template #inner>
<p> <p>
STEMMechanics supports the international community open to STEMMechanics supports the international community open to
everyone without discrimination. We want this community to be a everyone without discrimination. We want this community to be a
@@ -12,11 +12,24 @@
</p> </p>
<h3>Philosophy</h3> <h3>Philosophy</h3>
<p> <p>
If you have a question or would like help with a project, you In the STEMMechanics community, participants from all over the
can send it our way using the form on this page or be emailing world come together to create and work on STEM projects. This is
<a href="mailto:hello@stemmechanics.com.au" made possible by the support, hard work, and enthusiasm of
>hello@stemmechanics.com.au</a people who collaborate towards the common goal of creating great
>. ideas. Cooperation at such a scale requires common guidelines to
ensure a positive and inspiring atmosphere in the community.
</p>
<p>
This is why we have this Code of Conduct: it explains the type
of community we want to have. The rules below are not applied to
all interactions with a simple matching algorithm. Human
interactions happen in context and are complex. Perceived
violations are evaluated by real humans who will try to
interpret the interactions and the rules with kindness.
Accordingly, there is no need to hypothesize on how these rules
would affect normal interactions. Be reasonable, the
<a href="#coc-team">Code of Conduct team</a> surely will be as
well.
</p> </p>
<h3>Application</h3> <h3>Application</h3>
<p> <p>
@@ -47,18 +60,84 @@
please accept it gracefully. please accept it gracefully.
</li> </li>
</ul> </ul>
<h3>Code of Conduct team</h3> <h3>Restricted conduct</h3>
<p>
Participating in restricted conduct will lead to a warning from
community moderators and/or the Code of Conduct team and may
lead to exclusion from the community in the form of a ban from
one or all platforms.
</p>
<ul>
<li>
STEMMechanics is committed to providing a friendly and safe
environment for everyone, regardless of level of experience,
gender identity and expression, sexual orientation,
disability, physical appearance, body size, race, ethnicity,
language proficiency, age, political orientation,
nationality, religion, or other similar characteristics. We
do not tolerate harassment or discrimination of participants
in any form.
</li>
<li>
In particular, we strive to be welcoming to all and to
ensure that anyone can take a more active role in the
community and a project. Targeted harassment of minorities
or individuals is unacceptable.
</li>
<li>Aggressive or offensive behavior is not acceptable.</li>
<li>
You will be excluded from participating in the community if
you insult, demean, harass, intentionally make others
uncomfortable by any means, or participate in any other
hateful conduct, either publicly or privately.
</li>
<li>
Likewise, any spamming, trolling, flaming, baiting, or other
attention-stealing behavior is not welcome and will result
in exclusion from the community.
</li>
<li>
Any form of retaliation against a participant who contacts
the Code of Conduct team is completely unacceptable,
regardless of the outcome of the complaint. Any such
behavior will result in exclusion from the community.
</li>
<li>
For certainty, any conduct which could reasonably be
considered inappropriate in a professional setting is not
acceptable.
</li>
</ul>
<h3>Reporting a breach</h3>
<p>
If you witness or are involved in an interaction with another
community member that you think may violate this Code of
Conduct, please contact STEMMechanics
<a href="#coc-team">Code of Conduct team</a>.
</p>
<p>
STEMMechanics recognizes that it can be difficult to come
forward in cases of a violation of the Code of Conduct. To make
it easier to report violations, we provide a single point of
contact via email at:
<a href="conduct@stemmechanics.com.au"
>conduct@stemmechanics.com.au</a
>. If you are more comfortable reaching out to a single person,
you are also welcome to contact one or more members of the team
using their personal emails listed below, or via direct
messaging on community platforms where they are present.
</p>
<h3 id="coc-item">Code of Conduct team</h3>
<ul> <ul>
<li>James Collins, james@stemmechanics.com.au</li> <li>James Collins, james@stemmechanics.com.au</li>
<ul> <ul>
<li> <li>
GitHub / Discord / Reddit: GitHub / Discord / Reddit / Twitter:
<span class="italic">nomadjimbob</span> <span class="italic">nomadjimbob</span>
</li> </li>
<li>Languages: English</li>
</ul> </ul>
</ul> </ul>
</div> </template>
</SMContainer> </SMContainer>
</template> </template>
@@ -67,29 +146,9 @@ import SMMastHead from "../components/SMMastHead.vue";
</script> </script>
<style lang="scss"> <style lang="scss">
.container-text { .page-code-of-conduct {
max-width: 800px;
margin: 0 auto;
line-height: 1.4em;
h3 { h3 {
margin-top: 60px; margin-top: 60px;
} }
} }
.sm-contact-socials {
list-style-type: none;
padding: 0;
display: flex;
font-size: 200%;
justify-content: center;
li {
margin: 0 16px;
}
}
.address {
margin-top: 60px;
}
</style> </style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<SMContainer class="sm-privacy"> <SMMastHead title="Privacy Policy" />
<template #container> <SMContainer narrow>
<h1>Privacy Policy</h1> <template #inner>
<h3>We take our customers' privacy & security seriously.</h3> <h3>We take our customers' privacy & security seriously.</h3>
<p> <p>
At STEMMechanics, we take our customers' privacy and security At STEMMechanics, we take our customers' privacy and security
@@ -23,8 +23,8 @@
<p> <p>
By using the Website and our online services, you agree to By using the Website and our online services, you agree to
accept the Privacy Policy and the Site's Terms and Conditions accept the Privacy Policy and the Site's Terms and Conditions
<router-link :to="{ name: 'terms' }" <router-link :to="{ name: 'terms-and-conditions' }"
>https://www.stemmechanics.com.au/terms</router-link >https://www.stemmechanics.com.au/terms-and-conditions</router-link
> >
(Terms and Conditions). Where the Privacy Policy uses a word (Terms and Conditions). Where the Privacy Policy uses a word
starting with a capital letter, that term will be defined in the starting with a capital letter, that term will be defined in the
@@ -336,14 +336,18 @@
</SMContainer> </SMContainer>
</template> </template>
<script setup lang="ts">
import SMMastHead from "../components/SMMastHead.vue";
</script>
<style lang="scss"> <style lang="scss">
.sm-privacy { .page-privacy {
h4 { h3 {
margin-bottom: 0.5rem; margin-top: 60px;
} }
ul li { h4 {
margin-bottom: 0.5rem; margin-top: 30px;
} }
} }
</style> </style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<SMPage class="sm-page-terms"> <SMMastHead title="Terms and Conditions" />
<template #container> <SMContainer narrow>
<h1>Terms and Conditions</h1> <template #inner>
<p> <p>
Please read these terms carefully. By accessing or using our Please read these terms carefully. By accessing or using our
website and online servers, you agree to be bound by these terms website and online servers, you agree to be bound by these terms
@@ -20,7 +20,7 @@
agrees to indemnify you and STEMMechanics for its violations of agrees to indemnify you and STEMMechanics for its violations of
these Terms. these Terms.
</p> </p>
<h4>1. Eligibility, registration &amp; account</h4> <h3>1. Eligibility, registration &amp; account</h3>
<p> <p>
You must be 18 years of age to use the Website. If you are under You must be 18 years of age to use the Website. If you are under
18 years of age you must have the permission of your parent or 18 years of age you must have the permission of your parent or
@@ -50,7 +50,7 @@
if you discover or otherwise suspect any security breaches if you discover or otherwise suspect any security breaches
related to the Sites. related to the Sites.
</p> </p>
<h4>2. Ownership of site content</h4> <h3>2. Ownership of site content</h3>
<p> <p>
Unless otherwise indicated on our Sites, the Sites and all Unless otherwise indicated on our Sites, the Sites and all
content and materials therein, including but not limited to the content and materials therein, including but not limited to the
@@ -86,7 +86,7 @@
intellectual property rights, whether by estoppel, implication intellectual property rights, whether by estoppel, implication
or otherwise. This license is revocable at any time. or otherwise. This license is revocable at any time.
</p> </p>
<h4>3. Hyperlinks</h4> <h3>3. Hyperlinks</h3>
<p> <p>
You are granted a limited, non-exclusive right to create a text You are granted a limited, non-exclusive right to create a text
hyperlink to the Sites for non-commercial purposes, provided hyperlink to the Sites for non-commercial purposes, provided
@@ -119,7 +119,7 @@
including privacy and data gathering practices, of any site to including privacy and data gathering practices, of any site to
which you navigate from the Sites. which you navigate from the Sites.
</p> </p>
<h4>4. User content</h4> <h3>4. User content</h3>
<p> <p>
The Sites may include discussion blogs, profiles, product The Sites may include discussion blogs, profiles, product
reviews or other interactive features or areas (collectively, reviews or other interactive features or areas (collectively,
@@ -223,7 +223,7 @@
among other things, termination or suspension of your rights to among other things, termination or suspension of your rights to
use the Sites. use the Sites.
</p> </p>
<h4>5. Rights in user content</h4> <h3>5. Rights in user content</h3>
<p> <p>
Except as otherwise provided herein, on the Sites or in a Except as otherwise provided herein, on the Sites or in a
separate agreement with us (such as the rules of a STEMMechanics separate agreement with us (such as the rules of a STEMMechanics
@@ -261,7 +261,7 @@
guidelines or policies or any applicable law, rule or guidelines or policies or any applicable law, rule or
regulation. regulation.
</p> </p>
<h4>6. Feedback</h4> <h3>6. Feedback</h3>
<p> <p>
Separate and apart from User Content, you have the ability to Separate and apart from User Content, you have the ability to
submit questions, comments suggestions, reviews, ideas, plans, submit questions, comments suggestions, reviews, ideas, plans,
@@ -279,7 +279,7 @@
idea might be great, but we may have already had the same or a idea might be great, but we may have already had the same or a
similar idea and we do not want disputes. similar idea and we do not want disputes.
</p> </p>
<h4>7. User conduct</h4> <h3>7. User conduct</h3>
<p> <p>
You agree that you will not violate any law, contract or You agree that you will not violate any law, contract or
intellectual property or other third party right or commit a intellectual property or other third party right or commit a
@@ -364,14 +364,14 @@
before contacting or meeting anyone (online or offline) that is before contacting or meeting anyone (online or offline) that is
unfamiliar to you. unfamiliar to you.
</p> </p>
<h4>8. No third-party beneficiaries</h4> <h3>8. No third-party beneficiaries</h3>
<p> <p>
These Terms are for the benefit of, and will be enforceable by, These Terms are for the benefit of, and will be enforceable by,
the parties only. These Terms are not intended to confer any the parties only. These Terms are not intended to confer any
right or benefit on any third party or to create any obligations right or benefit on any third party or to create any obligations
or liability of a party to any such third party. or liability of a party to any such third party.
</p> </p>
<h4>9. Indemnification</h4> <h3>9. Indemnification</h3>
<p> <p>
To the fullest extent permitted by applicable law, you agree to To the fullest extent permitted by applicable law, you agree to
defend, indemnify and hold harmless STEMMechanics and our defend, indemnify and hold harmless STEMMechanics and our
@@ -385,7 +385,7 @@
provide; (d) your violation of these Terms; and (e) your provide; (d) your violation of these Terms; and (e) your
violation of any rights of another. violation of any rights of another.
</p> </p>
<h4>10. Disclaimers</h4> <h3>10. Disclaimers</h3>
<p> <p>
Except as expressly provided, the Sites, Site Content, User Except as expressly provided, the Sites, Site Content, User
Content and services provided on or in connection with the Sites Content and services provided on or in connection with the Sites
@@ -413,7 +413,7 @@
is not a substitute for in-person guidance by a qualified is not a substitute for in-person guidance by a qualified
instructor. instructor.
</p> </p>
<h4>11. Liability</h4> <h3>11. Liability</h3>
<p> <p>
To the fullest extent permitted by applicable law, in no event To the fullest extent permitted by applicable law, in no event
shall the STEMMechanics parties be liable for any special, shall the STEMMechanics parties be liable for any special,
@@ -433,13 +433,13 @@
access to an STEMMechanics party's records, programs or access to an STEMMechanics party's records, programs or
services. services.
</p> </p>
<h4>12. Modifications to site</h4> <h3>12. Modifications to site</h3>
<p> <p>
STEMMechanics reserves the right to modify or discontinue, STEMMechanics reserves the right to modify or discontinue,
temporarily or permanently, the Sites or any features or temporarily or permanently, the Sites or any features or
portions thereof without prior notice. portions thereof without prior notice.
</p> </p>
<h4>13. Termination</h4> <h3>13. Termination</h3>
<p> <p>
You may terminate the Terms at any time by closing your account, You may terminate the Terms at any time by closing your account,
discontinuing your use of the Sites and providing STEMMechanics discontinuing your use of the Sites and providing STEMMechanics
@@ -449,14 +449,14 @@
block or prevent your future access to and use of the Sites or block or prevent your future access to and use of the Sites or
any portion of the Sites. any portion of the Sites.
</p> </p>
<h4>14. Severability</h4> <h3>14. Severability</h3>
<p> <p>
If any provision of these Terms shall be deemed unlawful, void If any provision of these Terms shall be deemed unlawful, void
or for any reason unenforceable, then that provision shall be or for any reason unenforceable, then that provision shall be
deemed severable from these Terms and shall not affect the deemed severable from these Terms and shall not affect the
validity and enforceability of any remaining provisions. validity and enforceability of any remaining provisions.
</p> </p>
<h4>15. Ordering online</h4> <h3>15. Ordering online</h3>
<p> <p>
Upon completing your order and submitting it through the Upon completing your order and submitting it through the
checkout system, an order reference number will be issued to you checkout system, an order reference number will be issued to you
@@ -486,7 +486,7 @@
Digital items cannot be cancelled or edited after receiving Digital items cannot be cancelled or edited after receiving
payment. payment.
</p> </p>
<h4>16. Pricing &amp; availability</h4> <h3>16. Pricing &amp; availability</h3>
<p> <p>
All prices are shown in Australia dollars (AUD). All items are All prices are shown in Australia dollars (AUD). All items are
subject to availability and we reserve the right to impose subject to availability and we reserve the right to impose
@@ -497,7 +497,7 @@
from those in the store or from store-advertised prices. All from those in the store or from store-advertised prices. All
purchases on applicable products include GST at the rate of 10%. purchases on applicable products include GST at the rate of 10%.
</p> </p>
<h4>17. Errors</h4> <h3>17. Errors</h3>
<p> <p>
We attempt to be as accurate as possible and eliminate errors on We attempt to be as accurate as possible and eliminate errors on
the Sites; however, we do not warrant that any product, service, the Sites; however, we do not warrant that any product, service,
@@ -511,7 +511,7 @@
any amount charged. Your sole remedy in the event of such error any amount charged. Your sole remedy in the event of such error
is to cancel your order and obtain a refund. is to cancel your order and obtain a refund.
</p> </p>
<h4>18. Out of stock / pre-order items</h4> <h3>18. Out of stock / pre-order items</h3>
<p> <p>
If the colour or size you want is not listed in the "Choose Your If the colour or size you want is not listed in the "Choose Your
Colour/Size" drop-down box on the Product Information page, it Colour/Size" drop-down box on the Product Information page, it
@@ -527,10 +527,10 @@
the availability of that item. If you have items on pre-order the availability of that item. If you have items on pre-order
that you would like to cancel, please contact us. that you would like to cancel, please contact us.
</p> </p>
<h4> <h3>
19. Agreement to Conduct Transactions Electronically; Recording; 19. Agreement to Conduct Transactions Electronically; Recording;
Copies Copies
</h4> </h3>
<p> <p>
You agree that all of your transactions with or through the You agree that all of your transactions with or through the
Sites may, at our option, be conducted electronically from start Sites may, at our option, be conducted electronically from start
@@ -542,7 +542,7 @@
other contract or disclosure that we are required to provide to other contract or disclosure that we are required to provide to
you. you.
</p> </p>
<h4>20. Payment</h4> <h3>20. Payment</h3>
<p> <p>
We currently accept Visa and Mastercard online. Only valid We currently accept Visa and Mastercard online. Only valid
credit cards or other payment method acceptable to us may be credit cards or other payment method acceptable to us may be
@@ -556,7 +556,7 @@
otherwise acceptable, your order may be suspended or cancelled otherwise acceptable, your order may be suspended or cancelled
automatically. automatically.
</p> </p>
<h4>21. Third-party sellers / on-sellers (buying &amp; selling)</h4> <h3>21. Third-party sellers / on-sellers (buying &amp; selling)</h3>
<p> <p>
You may not place orders with the intention to immediately You may not place orders with the intention to immediately
on-forward the products to another person in a business on-forward the products to another person in a business
@@ -592,5 +592,17 @@
but this is allowed under the Law. but this is allowed under the Law.
</p> </p>
</template> </template>
</SMPage> </SMContainer>
</template> </template>
<script setup lang="ts">
import SMMastHead from "../components/SMMastHead.vue";
</script>
<style lang="scss">
.page-terms-and-conditions {
h3 {
margin-top: 60px;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<SMMastHead title="Workshops" /> <SMMastHead title="Workshops" />
<SMContainer> <SMContainer>
<SMToolbar> <SMToolbar class="align-items-start">
<SMInput <SMInput
v-model="filterKeywords" v-model="filterKeywords"
label="Keywords" label="Keywords"
@@ -10,12 +10,14 @@
<SMInput <SMInput
v-model="filterLocation" v-model="filterLocation"
label="Location" label="Location"
:show-clear="true"
@change="handleFilter" /> @change="handleFilter" />
<SMInput <SMInput
v-model="filterDateRange" v-model="filterDateRange"
type="daterange" type="daterange"
label="Date Range" label="Date Range"
:feedback-invalid="dateRangeError" :feedback-invalid="dateRangeError"
:show-clear="true"
@change="handleFilter" /> @change="handleFilter" />
</SMToolbar> </SMToolbar>
<SMPagination <SMPagination
@@ -30,9 +32,9 @@
:message="formMessage" :message="formMessage"
class="mt-5" /> class="mt-5" />
<div class="events-list"> <div v-if="postsTotal > 0" class="events">
<router-link <router-link
class="event" class="event-card"
v-for="event in events" v-for="event in events"
:key="event.id" :key="event.id"
:to="{ name: 'event', params: { id: event.id } }"> :to="{ name: 'event', params: { id: event.id } }">
@@ -62,6 +64,7 @@
</div> </div>
</router-link> </router-link>
</div> </div>
<SMNoItems v-else />
</SMContainer> </SMContainer>
</template> </template>
@@ -76,6 +79,7 @@ import { Event, EventCollection } from "../helpers/api.types";
import { SMDate } from "../helpers/datetime"; import { SMDate } from "../helpers/datetime";
import SMMastHead from "../components/SMMastHead.vue"; import SMMastHead from "../components/SMMastHead.vue";
import SMContainer from "../components/SMContainer.vue"; import SMContainer from "../components/SMContainer.vue";
import SMNoItems from "../components/SMNoItems.vue";
interface EventData { interface EventData {
event: Event; event: Event;
@@ -87,7 +91,7 @@ const loading = ref(true);
let events: Event[] = reactive([]); let events: Event[] = reactive([]);
const dateRangeError = ref(""); const dateRangeError = ref("");
const formMessage = ref(""); const formMessage = ref("123");
const filterKeywords = ref(""); const filterKeywords = ref("");
const filterLocation = ref(""); const filterLocation = ref("");
@@ -330,13 +334,13 @@ handleLoad();
<style lang="scss"> <style lang="scss">
.page-workshops { .page-workshops {
.events-list { .events {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 30px; gap: 30px;
width: 100%; width: 100%;
.event { .event-card {
background-color: var(--base-color-light); background-color: var(--base-color-light);
box-shadow: 0 5px 10px -3px rgba(0, 0, 0, 0.25); box-shadow: 0 5px 10px -3px rgba(0, 0, 0, 0.25);
border-radius: 8px; border-radius: 8px;
@@ -359,6 +363,7 @@ handleLoad();
.title { .title {
margin: 0 0 16px 0; margin: 0 0 16px 0;
font-size: 100%; font-size: 100%;
word-break: break-all;
} }
.row { .row {
@@ -387,7 +392,7 @@ handleLoad();
@media (min-width: 768px) { @media (min-width: 768px) {
.page-workshops { .page-workshops {
.events-list { .events {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
} }
@@ -395,43 +400,9 @@ handleLoad();
@media (min-width: 1024px) { @media (min-width: 1024px) {
.page-workshops { .page-workshops {
.events-list { .events {
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
} }
} }
} }
// .sm-page-workshop-list {
// background-color: #f8f8f8;
// .toolbar {
// display: flex;
// flex-direction: row;
// flex: 1;
// & > * {
// padding-left: map-get($spacer, 1);
// padding-right: map-get($spacer, 1);
// &:first-child {
// padding-left: 0;
// }
// &:last-child {
// padding-right: 0;
// }
// }
// }
// }
// @media screen and (max-width: 768px) {
// .sm-page-workshop-list .toolbar {
// flex-direction: column;
// & > * {
// padding-left: 0;
// padding-right: 0;
// }
// }
// }
</style> </style>

View File

@@ -1,67 +1,70 @@
<template> <template>
<SMMastHead title="Dashboard" /> <SMMastHead title="Dashboard" />
<div class="boxes"> <SMContainer>
<router-link to="/dashboard/details" class="box"> <div class="cards">
<router-link to="/dashboard/details" class="admin-card details">
<ion-icon name="location-outline" /> <ion-icon name="location-outline" />
<h2>My Details</h2> <h3>My Details</h3>
</router-link> </router-link>
<router-link <router-link
v-if="userStore.permissions.includes('admin/posts')" v-if="userStore.permissions.includes('admin/posts')"
:to="{ name: 'dashboard-post-list' }" :to="{ name: 'dashboard-post-list' }"
class="box"> class="admin-card posts">
<ion-icon name="newspaper-outline" /> <ion-icon name="newspaper-outline" />
<h2>Posts</h2> <h3>Posts</h3>
</router-link> </router-link>
<router-link <router-link
v-if="userStore.permissions.includes('admin/users')" v-if="userStore.permissions.includes('admin/users')"
:to="{ name: 'dashboard-user-list' }" :to="{ name: 'dashboard-user-list' }"
class="box"> class="admin-card users">
<ion-icon name="people-outline" /> <ion-icon name="people-outline" />
<h2>Users</h2> <h3>Users</h3>
</router-link> </router-link>
<router-link <router-link
v-if="userStore.permissions.includes('admin/events')" v-if="userStore.permissions.includes('admin/events')"
:to="{ name: 'dashboard-event-list' }" :to="{ name: 'dashboard-event-list' }"
class="box"> class="admin-card events">
<ion-icon name="calendar-outline" /> <ion-icon name="calendar-outline" />
<h2>Events</h2> <h3>Events</h3>
</router-link> </router-link>
<router-link <router-link
v-if="userStore.permissions.includes('admin/courses')" v-if="userStore.permissions.includes('admin/courses')"
:to="{ name: 'dashboard-event-list' }" :to="{ name: 'dashboard-event-list' }"
class="box"> class="admin-card courses">
<ion-icon name="school-outline" /> <ion-icon name="school-outline" />
<h2>{{ courseBoxTitle }}</h2> <h3>{{ courseBoxTitle }}</h3>
</router-link> </router-link>
<router-link <router-link
v-if="userStore.permissions.includes('admin/media')" v-if="userStore.permissions.includes('admin/media')"
:to="{ name: 'dashboard-media-list' }" :to="{ name: 'dashboard-media-list' }"
class="box"> class="admin-card media">
<ion-icon name="film-outline" /> <ion-icon name="film-outline" />
<h2>Media</h2> <h3>Media</h3>
</router-link> </router-link>
<router-link <router-link
v-if="userStore.permissions.includes('admin/media')" v-if="userStore.permissions.includes('admin/media')"
:to="{ name: 'dashboard-media-list' }" :to="{ name: 'dashboard-media-list' }"
class="box" class="admin-card minecraft"
style="background-image: url('/img/minecraft.png')"> style="background-image: url('/img/minecraft.png')">
<img src="/img/minecraft-grass-block.png" /> <img src="/img/minecraft-grass-block.png" />
<h2>Minecraft</h2> <h3>Minecraft</h3>
</router-link> </router-link>
<router-link <router-link
v-if="userStore.permissions.includes('logs/discord')" v-if="userStore.permissions.includes('logs/discord')"
:to="{ name: 'dashboard-discord-bot-logs' }" :to="{ name: 'dashboard-discord-bot-logs' }"
class="box"> class="admin-card discord">
<ion-icon name="logo-discord" /> <ion-icon name="logo-discord" />
<h2>Discord Bot Logs</h2> <h3>Discord Bot Logs</h3>
</router-link> </router-link>
</div> </div>
</SMContainer>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import { computed } from "vue";
import { useUserStore } from "../../store/UserStore"; import { useUserStore } from "../../store/UserStore";
import SMMastHead from "../../components/SMMastHead.vue"; import SMMastHead from "../../components/SMMastHead.vue";
import SMContainer from "../../components/SMContainer.vue";
const userStore = useUserStore(); const userStore = useUserStore();
@@ -75,51 +78,36 @@ const courseBoxTitle = computed(() => {
</script> </script>
<style lang="scss"> <style lang="scss">
.boxes { .page-dashboard {
.cards {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 30px;
justify-content: center; justify-content: center;
margin: -#{map-get($spacer, 3)};
.box { .admin-card {
display: flex; display: flex;
flex-basis: map-get($spacer, 5) * 4.5;
flex-direction: column; flex-direction: column;
border-radius: 12px; flex-basis: 224px;
border: 2px solid $primary-color-dark;
background-color: #f8f8f8;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
padding: map-get($spacer, 5) map-get($spacer, 4);
margin: map-get($spacer, 3);
font-size: map-get($spacer, 3);
color: $primary-color-dark !important;
margin-bottom: map-get($spacer, 5);
transition: all 0.2s ease-in-out;
align-items: center; align-items: center;
text-align: center; color: var(--base-color-text);
border-radius: 10px;
h2 { background-color: var(--base-color-light);
margin-top: map-get($spacer, 2); text-decoration: none;
margin-bottom: 0; box-shadow: var(--base-shadow);
} padding: 32px;
ion-icon { ion-icon {
font-size: map-get($spacer, 5); font-size: 64px;
} }
img { img {
height: map-get($spacer, 5); height: 64px;
} }
&:hover { &.minecraft {
text-decoration: none; color: #eee;
background-color: $primary-color-lighter; }
border-color: $primary-color-darker;
color: $primary-color-darker !important;
box-shadow: 0 0 14px rgba(0, 0, 0, 0.25);
transform: scale(1.01);
} }
} }
} }

View File

@@ -1,15 +1,23 @@
<template> <template>
<SMPage class="sm-page-user-edit"> <SMMastHead
<template #container> :title="pageHeading"
<SMHeading :heading="pageHeading" /> :back-link="{ name: 'dashboard' }"
back-title="Back to Dashboard" />
<SMContainer>
<SMForm :model-value="form" @submit="handleSubmit"> <SMForm :model-value="form" @submit="handleSubmit">
<SMRow>
<SMColumn><SMInput control="username" disabled /></SMColumn>
<SMColumn><SMInput control="display_name" /></SMColumn>
</SMRow>
<SMRow> <SMRow>
<SMColumn><SMInput control="first_name" /></SMColumn> <SMColumn><SMInput control="first_name" /></SMColumn>
<SMColumn><SMInput control="last_name" /></SMColumn> <SMColumn><SMInput control="last_name" /></SMColumn>
</SMRow> </SMRow>
<SMRow> <SMRow>
<SMColumn><SMInput control="email" /></SMColumn> <SMColumn><SMInput control="email" /></SMColumn>
<SMColumn><SMInput control="phone" /></SMColumn> <SMColumn
><SMInput control="phone">This field is optional</SMInput>
</SMColumn>
</SMRow> </SMRow>
<SMRow> <SMRow>
<SMColumn> <SMColumn>
@@ -25,31 +33,34 @@
</SMColumn> </SMColumn>
</SMRow> </SMRow>
</SMForm> </SMForm>
</template> </SMContainer>
</SMPage>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, reactive } from "vue"; import { computed, reactive } from "vue";
import { useRoute } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { openDialog } from "../../components/SMDialog"; import { openDialog } from "../../components/SMDialog";
import SMDialogChangePassword from "../../components/dialogs/SMDialogChangePassword.vue"; import SMDialogChangePassword from "../../components/dialogs/SMDialogChangePassword.vue";
import SMButton from "../../components/SMButton.vue"; import SMButton from "../../components/SMButton.vue";
import SMForm from "../../components/SMForm.vue"; import SMForm from "../../components/SMForm.vue";
import SMFormFooter from "../../components/SMFormFooter.vue"; import SMFormFooter from "../../components/SMFormFooter.vue";
import SMHeading from "../../components/SMHeading.vue"; import SMInput from "../../components/SMInput.vue";
import SMInput from "../../depreciated/SMInput-old.vue";
import { api } from "../../helpers/api"; import { api } from "../../helpers/api";
import { UserResponse } from "../../helpers/api.types"; import { UserResponse } from "../../helpers/api.types";
import { Form, FormControl } from "../../helpers/form"; import { Form, FormControl } from "../../helpers/form";
import { And, Email, Phone, Required } from "../../helpers/validate"; import { And, Email, Phone, Required } from "../../helpers/validate";
import { useUserStore } from "../../store/UserStore"; import { useUserStore } from "../../store/UserStore";
import SMMastHead from "../../components/SMMastHead.vue";
import { useToastStore } from "../../store/ToastStore";
const route = useRoute(); const route = useRoute();
const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
let form = reactive( let form = reactive(
Form({ Form({
username: FormControl("", And([Required()])),
display_name: FormControl("", And([Required()])),
first_name: FormControl("", And([Required()])), first_name: FormControl("", And([Required()])),
last_name: FormControl("", And([Required()])), last_name: FormControl("", And([Required()])),
email: FormControl("", And([Required(), Email()])), email: FormControl("", And([Required(), Email()])),
@@ -65,7 +76,7 @@ const loadData = async () => {
try { try {
form.loading(true); form.loading(true);
const result = await api.get({ const result = await api.get({
url: "users/{id}", url: "/users/{id}",
params: { params: {
id: route.params.id, id: route.params.id,
}, },
@@ -85,6 +96,7 @@ const loadData = async () => {
form.loading(false); form.loading(false);
} }
} else { } else {
form.controls.username.value = userStore.username;
form.controls.first_name.value = userStore.firstName; form.controls.first_name.value = userStore.firstName;
form.controls.last_name.value = userStore.lastName; form.controls.last_name.value = userStore.lastName;
form.controls.phone.value = userStore.phone; form.controls.phone.value = userStore.phone;
@@ -99,7 +111,7 @@ const handleSubmit = async () => {
try { try {
form.loading(true); form.loading(true);
const result = await api.put({ const result = await api.put({
url: "users/{id}", url: "/users/{id}",
params: { params: {
id: userStore.id, id: userStore.id,
}, },
@@ -117,7 +129,15 @@ const handleSubmit = async () => {
userStore.setUserDetails(data.user); userStore.setUserDetails(data.user);
} }
form.message("Your details have been updated", "success"); useToastStore().addToast({
title: route.params.id ? "Details Updated" : "User Created",
content: route.params.id
? "The user has been updated."
: "The user has been created.",
type: "success",
});
router.push({ name: "dashboard" });
} catch (err) { } catch (err) {
form.apiErrors(err); form.apiErrors(err);
} finally { } finally {