Skip to content

Commit 37ca2b9

Browse files
committed
chore: wip
1 parent a7b6d77 commit 37ca2b9

File tree

3 files changed

+745
-0
lines changed

3 files changed

+745
-0
lines changed

storage/framework/defaults/views/dashboard/models/subscribers.vue

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,22 @@
9999
</div>
100100
</div>
101101

102+
<div class="mb-8 px-4 lg:px-8 sm:px-6">
103+
<div class="bg-white dark:bg-blue-gray-700 rounded-lg shadow">
104+
<div class="p-6">
105+
<div class="flex items-center justify-between mb-6">
106+
<div>
107+
<h3 class="text-base font-medium text-gray-900 dark:text-gray-100">Subscriber Model Relationships</h3>
108+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Interactive diagram showing Subscriber model relationships</p>
109+
</div>
110+
</div>
111+
<div ref="diagramContainer" class="h-[400px] relative">
112+
<!-- D3 diagram will be rendered here -->
113+
</div>
114+
</div>
115+
</div>
116+
</div>
117+
102118
<div class="px-4 pt-12 lg:px-8 sm:px-6">
103119
<div class="sm:flex sm:items-center">
104120
<div class="sm:flex-auto">
@@ -247,6 +263,8 @@ import {
247263
Scale,
248264
CoreScaleOptions,
249265
} from 'chart.js'
266+
import * as d3 from 'd3'
267+
import { useRouter } from 'vue-router'
250268
251269
ChartJS.register(
252270
CategoryScale,
@@ -399,4 +417,226 @@ onMounted(async () => {
399417
await new Promise(resolve => setTimeout(resolve, 500))
400418
isLoading.value = false
401419
})
420+
421+
// Get router instance
422+
const router = useRouter()
423+
424+
// Model node interface
425+
interface ModelNode extends d3.SimulationNodeDatum {
426+
id: string
427+
name: string
428+
properties: string[]
429+
relationships: string[]
430+
emoji: string
431+
color: string
432+
x?: number
433+
y?: number
434+
fx?: number | null
435+
fy?: number | null
436+
}
437+
438+
// Relationship link interface
439+
interface RelationshipLink {
440+
source: string | ModelNode
441+
target: string | ModelNode
442+
type: 'hasMany' | 'belongsTo' | 'hasOne' | 'belongsToMany'
443+
}
444+
445+
// Subscriber model and its relationships
446+
const models: ModelNode[] = [
447+
{
448+
id: 'subscriber',
449+
name: 'Subscriber',
450+
properties: ['id', 'email', 'status'],
451+
relationships: ['subscriberEmails'],
452+
emoji: '📫',
453+
color: '#0D9488'
454+
},
455+
{
456+
id: 'subscriberEmail',
457+
name: 'SubscriberEmail',
458+
properties: ['id', 'email', 'subscriber_id'],
459+
relationships: ['subscriber'],
460+
emoji: '✉️',
461+
color: '#2DD4BF'
462+
}
463+
]
464+
465+
// Define relationships
466+
const relationships: RelationshipLink[] = [
467+
{ source: 'subscriber', target: 'subscriberEmail', type: 'hasMany' },
468+
{ source: 'subscriberEmail', target: 'subscriber', type: 'belongsTo' }
469+
]
470+
471+
// Visualization state
472+
const diagramContainer = ref<HTMLElement | null>(null)
473+
let simulation: d3.Simulation<ModelNode, undefined>
474+
475+
// Function to get route path for a model
476+
const getModelRoute = (modelId: string) => {
477+
const routes: Record<string, string> = {
478+
user: '/models/users',
479+
team: '/models/teams',
480+
accessToken: '/models/access-tokens',
481+
activity: '/models/activities',
482+
post: '/models/posts',
483+
subscriber: '/models/subscribers',
484+
subscriberEmail: '/models/subscriber-emails'
485+
}
486+
return routes[modelId] || '/models'
487+
}
488+
489+
onMounted(() => {
490+
if (!diagramContainer.value) return
491+
492+
const width = 800
493+
const height = 400
494+
const svg = d3.select(diagramContainer.value)
495+
.append('svg')
496+
.attr('width', width)
497+
.attr('height', height)
498+
.attr('viewBox', [0, 0, width, height])
499+
.attr('style', 'max-width: 100%; height: auto;')
500+
501+
// Create arrow marker
502+
svg.append('defs').selectAll('marker')
503+
.data(['arrow'])
504+
.join('marker')
505+
.attr('id', d => d)
506+
.attr('viewBox', '0 -5 10 10')
507+
.attr('refX', 25)
508+
.attr('refY', 0)
509+
.attr('markerWidth', 6)
510+
.attr('markerHeight', 6)
511+
.attr('orient', 'auto')
512+
.append('path')
513+
.attr('fill', '#999')
514+
.attr('d', 'M0,-5L10,0L0,5')
515+
516+
// Create the simulation
517+
simulation = d3.forceSimulation<ModelNode>(models)
518+
.force('link', d3.forceLink<ModelNode, RelationshipLink>(relationships)
519+
.id(d => d.id)
520+
.distance(150))
521+
.force('charge', d3.forceManyBody().strength(-800))
522+
.force('center', d3.forceCenter(width / 2, height / 2))
523+
524+
// Draw the links
525+
const link = svg.append('g')
526+
.selectAll('line')
527+
.data(relationships)
528+
.join('line')
529+
.attr('stroke', '#999')
530+
.attr('stroke-opacity', 0.6)
531+
.attr('stroke-width', 2)
532+
.attr('marker-end', 'url(#arrow)')
533+
534+
// Draw the nodes with proper typing
535+
const node = svg.append('g')
536+
.selectAll<SVGGElement, ModelNode>('g')
537+
.data(models)
538+
.join('g')
539+
.call((selection) => {
540+
const drag = d3.drag<SVGGElement, ModelNode>()
541+
.on('start', dragstarted)
542+
.on('drag', dragged)
543+
.on('end', dragended)
544+
return drag(selection)
545+
})
546+
.style('cursor', 'pointer')
547+
.on('click', (event, d) => {
548+
router.push(getModelRoute(d.id))
549+
})
550+
551+
// Add hover effect to nodes
552+
node.on('mouseover', function() {
553+
d3.select(this).select('circle')
554+
.transition()
555+
.duration(200)
556+
.attr('r', 22) // Slightly larger on hover
557+
})
558+
.on('mouseout', function() {
559+
d3.select(this).select('circle')
560+
.transition()
561+
.duration(200)
562+
.attr('r', 20) // Back to normal size
563+
})
564+
565+
// Add circles for nodes
566+
node.append('circle')
567+
.attr('r', 20)
568+
.attr('fill', d => d.color)
569+
570+
// Add emojis
571+
node.append('text')
572+
.attr('dy', '0.35em')
573+
.attr('text-anchor', 'middle')
574+
.text(d => d.emoji)
575+
.attr('font-size', '20px')
576+
577+
// Add labels
578+
node.append('text')
579+
.attr('dy', 35)
580+
.attr('text-anchor', 'middle')
581+
.text(d => d.name)
582+
.attr('fill', '#374151')
583+
.attr('font-size', '14px')
584+
.attr('font-weight', 'bold')
585+
586+
// Update positions on each tick
587+
simulation.on('tick', () => {
588+
link
589+
.attr('x1', d => (d.source as ModelNode).x!)
590+
.attr('y1', d => (d.source as ModelNode).y!)
591+
.attr('x2', d => (d.target as ModelNode).x!)
592+
.attr('y2', d => (d.target as ModelNode).y!)
593+
594+
node
595+
.attr('transform', d => `translate(${d.x},${d.y})`)
596+
})
597+
598+
// Drag functions
599+
function dragstarted(event: d3.D3DragEvent<SVGGElement, ModelNode, ModelNode>) {
600+
if (!event.active) simulation.alphaTarget(0.3).restart()
601+
event.subject.fx = event.subject.x
602+
event.subject.fy = event.subject.y
603+
}
604+
605+
function dragged(event: d3.D3DragEvent<SVGGElement, ModelNode, ModelNode>) {
606+
event.subject.fx = event.x
607+
event.subject.fy = event.y
608+
}
609+
610+
function dragended(event: d3.D3DragEvent<SVGGElement, ModelNode, ModelNode>) {
611+
if (!event.active) simulation.alphaTarget(0)
612+
event.subject.fx = null
613+
event.subject.fy = null
614+
}
615+
})
402616
</script>
617+
618+
<style scoped>
619+
/* Add these styles for the diagram */
620+
:deep(svg) {
621+
background-color: transparent;
622+
border-radius: 0.5rem;
623+
}
624+
625+
:deep(line) {
626+
stroke-linecap: round;
627+
}
628+
629+
:deep(text) {
630+
user-select: none;
631+
}
632+
633+
:deep(circle) {
634+
cursor: grab;
635+
}
636+
637+
:deep(circle:active) {
638+
cursor: grabbing;
639+
}
640+
641+
/* ... existing styles ... */
642+
</style>

0 commit comments

Comments
 (0)