Initial CRM workspace
This commit is contained in:
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
.nuxt
|
||||
.output
|
||||
.data
|
||||
dist
|
||||
coverage
|
||||
npm-debug.log*
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
|
||||
4
Actor/README.md
Normal file
4
Actor/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Actor
|
||||
|
||||
Placeholder for background actors/workers (async jobs, scheduled tasks, integrations).
|
||||
|
||||
2242
Frontend/app.vue
Normal file
2242
Frontend/app.vue
Normal file
File diff suppressed because it is too large
Load Diff
16
Frontend/assets/css/main.css
Normal file
16
Frontend/assets/css/main.css
Normal file
@@ -0,0 +1,16 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "daisyui";
|
||||
|
||||
:root {
|
||||
--color-accent: #1e6bff;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(circle at 100% 0%, rgba(30, 107, 255, 0.08), transparent 40%),
|
||||
radial-gradient(circle at 0% 100%, rgba(30, 107, 255, 0.08), transparent 40%),
|
||||
#f5f7fb;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
237
Frontend/components/ContactCollaborativeEditor.client.vue
Normal file
237
Frontend/components/ContactCollaborativeEditor.client.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { EditorContent, useEditor } from "@tiptap/vue-3";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import Collaboration from "@tiptap/extension-collaboration";
|
||||
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import * as Y from "yjs";
|
||||
import { WebrtcProvider } from "y-webrtc";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
room: string;
|
||||
placeholder?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "update:modelValue", value: string): void;
|
||||
}>();
|
||||
|
||||
const ydoc = new Y.Doc();
|
||||
const provider = new WebrtcProvider(props.room, ydoc);
|
||||
const isBootstrapped = ref(false);
|
||||
const awarenessVersion = ref(0);
|
||||
|
||||
const userPalette = ["#2563eb", "#0ea5e9", "#14b8a6", "#16a34a", "#eab308", "#f97316", "#ef4444"];
|
||||
const currentUser = {
|
||||
name: `You ${Math.floor(Math.random() * 900 + 100)}`,
|
||||
color: userPalette[Math.floor(Math.random() * userPalette.length)],
|
||||
};
|
||||
|
||||
function escapeHtml(value: string) {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function normalizeInitialContent(value: string) {
|
||||
const input = value.trim();
|
||||
if (!input) return "<p></p>";
|
||||
if (input.includes("<") && input.includes(">")) return value;
|
||||
|
||||
const blocks = value
|
||||
.replaceAll("\r\n", "\n")
|
||||
.split(/\n\n+/)
|
||||
.map((block) => `<p>${escapeHtml(block).replaceAll("\n", "<br />")}</p>`);
|
||||
|
||||
return blocks.join("");
|
||||
}
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
history: false,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: props.placeholder ?? "Type here...",
|
||||
includeChildren: true,
|
||||
}),
|
||||
Collaboration.configure({
|
||||
document: ydoc,
|
||||
field: "contact",
|
||||
}),
|
||||
CollaborationCursor.configure({
|
||||
provider,
|
||||
user: currentUser,
|
||||
}),
|
||||
],
|
||||
autofocus: true,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: "contact-editor-content",
|
||||
spellcheck: "true",
|
||||
},
|
||||
},
|
||||
onCreate: ({ editor: instance }) => {
|
||||
if (instance.isEmpty) {
|
||||
instance.commands.setContent(normalizeInitialContent(props.modelValue), false);
|
||||
}
|
||||
isBootstrapped.value = true;
|
||||
},
|
||||
onUpdate: ({ editor: instance }) => {
|
||||
emit("update:modelValue", instance.getHTML());
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(incoming) => {
|
||||
const instance = editor.value;
|
||||
if (!instance || !isBootstrapped.value) return;
|
||||
|
||||
const current = instance.getHTML();
|
||||
if (incoming === current || !incoming.trim()) return;
|
||||
|
||||
if (instance.isEmpty) {
|
||||
instance.commands.setContent(normalizeInitialContent(incoming), false);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const peerCount = computed(() => {
|
||||
awarenessVersion.value;
|
||||
const states = Array.from(provider.awareness.getStates().values());
|
||||
return states.length;
|
||||
});
|
||||
|
||||
const onAwarenessChange = () => {
|
||||
awarenessVersion.value += 1;
|
||||
};
|
||||
|
||||
provider.awareness.on("change", onAwarenessChange);
|
||||
|
||||
function runCommand(action: () => void) {
|
||||
const instance = editor.value;
|
||||
if (!instance) return;
|
||||
action();
|
||||
instance.commands.focus();
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
provider.awareness.off("change", onAwarenessChange);
|
||||
editor.value?.destroy();
|
||||
provider.destroy();
|
||||
ydoc.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 rounded-xl border border-base-300 bg-base-100 p-2">
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<button
|
||||
class="btn btn-xs"
|
||||
:class="editor?.isActive('bold') ? 'btn-primary' : 'btn-ghost'"
|
||||
@click="runCommand(() => editor?.chain().focus().toggleBold().run())"
|
||||
>
|
||||
B
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs"
|
||||
:class="editor?.isActive('italic') ? 'btn-primary' : 'btn-ghost'"
|
||||
@click="runCommand(() => editor?.chain().focus().toggleItalic().run())"
|
||||
>
|
||||
I
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs"
|
||||
:class="editor?.isActive('bulletList') ? 'btn-primary' : 'btn-ghost'"
|
||||
@click="runCommand(() => editor?.chain().focus().toggleBulletList().run())"
|
||||
>
|
||||
List
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs"
|
||||
:class="editor?.isActive('heading', { level: 2 }) ? 'btn-primary' : 'btn-ghost'"
|
||||
@click="runCommand(() => editor?.chain().focus().toggleHeading({ level: 2 }).run())"
|
||||
>
|
||||
H2
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs"
|
||||
:class="editor?.isActive('blockquote') ? 'btn-primary' : 'btn-ghost'"
|
||||
@click="runCommand(() => editor?.chain().focus().toggleBlockquote().run())"
|
||||
>
|
||||
Quote
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="px-1 text-xs text-base-content/60">Live: {{ peerCount }}</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-base-300 bg-base-100 p-2">
|
||||
<EditorContent :editor="editor" class="contact-editor min-h-[420px]" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.contact-editor :deep(.ProseMirror) {
|
||||
min-height: 390px;
|
||||
padding: 0.75rem;
|
||||
outline: none;
|
||||
line-height: 1.65;
|
||||
color: rgba(17, 24, 39, 0.95);
|
||||
}
|
||||
|
||||
.contact-editor :deep(.ProseMirror p) {
|
||||
margin: 0.45rem 0;
|
||||
}
|
||||
|
||||
.contact-editor :deep(.ProseMirror h1),
|
||||
.contact-editor :deep(.ProseMirror h2),
|
||||
.contact-editor :deep(.ProseMirror h3) {
|
||||
margin: 0.75rem 0 0.45rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.contact-editor :deep(.ProseMirror ul),
|
||||
.contact-editor :deep(.ProseMirror ol) {
|
||||
margin: 0.45rem 0;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.contact-editor :deep(.ProseMirror blockquote) {
|
||||
margin: 0.6rem 0;
|
||||
border-left: 3px solid rgba(30, 107, 255, 0.5);
|
||||
padding-left: 0.75rem;
|
||||
color: rgba(55, 65, 81, 0.95);
|
||||
}
|
||||
|
||||
.contact-editor :deep(.ProseMirror .collaboration-cursor__caret) {
|
||||
margin-left: -1px;
|
||||
margin-right: -1px;
|
||||
border-left: 1px solid currentColor;
|
||||
border-right: 1px solid currentColor;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.contact-editor :deep(.ProseMirror .collaboration-cursor__label) {
|
||||
position: absolute;
|
||||
top: -1.35em;
|
||||
left: -1px;
|
||||
border-radius: 4px;
|
||||
padding: 0.1rem 0.4rem;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
</style>
|
||||
11
Frontend/nuxt.config.ts
Normal file
11
Frontend/nuxt.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: "2025-07-15",
|
||||
devtools: { enabled: true },
|
||||
css: ["~/assets/css/main.css"],
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
});
|
||||
|
||||
11350
Frontend/package-lock.json
generated
Normal file
11350
Frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
Frontend/package.json
Normal file
26
Frontend/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "crm-frontend",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tiptap/extension-collaboration": "^2.27.2",
|
||||
"@tiptap/extension-collaboration-cursor": "^2.27.2",
|
||||
"@tiptap/extension-placeholder": "^2.27.2",
|
||||
"@tiptap/starter-kit": "^2.27.2",
|
||||
"@tiptap/vue-3": "^2.27.2",
|
||||
"daisyui": "^5.5.18",
|
||||
"nuxt": "^4.3.1",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vue": "^3.5.27",
|
||||
"y-webrtc": "^10.3.0",
|
||||
"yjs": "^13.6.29"
|
||||
}
|
||||
}
|
||||
20
README.md
Normal file
20
README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# CRM Draft
|
||||
|
||||
Minimal CRM draft focused on:
|
||||
- AI chat panel (left side)
|
||||
- Calendar + contacts core workflow (right side)
|
||||
- Additional tabs: chats, calls, feed, deals placeholder
|
||||
|
||||
## Project structure
|
||||
|
||||
- `Frontend` - Nuxt 4 + Tailwind 4 + DaisyUI UI app
|
||||
- `Actor` - placeholder for workers/automation
|
||||
- `Testflow` / `Testfow` - placeholder for test scenarios
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
cd Frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
4
Testflow/README.md
Normal file
4
Testflow/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Testflow
|
||||
|
||||
Placeholder for end-to-end and regression scenarios (Playwright/Cypress flows, fixtures, mocks).
|
||||
|
||||
4
Testfow/README.md
Normal file
4
Testfow/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Testflow
|
||||
|
||||
Placeholder for end-to-end and regression scenarios (Playwright/Cypress flows, fixtures, mocks).
|
||||
|
||||
Reference in New Issue
Block a user