Skip to content

Commit

Permalink
Add share button
Browse files Browse the repository at this point in the history
  • Loading branch information
monodot committed Jan 4, 2025
1 parent 16a9ca1 commit 9db309b
Show file tree
Hide file tree
Showing 10 changed files with 404 additions and 20 deletions.
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
"preview": "vite preview"
},
"dependencies": {
"@types/pako": "^2.0.3",
"@vee-validate/zod": "^4.15.0",
"@vueuse/core": "^12.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"js-yaml": "^4.1.0",
"lucide-vue-next": "^0.469.0",
"pako": "^2.1.0",
"radix-vue": "^1.9.11",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
Expand Down
75 changes: 57 additions & 18 deletions src/components/Playground.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
<script setup lang="ts">
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Toaster } from "@/components/ui/toast";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@/components/ui/resizable';
import {computed, ref} from "vue";
import CodeViewer from "./CodeViewer.vue";
import TemplateDialog from "./TemplateDialog.vue";
import {Badge} from "@/components/ui/badge";
import {Button} from "@/components/ui/button";
import {Toaster} from "@/components/ui/toast";
import {ResizableHandle, ResizablePanel, ResizablePanelGroup,} from '@/components/ui/resizable';
import {computed, onMounted, ref} from "vue";
import CodeViewer from "@/components/CodeViewer.vue";
import TemplateDialog from "@/components/TemplateDialog.vue";
import ImportDialog from "@/components/ImportDialog.vue";
import ResourcesList from "./ResourcesList.vue";
import { useToast } from "@/components/ui/toast/use-toast";
import { Clipboard, ExternalLink } from "lucide-vue-next";
import { dump } from 'js-yaml';
import ResourcesList from "@/components/ResourcesList.vue";
import {useToast} from "@/components/ui/toast/use-toast";
import {Clipboard, ExternalLink} from "lucide-vue-next";
import {dump} from 'js-yaml';
import type {Resource} from "@/types/resource.ts";
import ResourceForm from "@/components/ResourceForm.vue";
import { generateId } from "@/lib/utils.ts";
import { resources as defaultResources } from "@/templates/default"; // Load an initial/default set of resources
import WelcomeDialog from "@/components//WelcomeDialog.vue";
import {generateId} from "@/lib/utils.ts";
import {resources as defaultResources} from "@/templates/default"; // Load an initial/default set of resources
import WelcomeDialog from "@/components/WelcomeDialog.vue";
import {decodeResources} from "@/lib/sharing.ts";
import ShareButton from "@/components/ShareButton.vue";
const { toast } = useToast();
Expand Down Expand Up @@ -67,6 +65,43 @@ const copyToClipboard = () => {
});
};
const loadSharedResources = () => {
const params = new URLSearchParams(window.location.search);
const shared = params.get('resources');
if (!shared) return;
const { resources: decodedResources, errors } = decodeResources(shared);
if (errors.length > 0) {
toast({
variant: "destructive",
description: "Failed to load some shared resources. They may be invalid or corrupted.",
});
console.error('Share decode errors:', errors);
}
if (decodedResources.length > 0) {
// TODO: Merge this with the code in "ImportDialog", because they basically do the same thing
resources.value = []; // reset
decodedResources.forEach(resource => {
resources.value.push({
id: generateId(resources.value),
...resource
});
})
selectedResourceId.value = resources.value[0].id || null;
toast({
description: `Loaded ${decodedResources.length} shared resources!`,
});
}
};
onMounted(() => {
loadSharedResources();
});
</script>

