Compare commits
No commits in common. 'main' and 'katex' have entirely different histories.
@ -1,53 +0,0 @@
|
||||
---
|
||||
kind: pipeline
|
||||
type: kubernetes
|
||||
name: default
|
||||
|
||||
metadata:
|
||||
labels:
|
||||
pod-security.kubernetes.io/audit: privileged
|
||||
|
||||
services:
|
||||
- name: docker daemon
|
||||
image: docker:dind
|
||||
privileged: true
|
||||
environment:
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
- promote
|
||||
|
||||
steps:
|
||||
- name: node.js build
|
||||
image: node:18
|
||||
commands:
|
||||
- "npm add --global pnpm"
|
||||
- pnpm i
|
||||
- pnpm run build
|
||||
|
||||
- name: container build
|
||||
image: docker:latest
|
||||
privileged: true
|
||||
commands:
|
||||
- "while ! docker stats --no-stream; do sleep 1; done"
|
||||
- "docker build -t $DOCKER_REGISTRY/glmdev/mathy ."
|
||||
- "docker push $DOCKER_REGISTRY/glmdev/mathy"
|
||||
environment:
|
||||
DOCKER_HOST: tcp://localhost:2375
|
||||
DOCKER_REGISTRY:
|
||||
from_secret: DOCKER_REGISTRY
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
- promote
|
||||
|
||||
- name: k8s rollout
|
||||
image: bitnami/kubectl
|
||||
commands:
|
||||
- cd deploy && kubectl apply -f .
|
||||
- kubectl rollout -n mathy restart deployment/mathy
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
- promote
|
@ -0,0 +1,56 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mathy
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
name: mathy
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: mathy
|
||||
spec:
|
||||
containers:
|
||||
- name: mathy
|
||||
image: glmdev/mathy:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mathy-service
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
protocol: TCP
|
||||
targetPort: 80
|
||||
selector:
|
||||
name: mathy
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: mathy-ingress
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "nginx"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- mathy.glmdev.tech
|
||||
secretName: mathy-tls
|
||||
rules:
|
||||
- host: "mathy.glmdev.tech"
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
name: mathy-service
|
||||
port:
|
||||
number: 80
|
@ -1,5 +0,0 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: mathy
|
@ -1,12 +0,0 @@
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: mathy-tls
|
||||
namespace: mathy
|
||||
spec:
|
||||
secretName: mathy-tls
|
||||
dnsNames:
|
||||
- 'crystalmath.tech'
|
||||
issuerRef:
|
||||
name: letsencrypt-ca
|
||||
kind: ClusterIssuer
|
@ -1,127 +0,0 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mathy
|
||||
namespace: mathy
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
name: mathy
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: mathy
|
||||
spec:
|
||||
containers:
|
||||
- name: mathy
|
||||
image: glmdev/mathy:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 80
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mathy-api
|
||||
namespace: mathy
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
name: mathy-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: mathy-api
|
||||
spec:
|
||||
containers:
|
||||
- name: mathy
|
||||
image: glmdev/mathy-api:latest
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: REDIS_HOST
|
||||
value: localhost
|
||||
- name: DATABASE_HOST
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: psql-conn
|
||||
key: host
|
||||
optional: false
|
||||
- name: DATABASE_USERNAME
|
||||
value: mathy
|
||||
- name: DATABASE_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: psql-conn
|
||||
key: password
|
||||
optional: false
|
||||
- name: DATABASE_NAME
|
||||
value: mathy
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
|
||||
- name: redis
|
||||
image: redis:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mathy-api-service
|
||||
namespace: mathy
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
protocol: TCP
|
||||
targetPort: 8000
|
||||
selector:
|
||||
name: mathy-api
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mathy-service
|
||||
namespace: mathy
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
protocol: TCP
|
||||
targetPort: 80
|
||||
selector:
|
||||
name: mathy
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: mathy-ingress
|
||||
namespace: mathy
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "nginx"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- crystalmath.tech
|
||||
secretName: mathy-tls
|
||||
rules:
|
||||
- host: "crystalmath.tech"
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/api"
|
||||
backend:
|
||||
service:
|
||||
name: mathy-api-service
|
||||
port:
|
||||
number: 80
|
||||
- pathType: Prefix
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
name: mathy-service
|
||||
port:
|
||||
number: 80
|
@ -1,94 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import Home from "./pages/Home.vue";
|
||||
import Home from "./pages/Login.vue";
|
||||
// This starter template is using Vue 3 <script setup> SFCs
|
||||
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
|
||||
import HelloWorld from "./components/HelloWorld.vue";
|
||||
import { MathStatement } from "./support/parse";
|
||||
import { MathPage } from "./support/page";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { checkLoggedIn, loggedOut, loggedIn } from "./support/auth";
|
||||
import router from "./router";
|
||||
import { Dark } from "quasar";
|
||||
import { ref } from "vue";
|
||||
|
||||
(window as any).Stmt = MathStatement;
|
||||
(window as any).Pg = MathPage;
|
||||
|
||||
const status = ref(checkLoggedIn());
|
||||
(async () => {
|
||||
const response = await fetch("/api/login/status/", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
const res = ((await response.json()) as unknown) as any;
|
||||
if (res.data.hasUser) {
|
||||
loggedIn();
|
||||
}
|
||||
status.value = checkLoggedIn();
|
||||
})();
|
||||
|
||||
onMounted(() => {
|
||||
status.value = checkLoggedIn();
|
||||
console.log(status.value);
|
||||
});
|
||||
|
||||
router.afterEach(() => {
|
||||
status.value = checkLoggedIn();
|
||||
console.log(status.value);
|
||||
});
|
||||
|
||||
const logout = async () => {
|
||||
const response = await fetch("/api/logout/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
loggedOut();
|
||||
status.value = checkLoggedIn();
|
||||
router.push({ path: "/" });
|
||||
};
|
||||
Dark.set(true)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-layout view="hHr LpR fFf">
|
||||
<q-header elevated class="bg-primary text-white">
|
||||
<q-header elevated class="bg-primary text-white" height-hint="98">
|
||||
<q-toolbar>
|
||||
<q-toolbar-title>
|
||||
<q-avatar size="50px">
|
||||
<img src="./assets/l2.svg" />
|
||||
<q-avatar>
|
||||
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg" />
|
||||
</q-avatar>
|
||||
<span style="font-family: 'Cinzel Decorative', cursive;">Crystal Math Worktable</span>
|
||||
Title
|
||||
</q-toolbar-title>
|
||||
<q-btn v-if="status" @click="logout()" label="Logout"></q-btn>
|
||||
</q-toolbar>
|
||||
|
||||
<!-- <q-tabs>
|
||||
<q-route-tab to="/Scratch" label="Scratch" />
|
||||
<q-route-tab to="/Editor" label="Editor" />
|
||||
<q-tab v-if="status" @click="logout()" label="Logout" />
|
||||
</q-tabs>-->
|
||||
<q-tabs align="left">
|
||||
<q-route-tab to="/Scratch" label="Scratch" />
|
||||
<q-route-tab to="/Editor" label="Editor" />
|
||||
|
||||
</q-tabs>
|
||||
</q-header>
|
||||
|
||||
<q-page-container style="display: flex;padding-top: 0px;">
|
||||
<q-page-container>
|
||||
<router-view />
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@700&display=swap");
|
||||
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
margin-top: 60px;
|
||||
}
|
||||
.titleBar {
|
||||
font-family: "Cinzel Decorative";
|
||||
}
|
||||
</style>
|
||||
|
Before Width: | Height: | Size: 679 B After Width: | Height: | Size: 561 B |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.7 MiB |
@ -1,97 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {MathStatement} from '../support/parse'
|
||||
import {onMounted, ref} from 'vue'
|
||||
import {v4 as uuidv4} from 'uuid'
|
||||
import {StatementID} from '../support/types'
|
||||
import Katex from './Katex.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
statement?: MathStatement,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(eventName: 'save', statement: MathStatement): void,
|
||||
}>()
|
||||
|
||||
const functionPreview = ref(MathStatement.temp(''))
|
||||
let functionPreviewKey = ref(uuidv4())
|
||||
const functionError = ref<string|undefined>(undefined)
|
||||
const functionValue = ref('')
|
||||
|
||||
const updateFunctionPreview = () => {
|
||||
const previewStmt = MathStatement.temp(functionValue.value)
|
||||
if ( previewStmt.isValid() ) {
|
||||
functionPreview.value = MathStatement.temp(functionValue.value)
|
||||
functionPreviewKey.value = uuidv4()
|
||||
}
|
||||
}
|
||||
|
||||
const validateFunction = () => {
|
||||
if ( !functionValue.value ) {
|
||||
return 'Missing function declaration'
|
||||
}
|
||||
|
||||
const stmt = MathStatement.temp(functionValue.value)
|
||||
if ( !stmt.isValid() || !stmt.isFunctionDeclaration() ) {
|
||||
return 'Function definition is invalid'
|
||||
}
|
||||
|
||||
if ( !stmt.isNotRecursive() ) {
|
||||
return 'A function may not reference itself'
|
||||
}
|
||||
}
|
||||
|
||||
const saveFunction = () => {
|
||||
functionError.value = validateFunction()
|
||||
if ( functionError.value ) {
|
||||
return
|
||||
}
|
||||
|
||||
if ( props.statement ) {
|
||||
props.statement.raw = functionValue.value
|
||||
emit('save', props.statement)
|
||||
} else {
|
||||
emit('save', new MathStatement(
|
||||
uuidv4() as StatementID,
|
||||
functionValue.value
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if ( props.statement ) {
|
||||
functionValue.value = props.statement.raw
|
||||
updateFunctionPreview()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div v-if="functionValue" style="display: flex; justify-content: center">
|
||||
<Katex
|
||||
:key="functionPreviewKey"
|
||||
:statement="functionPreview"
|
||||
/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="functionError">
|
||||
<div style="color: darkred; font-weight: bold">{{ functionError }}</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-model="functionValue"
|
||||
v-on:update:model-value="() => updateFunctionPreview()"
|
||||
outlined
|
||||
type="textarea"
|
||||
autogrow
|
||||
label="Value"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right" class="text-primary">
|
||||
<q-btn flat label="Cancel" v-close-popup></q-btn>
|
||||
<q-btn flat label="Save" @click="() => saveFunction()"></q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</template>
|
@ -1,73 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { ImageContainer } from "../support/types";
|
||||
import { stepX, stepY } from "../support/const";
|
||||
const props = defineProps<{ value: ImageContainer }>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(eventName: 'move', x: number, y: number): void,
|
||||
(eventName: 'edit',): void,
|
||||
(eventName: 'remove',): void,
|
||||
|
||||
}>()
|
||||
|
||||
|
||||
function onControlledDrag(e: { event: MouseEvent, data: { x: number, y: number } }) {
|
||||
|
||||
// const x = e.x;
|
||||
// const y = e.y;
|
||||
const { x, y } = e.data;
|
||||
props.value.x = x;
|
||||
props.value.y = y;
|
||||
console.log(e)
|
||||
}
|
||||
function onControlledDragStop(e: { event: MouseEvent, data: { x: number, y: number } }) {
|
||||
// console.log(typeof(e))
|
||||
const { x, y } = e.data;
|
||||
// const x = e.x;
|
||||
// const y = e.y;
|
||||
console.log(self)
|
||||
emit('move', x, y);
|
||||
onControlledDrag(e);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Draggable
|
||||
:grid="[stepX, stepY]"
|
||||
:default-position="{ x: props.value.x, y: props.value.y }"
|
||||
:position="{ x: props.value.x, y: props.value.y }"
|
||||
@stop="onControlledDragStop"
|
||||
>
|
||||
<div>
|
||||
<q-card flat bordered>
|
||||
<q-card-section style="padding: 0">
|
||||
<div class="row items-center no-wrap">
|
||||
<q-card-section>
|
||||
<q-img :src="props.value.url" style="min-width: 200px"></q-img>
|
||||
</q-card-section>
|
||||
<div class="col-auto">
|
||||
<q-btn color="grey-7" round flat icon="more_vert">
|
||||
<q-menu cover auto-close>
|
||||
<q-list>
|
||||
<q-item clickable>
|
||||
<q-item-section @click="() => $emit('edit')">Edit</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable>
|
||||
<q-item-section @click="() => $emit('remove')">Remove</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</Draggable>
|
||||
</template>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
|
||||
</style>
|
@ -1,126 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import validator from 'validator'
|
||||
import { checkLoggedIn, loggedIn } from '../support/auth'
|
||||
import router from '../router'
|
||||
const name = ref<string>('')
|
||||
const email = ref<string>('')
|
||||
const password = ref<string>('')
|
||||
const passwordConfirm = ref<string>('')
|
||||
const btnDisabled = ref<boolean>(true)
|
||||
const isRegistration = ref<boolean>(false)
|
||||
const error = ref<string>('')
|
||||
|
||||
const checkForm = () => {
|
||||
btnDisabled.value =
|
||||
(isRegistration.value
|
||||
&& (password.value != passwordConfirm.value
|
||||
|| name.value.length == 0))
|
||||
|| password.value.length < 8
|
||||
|| email.value.length == 0
|
||||
|| !validator.isEmail(email.value);
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
if (isRegistration.value == true) {
|
||||
error.value = await register()
|
||||
} else {
|
||||
error.value = await login()
|
||||
}
|
||||
}
|
||||
|
||||
const login = async () => {
|
||||
const response = await fetch('/api/login/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email: email.value, password: password.value })
|
||||
})
|
||||
|
||||
const user = (await response.json()) as unknown as any
|
||||
if (!user.success) {
|
||||
return user.message
|
||||
}
|
||||
loggedIn()
|
||||
router.push({ path: '/Listings'})
|
||||
}
|
||||
|
||||
const register = async () => {
|
||||
const response = await fetch('/api/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ name:name.value, email:email.value, password:password.value })
|
||||
})
|
||||
const user = (await response.json()) as unknown as any
|
||||
if (!user.success) {
|
||||
return user.message
|
||||
}
|
||||
loggedIn()
|
||||
router.push({ path: '/Listings'})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if ( checkLoggedIn() ) {
|
||||
router.push({ path: '/Listings'})
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-card-section v-if="!isRegistration">
|
||||
<q-form class="q-gutter-md">
|
||||
<q-input v-model="email" v-on:keyup="checkForm()" type="email" label="email" />
|
||||
<q-input v-model="password" v-on:keyup="checkForm()" type="password" label="password" />
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
<q-card-section v-else>
|
||||
<q-form class="q-gutter-md">
|
||||
<q-input v-model="name" v-on:keyup="checkForm()" type="text" label="name" />
|
||||
<q-input v-model="email" v-on:keyup="checkForm()" type="email" label="email" />
|
||||
<q-input v-model="password" v-on:keyup="checkForm()" type="password" label="password" />
|
||||
<q-input
|
||||
v-model="passwordConfirm"
|
||||
v-on:keyup="checkForm()"
|
||||
type="password"
|
||||
label="password confirmation"
|
||||
/>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
<q-card-section v-if="error">
|
||||
<p class="error">
|
||||
<b>{{ error }}</b>
|
||||
</p>
|
||||
</q-card-section>
|
||||
<q-card-actions class="q-px-md">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
size="lg"
|
||||
:disable="btnDisabled"
|
||||
class="full-width"
|
||||
:label="isRegistration ? 'Register' : 'Login'"
|
||||
@click="submit()"
|
||||
/>
|
||||
</q-card-actions>
|
||||
<q-card-section class="text-center q-pa-none">
|
||||
<p class="text-grey-6">
|
||||
{{ isRegistration ? 'Have an account?' : 'Not registered?' }}
|
||||
<a
|
||||
href="#"
|
||||
@click="isRegistration = !isRegistration; checkForm()"
|
||||
>{{ isRegistration ? 'Login' : 'Create an Account' }}</a>
|
||||
</p>
|
||||
</q-card-section>
|
||||
</template>
|
||||
<style>
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,160 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import * as math from 'mathjs'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { LineChart } from 'vue-chart-3'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Chart, ChartData, registerables } from 'chart.js'
|
||||
import { MathStatement } from '../support/parse'
|
||||
import { ChartBox } from '../support/types'
|
||||
import { stepX, stepY } from '../support/const'
|
||||
import {MathPage} from '../support/page'
|
||||
|
||||
Chart.register(...registerables)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(eventName: 'move', x: number, y: number): void,
|
||||
(eventName: 'edit'): void,
|
||||
(eventName: 'remove'): void,
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
page: MathPage,
|
||||
fn: MathStatement,
|
||||
value: ChartBox,
|
||||
}>()
|
||||
const options = ref({
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
color: "white",
|
||||
font: {
|
||||
size: 18
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
ticks: {
|
||||
color: "white",
|
||||
font: {
|
||||
size: 15,
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
color: '#ccc'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
ticks: {
|
||||
color: "white",
|
||||
font: {
|
||||
size: 14
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
color: '#ccc'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const getChartData = (): ChartData<'line'> => {
|
||||
const range = []
|
||||
const min = Math.min(props.value.minX, props.value.maxX)
|
||||
const max = Math.max(props.value.minX, props.value.maxX)
|
||||
for (let i = min; i <= max; i += parseFloat(String(props.value.stepX)) || 1) {
|
||||
range.push(i)
|
||||
}
|
||||
|
||||
if (!props.fn.isFunctionDeclaration()) {
|
||||
throw new TypeError('Cannot chart node that is not a function.')
|
||||
}
|
||||
|
||||
try {
|
||||
const evaluationResult = props.page.evaluate()
|
||||
const node = props.fn.parse() as math.FunctionAssignmentNode
|
||||
const fn = node.compile().evaluate(evaluationResult.scope)
|
||||
|
||||
return {
|
||||
labels: range.map(x => `${x}`),
|
||||
datasets: [{
|
||||
label: node.name,
|
||||
backgroundColor: '#553564',
|
||||
borderColor: '#ccc',
|
||||
data: range.map(x => fn(x)),
|
||||
pointRadius: 5
|
||||
}],
|
||||
}
|
||||
} catch (_) {
|
||||
return {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: '',
|
||||
backgroundColor: '#553564',
|
||||
borderColor: '#ccc',
|
||||
data: [],
|
||||
pointRadius: 5
|
||||
}],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const chartData = ref(getChartData())
|
||||
const chartKey = ref(uuidv4())
|
||||
computed(() => {
|
||||
chartData.value = getChartData()
|
||||
chartKey.value = uuidv4()
|
||||
})
|
||||
|
||||
function onControlledDrag(e: { event: MouseEvent, data: { x: number, y: number } }) {
|
||||
|
||||
// const x = e.x;
|
||||
// const y = e.y;
|
||||
const { x, y } = e.data;
|
||||
props.value.x = x;
|
||||
props.value.y = y;
|
||||
console.log(e)
|
||||
}
|
||||
function onControlledDragStop(e: { event: MouseEvent, data: { x: number, y: number } }) {
|
||||
// console.log(typeof(e))
|
||||
const { x, y } = e.data;
|
||||
// const x = e.x;
|
||||
// const y = e.y;
|
||||
console.log(self)
|
||||
emit('move', x, y);
|
||||
onControlledDrag(e);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Draggable
|
||||
:grid="[stepX, stepY]"
|
||||
:default-position="{ x: props.value.x, y: props.value.y }"
|
||||
:position="{ x: props.value.x, y: props.value.y }"
|
||||
@stop="onControlledDragStop"
|
||||
>
|
||||
<div
|
||||
style="background: var(--q-dark); display: flex; flex-direction: row; border: 1px solid #ccc; border-radius: 3px;"
|
||||
>
|
||||
<LineChart :options="options" :chartData="chartData" :key="chartKey" class="inner-chart" />
|
||||
<div class="sidebar">
|
||||
<q-btn color="grey-7" round flat icon="more_vert">
|
||||
<q-menu cover auto-close>
|
||||
<q-list>
|
||||
<q-item clickable>
|
||||
<q-item-section @click="() => $emit('edit')">Edit</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable>
|
||||
<q-item-section @click="() => $emit('remove')">Remove</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</Draggable>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
@ -1,118 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import * as math from 'mathjs'
|
||||
import {onMounted, ref} from 'vue'
|
||||
import {MathStatement} from '../support/parse'
|
||||
import {v4 as uuidv4} from 'uuid'
|
||||
import Katex from './Katex.vue'
|
||||
import {ChartBox, StatementID} from '../support/types'
|
||||
import {MathPage} from '../support/page'
|
||||
|
||||
const props = defineProps<{
|
||||
page: MathPage,
|
||||
chartBox?: ChartBox,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(eventName: 'save', statement: ChartBox): void,
|
||||
}>()
|
||||
|
||||
const chartBoxModalError = ref<string|undefined>(undefined)
|
||||
const chartBoxFunctionName = ref('')
|
||||
const chartBoxMinXValue = ref(0)
|
||||
const chartBoxMaxXValue = ref(30)
|
||||
const chartBoxStepXValue = ref(1)
|
||||
|
||||
const validateChartBox = () => {
|
||||
if ( !chartBoxFunctionName.value ) {
|
||||
return 'Missing function name'
|
||||
}
|
||||
|
||||
if ( isNaN(parseFloat(String(chartBoxMinXValue.value))) ) {
|
||||
return 'Invalid minimum X-value'
|
||||
}
|
||||
|
||||
if ( isNaN(parseFloat(String(chartBoxMaxXValue.value))) ) {
|
||||
return 'Invalid maximum X-value'
|
||||
}
|
||||
|
||||
if ( isNaN(parseFloat(String(chartBoxStepXValue.value))) ) {
|
||||
return 'Invalid X-value step size'
|
||||
}
|
||||
|
||||
if ( !props.page.getFunctionByName(chartBoxFunctionName.value) ) {
|
||||
return 'Invalid function name'
|
||||
}
|
||||
}
|
||||
|
||||
const saveChartBox = () => {
|
||||
chartBoxModalError.value = validateChartBox()
|
||||
if ( chartBoxModalError.value ) {
|
||||
return
|
||||
}
|
||||
|
||||
if ( props.chartBox ) {
|
||||
props.chartBox.fnName = chartBoxFunctionName.value
|
||||
props.chartBox.minX = chartBoxMinXValue.value
|
||||
props.chartBox.maxX = chartBoxMaxXValue.value
|
||||
props.chartBox.stepX = chartBoxStepXValue.value
|
||||
emit('save', props.chartBox)
|
||||
} else {
|
||||
emit('save', new ChartBox(
|
||||
chartBoxFunctionName.value,
|
||||
chartBoxMinXValue.value,
|
||||
chartBoxMaxXValue.value,
|
||||
chartBoxStepXValue.value,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if ( props.chartBox ) {
|
||||
chartBoxFunctionName.value = props.chartBox.fnName
|
||||
chartBoxMinXValue.value = props.chartBox.minX
|
||||
chartBoxMaxXValue.value = props.chartBox.maxX
|
||||
chartBoxStepXValue.value = props.chartBox.stepX
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-card>
|
||||
<q-card-section v-if="chartBoxModalError">
|
||||
<div style="color: darkred; font-weight: bold">{{ chartBoxModalError }}</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-model="chartBoxFunctionName"
|
||||
outlined
|
||||
autofocus
|
||||
label="Function name"
|
||||
/>
|
||||
<br>
|
||||
<q-input
|
||||
v-model="chartBoxMinXValue"
|
||||
outlined
|
||||
autofocus
|
||||
label="Minimum X-value"
|
||||
/>
|
||||
<br>
|
||||
<q-input
|
||||
v-model="chartBoxMaxXValue"
|
||||
outlined
|
||||
autofocus
|
||||
label="Maximum X-value"
|
||||
/>
|
||||
<br>
|
||||
<q-input
|
||||
v-model="chartBoxStepXValue"
|
||||
outlined
|
||||
autofocus
|
||||
label="X-value step size"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right" class="text-primary">
|
||||
<q-btn flat label="Cancel" v-close-popup></q-btn>
|
||||
<q-btn flat label="Save" @click="() => saveChartBox()"></q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</template>
|
@ -1,2 +0,0 @@
|
||||
|
||||
|
@ -1,46 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import Login from '../components/Login.vue'
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const show = ref<boolean>(true)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-layout>
|
||||
<h1>{{ msg }}</h1>
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated fadeInUp"
|
||||
leave-active-class="animated fadeOutUp"
|
||||
mode="out-in"
|
||||
>
|
||||
<q-btn color="primary" text-color="white" @click="show = !show" v-if="show">Start</q-btn>
|
||||
<div class="login" v-else>
|
||||
<q-card square bordered class="q-pa-lg shadow-1">
|
||||
<Login />
|
||||
</q-card>
|
||||
</div>
|
||||
</transition>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
background-color: #4484c4;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 15px 32px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 32px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.login {
|
||||
max-width: 30em;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
@ -1,66 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from 'vue'
|
||||
import router from '../router'
|
||||
|
||||
interface PageRecord {
|
||||
pageId: number|string,
|
||||
title: string,
|
||||
}
|
||||
|
||||
const pages = ref<PageRecord[]>([])
|
||||
const columns = ref([
|
||||
{
|
||||
name: 'title',
|
||||
field: 'title',
|
||||
label: 'Title',
|
||||
},
|
||||
])
|
||||
|
||||
const loadPages = async () => {
|
||||
const response = await fetch(`/api/editor/pages`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
const result = (await response.json()) as unknown as any
|
||||
if ( !result.success ) {
|
||||
alert('Unable to load pages: ' + result?.message)
|
||||
}
|
||||
|
||||
pages.value = result.data.records.map((row: any) => {
|
||||
return {
|
||||
pageId: row.pageId,
|
||||
title: JSON.parse(row.serialData).title || 'New Page',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(loadPages)
|
||||
|
||||
const onRowClick = (_: unknown, row: PageRecord) => {
|
||||
router.push({ path: `/Editor/${row.pageId}` })
|
||||
}
|
||||
|
||||
const onNewPageClick = () => {
|
||||
router.push('/Editor')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="width: 100%">
|
||||
<q-btn icon="add" style="margin-bottom: 10px" @click="onNewPageClick">New Page</q-btn>
|
||||
<q-table
|
||||
style="width: 100%"
|
||||
title="My Pages"
|
||||
:columns="columns"
|
||||
:rows="pages"
|
||||
row-key="pageId"
|
||||
hide-bottom
|
||||
:pagination="{ rowsPerPage: 10000 }"
|
||||
@row-click="onRowClick"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useAuth0 } from '@auth0/auth0-vue';
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const { loginWithRedirect } = useAuth0();
|
||||
|
||||
const login = () => {
|
||||
loginWithRedirect();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<button @click="login">Log in</button>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
background-color: #215b8a; /* Green */
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 15px 32px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 32px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
@ -1,12 +1,10 @@
|
||||
$primary : #553564
|
||||
$secondary : #008e80
|
||||
$primary : #1976D2
|
||||
$secondary : #26A69A
|
||||
$accent : #9C27B0
|
||||
|
||||
$dark : #1D1D1D
|
||||
// $dark-page: #333333;
|
||||
|
||||
|
||||
$positive : #21BA45
|
||||
$negative : #C10015
|
||||
$info : #7da9b2
|
||||
$info : #31CCEC
|
||||
$warning : #F2C037
|
@ -1,10 +0,0 @@
|
||||
let isLoggedIn = false
|
||||
export const checkLoggedIn = () => {
|
||||
return isLoggedIn
|
||||
}
|
||||
export const loggedIn = () => {
|
||||
isLoggedIn = true
|
||||
}
|
||||
export const loggedOut = () => {
|
||||
isLoggedIn = false
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export const stepX = 5
|
||||
export const stepY = 5
|
Loading…
Reference in new issue