diff --git a/assets/css/states.css b/assets/css/states.css
index cba281a..596d88f 100755
--- a/assets/css/states.css
+++ b/assets/css/states.css
@@ -61,3 +61,7 @@
.noshrink {
flex-shrink: 0;
}
+
+.capitalize {
+ text-transform: capitalize;
+}
diff --git a/assets/css/theme.css b/assets/css/theme.css
index 22aa4c1..88e1fa3 100644
--- a/assets/css/theme.css
+++ b/assets/css/theme.css
@@ -81,6 +81,9 @@
--success: #5c2;
--notice: #25c;
+ --approve: #3a1;
+ --reject: #a22;
+
--gold: #d5b522;
}
diff --git a/assets/img/icons/bubble-blocked.svg b/assets/img/icons/bubble-blocked.svg
new file mode 100755
index 0000000..790d466
--- /dev/null
+++ b/assets/img/icons/bubble-blocked.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/img/icons/bubble-cancel.svg b/assets/img/icons/bubble-cancel.svg
new file mode 100755
index 0000000..a4e20d9
--- /dev/null
+++ b/assets/img/icons/bubble-cancel.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/img/icons/bubble-check.svg b/assets/img/icons/bubble-check.svg
new file mode 100755
index 0000000..54bf80b
--- /dev/null
+++ b/assets/img/icons/bubble-check.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/img/icons/bubble-lines3.svg b/assets/img/icons/bubble-lines3.svg
new file mode 100755
index 0000000..635aab0
--- /dev/null
+++ b/assets/img/icons/bubble-lines3.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/img/icons/bubble-link.svg b/assets/img/icons/bubble-link.svg
new file mode 100755
index 0000000..700414a
--- /dev/null
+++ b/assets/img/icons/bubble-link.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/img/icons/bubble-notification2.svg b/assets/img/icons/bubble-notification2.svg
new file mode 100755
index 0000000..ce6e5b3
--- /dev/null
+++ b/assets/img/icons/bubble-notification2.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/img/icons/bubble-plus.svg b/assets/img/icons/bubble-plus.svg
new file mode 100755
index 0000000..9d91556
--- /dev/null
+++ b/assets/img/icons/bubble-plus.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/img/icons/hammer2.svg b/assets/img/icons/hammer2.svg
new file mode 100755
index 0000000..43b6463
--- /dev/null
+++ b/assets/img/icons/hammer2.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/img/icons/hand.svg b/assets/img/icons/hand.svg
new file mode 100755
index 0000000..778dc07
--- /dev/null
+++ b/assets/img/icons/hand.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/img/icons/spam.svg b/assets/img/icons/spam.svg
new file mode 100755
index 0000000..73ccb6c
--- /dev/null
+++ b/assets/img/icons/spam.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/img/icons/user-block.svg b/assets/img/icons/user-block.svg
new file mode 100755
index 0000000..25b2e44
--- /dev/null
+++ b/assets/img/icons/user-block.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/assets/img/icons/user-cancel.svg b/assets/img/icons/user-cancel.svg
new file mode 100755
index 0000000..320ba94
--- /dev/null
+++ b/assets/img/icons/user-cancel.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/assets/img/icons/user-check.svg b/assets/img/icons/user-check.svg
new file mode 100755
index 0000000..df273b4
--- /dev/null
+++ b/assets/img/icons/user-check.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/assets/img/icons/user-lock.svg b/assets/img/icons/user-lock.svg
new file mode 100755
index 0000000..4f67f24
--- /dev/null
+++ b/assets/img/icons/user-lock.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/assets/img/icons/user-minus.svg b/assets/img/icons/user-minus.svg
new file mode 100755
index 0000000..5f98fcb
--- /dev/null
+++ b/assets/img/icons/user-minus.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/assets/img/icons/user-plus.svg b/assets/img/icons/user-plus.svg
new file mode 100755
index 0000000..6798cb1
--- /dev/null
+++ b/assets/img/icons/user-plus.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/components/actors/bio.vue b/components/actors/bio.vue
index 5ba1dcb..f04fe5b 100644
--- a/components/actors/bio.vue
+++ b/components/actors/bio.vue
@@ -8,7 +8,7 @@
class="avatar-container"
>
@@ -296,7 +296,7 @@
+
+
diff --git a/components/edit/actors.vue b/components/edit/actors.vue
index 37c52ed..932afe8 100644
--- a/components/edit/actors.vue
+++ b/components/edit/actors.vue
@@ -85,11 +85,13 @@ watch(() => props.scene, () => { newActors.value = []; });
&.disabled {
.actor {
- color: var(--shadow);
+ background: var(--glass-weak-50);
+ color: var(--glass-strong-10);
.remove,
.add {
- background: var(--shadow-weak-40);
+ fill: var(--shadow-weak-30);
+ background: var(--shadow-weak-50);
}
}
@@ -103,9 +105,14 @@ watch(() => props.scene, () => { newActors.value = []; });
align-items: center;
margin-left: .25rem;
+ &:hover {
+ box-shadow: 0 0 3px var(--shadow-weak-20);
+ }
+
.icon {
height: 100%;
padding: 0 .5rem;
+ background: var(--success);
fill: var(--text-light);
}
}
@@ -114,8 +121,9 @@ watch(() => props.scene, () => { newActors.value = []; });
.actor {
display: flex;
align-items: stretch;
- background: var(--glass-weak-30);
border-radius: .25rem;
+ background: var(--background);
+ box-shadow: 0 0 3px var(--shadow-weak-30);
&.deleted {
color: var(--glass);
@@ -129,8 +137,8 @@ watch(() => props.scene, () => { newActors.value = []; });
.add {
height: auto;
padding: .25rem .3rem;
- fill: var(--highlight-strong-10);
border-radius: .25rem;
+ fill: var(--highlight-strong-10);
&:hover {
fill: var(--text-light);
@@ -139,11 +147,19 @@ watch(() => props.scene, () => { newActors.value = []; });
}
.remove {
- background: var(--error);
+ fill: var(--error);
+
+ &:hover {
+ background: var(--error);
+ }
}
.add {
- background: var(--success);
+ fill: var(--success);
+
+ &:hover {
+ background: var(--success);
+ }
}
}
diff --git a/components/edit/movies.vue b/components/edit/movies.vue
new file mode 100644
index 0000000..e4755a8
--- /dev/null
+++ b/components/edit/movies.vue
@@ -0,0 +1,173 @@
+
+
+ -
+ {{ movie.title }}
+
+
+
+ movieId !== movie.id))"
+ />
+
+
+ -
+
+
+
+
+
+
+
+
+
+
diff --git a/components/edit/revisions.vue b/components/edit/revisions.vue
new file mode 100644
index 0000000..67c476a
--- /dev/null
+++ b/components/edit/revisions.vue
@@ -0,0 +1,488 @@
+
+
+
+
+
+ -
+
+
+
+ -
+ {{ delta.key }}
+
+
+
+ [
+ - {{ item.name || item.id || item }}
]
+
+
+ {{ format(rev.base[delta.key], 'yyyy-MM-dd hh:mm') }}
+ {{ rev.base[delta.key] }}
+
+
+
⇒
+
+
+ [
+ - {{ item.name || item.id || item }}
]
+
+
+ {{ format(delta.value, 'yyyy-MM-dd hh:mm') }}
+ {{ delta.value }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/edit/tags.vue b/components/edit/tags.vue
index 43eb3eb..65339a1 100644
--- a/components/edit/tags.vue
+++ b/components/edit/tags.vue
@@ -85,11 +85,13 @@ watch(() => props.scene, () => { newTags.value = []; });
&.disabled {
.tag {
- color: var(--shadow);
+ background: var(--glass-weak-50);
+ color: var(--glass-strong-10);
.remove,
.add {
- background: var(--shadow-weak-40);
+ fill: var(--shadow-weak-30);
+ background: var(--shadow-weak-50);
}
}
@@ -103,9 +105,14 @@ watch(() => props.scene, () => { newTags.value = []; });
align-items: center;
margin-left: .25rem;
+ &:hover {
+ box-shadow: 0 0 3px var(--shadow-weak-20);
+ }
+
.icon {
height: 100%;
padding: 0 .5rem;
+ background: var(--success);
fill: var(--text-light);
}
}
@@ -114,8 +121,9 @@ watch(() => props.scene, () => { newTags.value = []; });
.tag {
display: flex;
align-items: stretch;
- background: var(--glass-weak-30);
border-radius: .25rem;
+ background: var(--background);
+ box-shadow: 0 0 3px var(--shadow-weak-30);
&.deleted {
color: var(--glass);
@@ -129,8 +137,8 @@ watch(() => props.scene, () => { newTags.value = []; });
.add {
height: auto;
padding: .25rem .3rem;
- fill: var(--highlight-strong-10);
border-radius: .25rem;
+ fill: var(--highlight-strong-10);
&:hover {
fill: var(--text-light);
@@ -139,11 +147,19 @@ watch(() => props.scene, () => { newTags.value = []; });
}
.remove {
- background: var(--error);
+ fill: var(--error);
+
+ &:hover {
+ background: var(--error);
+ }
}
.add {
- background: var(--success);
+ fill: var(--success);
+
+ &:hover {
+ background: var(--success);
+ }
}
}
diff --git a/components/form/checkbox.vue b/components/form/checkbox.vue
index 5b26446..d317385 100755
--- a/components/form/checkbox.vue
+++ b/components/form/checkbox.vue
@@ -11,6 +11,7 @@
:checked="checked"
type="checkbox"
class="check-checkbox"
+ :disabled="disabled"
@change="$emit('change', $event.target.checked)"
>
@@ -33,6 +34,10 @@ defineProps({
type: String,
default: null,
},
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
});
defineEmits(['change']);
@@ -98,6 +103,10 @@ defineEmits(['change']);
}
}
+.check-checkbox:disabled + .check {
+ background: var(--shadow);
+}
+
.check-container.minus .check-checkbox:checked + .check {
background: var(--error);
@@ -108,7 +117,6 @@ defineEmits(['change']);
.check-label {
overflow: hidden;
- text-transform: capitalize;
text-overflow: ellipsis;
margin: 0 .5rem 0 0;
}
diff --git a/components/header/header.vue b/components/header/header.vue
index aaf6d1d..6b91741 100644
--- a/components/header/header.vue
+++ b/components/header/header.vue
@@ -189,6 +189,20 @@
Settings
+
+
+
+
+
+
+
+
+
+
+ - {{ movie.title }} ({{ [format(movie.effectiveDate, 'yyyy')].filter(Boolean).join(', ') }})
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/default.cjs b/config/default.cjs
index 6e919fd..4f6d127 100755
--- a/config/default.cjs
+++ b/config/default.cjs
@@ -63,6 +63,9 @@ module.exports = {
usernameLength: [2, 24],
usernamePattern: /^[a-zA-Z0-9_-]+$/,
},
+ bans: {
+ defaultExpiry: 60 * 24 * 3, // in minutes, 3 days
+ },
apiAccess: {
graphqlEnabled: true,
keySize: 24, // bytes
diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs
index 7f3a78f..846f10d 100644
--- a/ecosystem.config.cjs
+++ b/ecosystem.config.cjs
@@ -3,7 +3,7 @@
module.exports = {
apps: [
{
- name: 'newtraxxx',
+ name: 'traxxx',
script: 'npm',
args: 'run server:prod',
exec_mode: 'cluster',
diff --git a/package-lock.json b/package-lock.json
index 5068a9b..81e82b4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -43,6 +43,7 @@
"manticoresearch": "^4.0.0",
"markdown-it": "^14.0.0",
"mathjs": "^12.2.1",
+ "merkle-json": "^2.6.0",
"mitt": "^3.0.1",
"mysql": "^2.18.1",
"nanoid": "^5.0.4",
@@ -7324,6 +7325,17 @@
"node": ">= 8"
}
},
+ "node_modules/merkle-json": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/merkle-json/-/merkle-json-2.6.0.tgz",
+ "integrity": "sha512-sJM+SNINn3/5GzFyY8MMCj+647UbDVcZv3wcynX1vv9Vhnm1gWGI5ZPOA+EYm3iInITyQHKnmcpYKqZkeY+iAQ==",
+ "dependencies": {
+ "merkle-json": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6.11.0"
+ }
+ },
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -15647,6 +15659,14 @@
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="
},
+ "merkle-json": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/merkle-json/-/merkle-json-2.6.0.tgz",
+ "integrity": "sha512-sJM+SNINn3/5GzFyY8MMCj+647UbDVcZv3wcynX1vv9Vhnm1gWGI5ZPOA+EYm3iInITyQHKnmcpYKqZkeY+iAQ==",
+ "requires": {
+ "merkle-json": "^2.1.0"
+ }
+ },
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
diff --git a/package.json b/package.json
index eb9f69c..a87ab54 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
"manticoresearch": "^4.0.0",
"markdown-it": "^14.0.0",
"mathjs": "^12.2.1",
+ "merkle-json": "^2.6.0",
"mitt": "^3.0.1",
"mysql": "^2.18.1",
"nanoid": "^5.0.4",
diff --git a/pages/admin/+Page.vue b/pages/admin/+Page.vue
new file mode 100644
index 0000000..e81d112
--- /dev/null
+++ b/pages/admin/+Page.vue
@@ -0,0 +1,9 @@
+
+
+ Admin Panel
+
+
+
+
diff --git a/pages/admin/+onBeforeRender.js b/pages/admin/+onBeforeRender.js
new file mode 100644
index 0000000..469e839
--- /dev/null
+++ b/pages/admin/+onBeforeRender.js
@@ -0,0 +1,13 @@
+import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
+
+export function onBeforeRender(pageContext) {
+ if (pageContext.user.role === 'user') {
+ throw render(404);
+ }
+
+ return {
+ pageContext: {
+ title: pageContext.routeParams.section,
+ },
+ };
+}
diff --git a/pages/admin/revisions/+Page.vue b/pages/admin/revisions/+Page.vue
new file mode 100644
index 0000000..bb1a79f
--- /dev/null
+++ b/pages/admin/revisions/+Page.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/pages/admin/revisions/+onBeforeRender.js b/pages/admin/revisions/+onBeforeRender.js
new file mode 100644
index 0000000..87e97fb
--- /dev/null
+++ b/pages/admin/revisions/+onBeforeRender.js
@@ -0,0 +1,30 @@
+import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
+import { fetchSceneRevisions } from '#/src/scenes.js';
+
+export async function onBeforeRender(pageContext) {
+ if (!pageContext.user || pageContext.user.role === 'user') {
+ throw render(404);
+ }
+
+ const {
+ revisions,
+ actors,
+ tags,
+ movies,
+ } = await fetchSceneRevisions(null, {
+ isFinalized: false,
+ limit: 50,
+ }, pageContext.user);
+
+ return {
+ pageContext: {
+ title: pageContext.routeParams.section,
+ pageProps: {
+ revisions,
+ actors,
+ tags,
+ movies,
+ },
+ },
+ };
+}
diff --git a/pages/admin/revisions/+route.js b/pages/admin/revisions/+route.js
new file mode 100644
index 0000000..d9cb684
--- /dev/null
+++ b/pages/admin/revisions/+route.js
@@ -0,0 +1 @@
+export default '/admin/@section/*';
diff --git a/pages/scene/+Page.vue b/pages/scene/+Page.vue
index b426735..efa9f96 100644
--- a/pages/scene/+Page.vue
+++ b/pages/scene/+Page.vue
@@ -286,7 +286,7 @@
v-if="user"
class="icon-link"
target="_blank"
- :href="`/user/${user.username}/summaries?t=${selectedTemplate}`"
+ :href="`/user/${user.username}/templates?t=${selectedTemplate}`"
>
{{ userTemplate.name }}
+
+
@@ -670,6 +687,13 @@ function copySummary() {
}
}
+.scene-actions {
+ display: flex;
+ justify-content: center;
+ gap: 2rem;
+ margin-top: 1rem;
+}
+
.icon-link {
display: flex;
height: auto;
diff --git a/pages/scene/edit/+Page.vue b/pages/scene/edit/+Page.vue
index 6baf09d..3a91bed 100644
--- a/pages/scene/edit/+Page.vue
+++ b/pages/scene/edit/+Page.vue
@@ -1,6 +1,40 @@
-
@@ -147,8 +201,13 @@ import { format } from 'date-fns';
import EditActors from '#/components/edit/actors.vue';
import EditTags from '#/components/edit/tags.vue';
+import EditMovies from '#/components/edit/movies.vue';
+import Checkbox from '#/components/form/checkbox.vue';
-import { get, patch } from '#/src/api.js';
+import {
+ // get,
+ post,
+} from '#/src/api.js';
const pageContext = inject('pageContext');
@@ -168,6 +227,11 @@ const fields = computed(() => [
type: 'tags',
value: scene.value.tags,
},
+ {
+ key: 'movies',
+ type: 'movies',
+ value: scene.value.movies,
+ },
{
key: 'title',
type: 'string',
@@ -211,6 +275,8 @@ const fields = computed(() => [
const editing = ref(new Set());
const edits = ref({});
const comment = ref(null);
+const apply = ref(user.role !== 'user');
+const submitted = ref(false);
function toggleField(item) {
if (editing.value.has(item.key)) {
@@ -219,6 +285,7 @@ function toggleField(item) {
return;
}
+
editing.value.add(item.key);
if (Array.isArray(item.value)) {
@@ -231,6 +298,8 @@ function toggleField(item) {
function setValue(item, event) {
edits.value[item.key] = event.target.value;
+
+ console.log(edits.value);
}
const timeUnits = ['h', 'm', 's'];
@@ -241,7 +310,8 @@ function setDuration(unit, event) {
async function submit() {
try {
- await patch(`/scenes/${scene.value.id}`, {
+ await post('/revisions', {
+ sceneId: scene.value.id,
edits: {
...edits.value,
duration: edits.value.duration
@@ -249,6 +319,7 @@ async function submit() {
: undefined,
},
comment: comment.value,
+ apply: apply.value,
}, {
successFeedback: 'Your revision has been submitted for approval.',
appendErrorMessage: true,
@@ -258,9 +329,9 @@ async function submit() {
edits.value = {};
comment.value = null;
- scene.value = await get(`/scenes/${scene.value.id}`);
+ submitted.value = true;
- console.log(scene.value);
+ // scene.value = await get(`/scenes/${scene.value.id}`);
} catch (error) {
// do nothing
}
@@ -270,6 +341,7 @@ async function submit() {
diff --git a/pages/scene/revisions/+onBeforeRender.js b/pages/scene/revisions/+onBeforeRender.js
new file mode 100644
index 0000000..b96d383
--- /dev/null
+++ b/pages/scene/revisions/+onBeforeRender.js
@@ -0,0 +1,34 @@
+import { fetchScenesById, fetchSceneRevisions } from '#/src/scenes.js';
+
+export async function onBeforeRender(pageContext) {
+ const [scene] = await fetchScenesById([Number(pageContext.routeParams.sceneId)], {
+ reqUser: pageContext.user,
+ includeAssets: true,
+ includePartOf: true,
+ actorStashes: true,
+ });
+
+ const {
+ revisions,
+ actors,
+ tags,
+ movies,
+ } = await fetchSceneRevisions(null, {
+ sceneId: scene.id,
+ isFinalized: true,
+ limit: 100,
+ }, pageContext.user);
+
+ return {
+ pageContext: {
+ title: `Revisions for '${scene.title}'`,
+ pageProps: {
+ scene,
+ revisions,
+ actors,
+ tags,
+ movies,
+ },
+ },
+ };
+}
diff --git a/pages/scene/revisions/+route.js b/pages/scene/revisions/+route.js
new file mode 100644
index 0000000..bc5568a
--- /dev/null
+++ b/pages/scene/revisions/+route.js
@@ -0,0 +1 @@
+export default '/scene/revisions/@sceneId/*';
diff --git a/pages/users/@username/+Page.vue b/pages/users/@username/+Page.vue
index 17b3e5f..bb53809 100644
--- a/pages/users/@username/+Page.vue
+++ b/pages/users/@username/+Page.vue
@@ -36,6 +36,12 @@
class="domain nolink"
:class="{ active: domain === 'templates' }"
>Templates
+
+
Revisions
@@ -46,6 +52,14 @@
:release="mockupRelease"
/>
+
+
+
+
+
@@ -56,6 +70,7 @@ import { formatDistanceStrict } from 'date-fns';
import Stashes from '#/components/stashes/stashes.vue';
import Alerts from '#/components/alerts/alerts.vue';
import Summaries from '#/components/scenes/summaries.vue';
+import Revisions from '#/components/edit/revisions.vue';
const pageContext = inject('pageContext');
const domain = pageContext.routeParams.domain;
@@ -125,8 +140,9 @@ const mockupRelease = {