<template>
Expand Down Expand Up @@ -111,6 +146,10 @@ const copyToClipboard = () => {
Copy all
</Button>

<ShareButton
:resources="resources"
/>

<a href="https://github.com/monodot/manikure" class="text-sm font-medium px-2 flex items-center gap-1">
<span>GitHub</span>
<ExternalLink class="size-3" />
Expand Down
140 changes: 140 additions & 0 deletions src/components/ShareButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Button } from '@/components/ui/button';
import { Share, AlertCircle, Copy, Check } from 'lucide-vue-next';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { useToast } from '@/components/ui/toast';
const props = defineProps<{
resources: any[];
onShare?: () => void;
}>();
const { toast } = useToast();
const showDialog = ref(false);
const copied = ref(false);
const shareUrl = ref<string>('');
const urlTooLong = ref(false);
// Import these from your sharing utilities
import { encodeResources, checkUrlLength } from '@/lib/sharing';
const generateShareUrl = () => {
try {
console.debug('Generating share URL...');
const encoded = encodeResources(props.resources);
const baseUrl = `${window.location.origin}${window.location.pathname}`;
const { valid } = checkUrlLength(baseUrl, encoded);
urlTooLong.value = !valid;
shareUrl.value = valid ? `${baseUrl}?resources=${encoded}` : '';
return valid;
} catch (err) {
console.error('Failed to generate share URL:', err);
return false;
}
};
const copyToClipboard = async () => {
if (!shareUrl.value) return;
try {
await navigator.clipboard.writeText(shareUrl.value);
copied.value = true;
setTimeout(() => {
copied.value = false;
}, 2000);
toast({
description: "Share URL copied to clipboard!",
});
showDialog.value = false;
} catch (err) {
toast({
variant: "destructive",
description: "Failed to copy URL. Please try again.",
});
}
};
// Generate URL when dialog opens
const handleDialogOpen = () => {
console.debug('Dialog opened');
generateShareUrl();
};
</script>

<template>
<Dialog v-model:open="showDialog" @update:open="handleDialogOpen">
<DialogTrigger asChild>
<Button
variant="outline"
size="sm"
class="gap-1.5 text-sm"
:disabled="resources.length === 0"
>
<Share class="size-3.5" />
Share
</Button>
</DialogTrigger>

<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>Share Project</DialogTitle>
<DialogDescription>
Share your Kubernetes resources with others using this URL
</DialogDescription>
</DialogHeader>

<div class="flex items-center space-x-2">
<div v-if="urlTooLong" class="flex-1">
<Alert variant="destructive">
<AlertCircle class="size-4" />
<AlertDescription>
Project is too large to share via URL. Try reducing the number of resources.
</AlertDescription>
</Alert>
</div>
<div v-else-if="shareUrl" class="grid flex-1 gap-4">
<div class="flex items-center">
<input
class="flex-1 truncate rounded-md border px-3 py-2 text-sm"
:value="shareUrl"
readonly
/>
</div>
</div>
</div>

<DialogFooter className="sm:justify-start">
<Button
v-if="!urlTooLong && shareUrl"
type="button"
variant="secondary"
size="sm"
class="gap-1.5"
@click="copyToClipboard"
>
<span v-if="!copied">
<Copy class="size-3.5" />
Copy URL
</span>
<span v-else>
<Check class="size-3.5" />
Copied!
</span>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
16 changes: 16 additions & 0 deletions src/components/ui/alert/Alert.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { type AlertVariants, alertVariants } from '.'
const props = defineProps<{
class?: HTMLAttributes['class']
variant?: AlertVariants['variant']
}>()
</script>

<template>
<div :class="cn(alertVariants({ variant }), props.class)" role="alert">
<slot />
</div>
</template>
14 changes: 14 additions & 0 deletions src/components/ui/alert/AlertDescription.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

<template>
<div :class="cn('text-sm [&_p]:leading-relaxed', props.class)">
<slot />
</div>
</template>
14 changes: 14 additions & 0 deletions src/components/ui/alert/AlertTitle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>

<template>
<h5 :class="cn('mb-1 font-medium leading-none tracking-tight', props.class)">
<slot />
</h5>
</template>
23 changes: 23 additions & 0 deletions src/components/ui/alert/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { cva, type VariantProps } from 'class-variance-authority'

export { default as Alert } from './Alert.vue'
export { default as AlertDescription } from './AlertDescription.vue'
export { default as AlertTitle } from './AlertTitle.vue'

export const alertVariants = cva(
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
{
variants: {
variant: {
default: 'bg-background text-foreground',
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
},
},
defaultVariants: {
variant: 'default',
},
},
)

export type AlertVariants = VariantProps<typeof alertVariants>
Loading

0 comments on commit 9db309b

Please sign in to comment.