├── .vscode └── settings.json ├── bun.lockb ├── pages ├── login.vue ├── register.vue ├── new_organization.vue ├── [organization] │ ├── manage │ │ ├── posts │ │ │ ├── add.vue │ │ │ └── [id].vue │ │ ├── index.vue │ │ └── taxonomies │ │ │ └── index.vue │ ├── [slug].vue │ └── manage.vue ├── write.vue ├── moderation.vue ├── thread │ └── [slug] │ │ └── edit.vue ├── jobs │ ├── index.vue │ └── [slug] │ │ └── index.vue ├── user │ ├── my-posts.vue │ └── [id].vue └── forums │ ├── index.vue │ └── [slug] │ └── index.vue ├── components ├── ui │ ├── input │ │ ├── index.ts │ │ └── Input.vue │ ├── label │ │ ├── index.ts │ │ └── Label.vue │ ├── slider │ │ ├── index.ts │ │ └── Slider.vue │ ├── switch │ │ ├── index.ts │ │ └── Switch.vue │ ├── checkbox │ │ ├── index.ts │ │ └── Checkbox.vue │ ├── progress │ │ ├── index.ts │ │ └── Progress.vue │ ├── skeleton │ │ ├── index.ts │ │ └── Skeleton.vue │ ├── textarea │ │ ├── index.ts │ │ └── Textarea.vue │ ├── separator │ │ ├── index.ts │ │ └── Separator.vue │ ├── aspect-ratio │ │ ├── index.ts │ │ └── AspectRatio.vue │ ├── radio-group │ │ ├── index.ts │ │ ├── RadioGroup.vue │ │ └── RadioGroupItem.vue │ ├── popover │ │ ├── index.ts │ │ ├── PopoverTrigger.vue │ │ ├── Popover.vue │ │ └── PopoverContent.vue │ ├── tooltip │ │ ├── index.ts │ │ ├── TooltipTrigger.vue │ │ ├── TooltipProvider.vue │ │ ├── Tooltip.vue │ │ └── TooltipContent.vue │ ├── accordion │ │ ├── index.ts │ │ ├── Accordion.vue │ │ ├── AccordionItem.vue │ │ ├── AccordionContent.vue │ │ └── AccordionTrigger.vue │ ├── avatar │ │ ├── AvatarImage.vue │ │ ├── AvatarFallback.vue │ │ ├── Avatar.vue │ │ └── index.ts │ ├── dialog │ │ ├── DialogClose.vue │ │ ├── DialogTrigger.vue │ │ ├── DialogHeader.vue │ │ ├── Dialog.vue │ │ ├── DialogFooter.vue │ │ ├── index.ts │ │ ├── DialogDescription.vue │ │ ├── DialogTitle.vue │ │ ├── DialogScrollContent.vue │ │ └── DialogContent.vue │ ├── select │ │ ├── SelectValue.vue │ │ ├── SelectItemText.vue │ │ ├── SelectLabel.vue │ │ ├── Select.vue │ │ ├── SelectSeparator.vue │ │ ├── SelectGroup.vue │ │ ├── index.ts │ │ ├── SelectScrollUpButton.vue │ │ ├── SelectScrollDownButton.vue │ │ ├── SelectTrigger.vue │ │ ├── SelectItem.vue │ │ └── SelectContent.vue │ ├── toast │ │ ├── ToastProvider.vue │ │ ├── ToastTitle.vue │ │ ├── ToastDescription.vue │ │ ├── ToastViewport.vue │ │ ├── Toast.vue │ │ ├── ToastClose.vue │ │ ├── Toaster.vue │ │ ├── ToastAction.vue │ │ └── index.ts │ ├── breadcrumb │ │ ├── Breadcrumb.vue │ │ ├── BreadcrumbItem.vue │ │ ├── BreadcrumbList.vue │ │ ├── BreadcrumbPage.vue │ │ ├── index.ts │ │ ├── BreadcrumbSeparator.vue │ │ ├── BreadcrumbLink.vue │ │ └── BreadcrumbEllipsis.vue │ ├── calendar │ │ ├── CalendarGridBody.vue │ │ ├── CalendarGridHead.vue │ │ ├── CalendarGridRow.vue │ │ ├── CalendarGrid.vue │ │ ├── CalendarHeader.vue │ │ ├── CalendarHeadCell.vue │ │ ├── CalendarHeading.vue │ │ ├── index.ts │ │ ├── CalendarCell.vue │ │ ├── CalendarPrevButton.vue │ │ ├── CalendarNextButton.vue │ │ ├── CalendarCellTrigger.vue │ │ └── Calendar.vue │ ├── auto-form │ │ ├── AutoFormLabel.vue │ │ ├── index.ts │ │ ├── AutoFormFieldNumber.vue │ │ ├── constant.ts │ │ ├── AutoFormFieldInput.vue │ │ ├── AutoFormField.vue │ │ ├── AutoFormFieldBoolean.vue │ │ ├── AutoFormFieldEnum.vue │ │ └── AutoFormFieldDate.vue │ ├── dropdown-menu │ │ ├── DropdownMenuGroup.vue │ │ ├── DropdownMenuShortcut.vue │ │ ├── DropdownMenuTrigger.vue │ │ ├── DropdownMenu.vue │ │ ├── DropdownMenuSub.vue │ │ ├── DropdownMenuRadioGroup.vue │ │ ├── DropdownMenuSeparator.vue │ │ ├── DropdownMenuLabel.vue │ │ ├── DropdownMenuSubTrigger.vue │ │ ├── DropdownMenuItem.vue │ │ ├── index.ts │ │ ├── DropdownMenuSubContent.vue │ │ ├── DropdownMenuRadioItem.vue │ │ ├── DropdownMenuCheckboxItem.vue │ │ └── DropdownMenuContent.vue │ ├── card │ │ ├── CardContent.vue │ │ ├── CardFooter.vue │ │ ├── CardHeader.vue │ │ ├── index.ts │ │ ├── CardDescription.vue │ │ ├── CardTitle.vue │ │ └── Card.vue │ ├── tags-input │ │ ├── index.ts │ │ ├── TagsInputItemText.vue │ │ ├── TagsInputInput.vue │ │ ├── TagsInputItemDelete.vue │ │ ├── TagsInputItem.vue │ │ └── TagsInput.vue │ ├── form │ │ ├── index.ts │ │ ├── FormMessage.vue │ │ ├── FormControl.vue │ │ ├── FormDescription.vue │ │ ├── FormItem.vue │ │ ├── FormLabel.vue │ │ └── useFormField.ts │ ├── badge │ │ ├── Badge.vue │ │ └── index.ts │ ├── carousel │ │ ├── index.ts │ │ ├── CarouselItem.vue │ │ ├── interface.ts │ │ ├── CarouselContent.vue │ │ ├── CarouselNext.vue │ │ ├── CarouselPrevious.vue │ │ ├── Carousel.vue │ │ └── useCarousel.ts │ ├── pagination │ │ ├── index.ts │ │ ├── PaginationEllipsis.vue │ │ ├── PaginationPrev.vue │ │ ├── PaginationLast.vue │ │ ├── PaginationNext.vue │ │ └── PaginationFirst.vue │ └── button │ │ ├── Button.vue │ │ └── index.ts ├── BlockWithTitle.vue ├── Container.vue ├── ModerationPosts.vue ├── AskToLogin.vue ├── icons │ └── GoogleLogo.vue ├── editor │ ├── slash-commands │ │ ├── commands.js │ │ └── CommandsList.vue │ ├── extensions.ts │ ├── image-upload │ │ ├── image-upload.ts │ │ └── ImageUploader.vue │ └── image-block │ │ └── Component.vue ├── ColorPalette.vue ├── FreezeLoading.vue ├── OrganizationCardLite.vue ├── PostRow.vue ├── NewStoryButton.vue ├── FormManagePost │ └── types.ts ├── Footer.vue ├── OrganizationInfo.vue ├── DevfeedSideBar.vue ├── CommunityTags.vue ├── JobTags.vue ├── NewFeedPost.vue ├── OrganizationCard.vue ├── PostCard.vue ├── UserCard.vue ├── ButtonNewTaxonomy.vue ├── SelectFeatureImage.vue ├── SelectSchemaDialog.vue ├── JobCardLite.vue ├── UpdateUserProfile.vue ├── JobRow.vue └── StoryCard.vue ├── public ├── hi.webp ├── favicon.ico └── github_banner.png ├── server └── tsconfig.json ├── lib ├── browser-file-table.ts ├── color.ts ├── id.ts ├── firebase.ts └── utils.ts ├── tsconfig.json ├── .env.example ├── app.vue ├── biome.json ├── .gitignore ├── layouts ├── default.vue └── tenant.vue ├── components.json ├── assets └── workers │ └── blurhash.js ├── .github └── FUNDING.yml ├── plugins ├── vee-validate.ts └── auth.client.ts ├── nuxt.config.ts ├── package.json └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hieuhani/techgoda/HEAD/bun.lockb -------------------------------------------------------------------------------- /pages/login.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Input } from './Input.vue' 2 | -------------------------------------------------------------------------------- /components/ui/label/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Label } from './Label.vue' 2 | -------------------------------------------------------------------------------- /components/ui/slider/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Slider } from './Slider.vue' 2 | -------------------------------------------------------------------------------- /components/ui/switch/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Switch } from './Switch.vue' 2 | -------------------------------------------------------------------------------- /pages/register.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public/hi.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hieuhani/techgoda/HEAD/public/hi.webp -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /components/ui/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Checkbox } from './Checkbox.vue' 2 | -------------------------------------------------------------------------------- /components/ui/progress/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Progress } from './Progress.vue' 2 | -------------------------------------------------------------------------------- /components/ui/skeleton/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Skeleton } from './Skeleton.vue' 2 | -------------------------------------------------------------------------------- /components/ui/textarea/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Textarea } from './Textarea.vue' 2 | -------------------------------------------------------------------------------- /lib/browser-file-table.ts: -------------------------------------------------------------------------------- 1 | export const browserFileTable: Record = {}; 2 | -------------------------------------------------------------------------------- /components/ui/separator/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Separator } from './Separator.vue' 2 | -------------------------------------------------------------------------------- /pages/new_organization.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hieuhani/techgoda/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /components/ui/aspect-ratio/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AspectRatio } from './AspectRatio.vue' 2 | -------------------------------------------------------------------------------- /pages/[organization]/manage/posts/add.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public/github_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hieuhani/techgoda/HEAD/public/github_banner.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /components/ui/radio-group/index.ts: -------------------------------------------------------------------------------- 1 | export { default as RadioGroup } from './RadioGroup.vue' 2 | export { default as RadioGroupItem } from './RadioGroupItem.vue' 3 | -------------------------------------------------------------------------------- /components/ui/popover/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Popover } from './Popover.vue' 2 | export { default as PopoverTrigger } from './PopoverTrigger.vue' 3 | export { default as PopoverContent } from './PopoverContent.vue' 4 | -------------------------------------------------------------------------------- /pages/write.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | VITE_FIREBASE_API_KEY=dummy-api-key 2 | VITE_FIREBASE_PROJECT_ID=dummy-project-id 3 | VITE_FIREBASE_AUTH_DOMAIN=dummy-auth-domain 4 | VITE_FIREBASE_APP_ID=dummy-app-id 5 | VITE_PUBLIZ_API_URL=https://publiz-techgoda.hieutran.workers.dev -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /pages/[organization]/manage/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/color.ts: -------------------------------------------------------------------------------- 1 | export function generateColor(stringInput: string) { 2 | let stringUniqueHash = [...stringInput].reduce((acc, char) => { 3 | return char.charCodeAt(0) + ((acc << 5) - acc); 4 | }, 0); 5 | return `hsl(${stringUniqueHash % 360}, 95%, 35%)`; 6 | } 7 | -------------------------------------------------------------------------------- /pages/moderation.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /components/ui/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Tooltip } from './Tooltip.vue' 2 | export { default as TooltipContent } from './TooltipContent.vue' 3 | export { default as TooltipTrigger } from './TooltipTrigger.vue' 4 | export { default as TooltipProvider } from './TooltipProvider.vue' 5 | -------------------------------------------------------------------------------- /components/ui/accordion/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Accordion } from './Accordion.vue' 2 | export { default as AccordionContent } from './AccordionContent.vue' 3 | export { default as AccordionItem } from './AccordionItem.vue' 4 | export { default as AccordionTrigger } from './AccordionTrigger.vue' 5 | -------------------------------------------------------------------------------- /components/ui/avatar/AvatarImage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogClose.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/select/SelectValue.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/aspect-ratio/AspectRatio.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /lib/id.ts: -------------------------------------------------------------------------------- 1 | import Sqids from "sqids"; 2 | 3 | const sqids = new Sqids({ 4 | alphabet: "abcdefghijklmnopqrstuvwxyz", 5 | }); 6 | 7 | export const encodeId = (id: number) => { 8 | return sqids.encode([id]); 9 | }; 10 | 11 | export const decodeId = (id: string) => { 12 | return sqids.decode(id)[0]; 13 | }; 14 | -------------------------------------------------------------------------------- /components/ui/toast/ToastProvider.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/avatar/AvatarFallback.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/popover/PopoverTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/select/SelectItemText.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/tooltip/TooltipTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/tooltip/TooltipProvider.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | service-account.json 26 | -------------------------------------------------------------------------------- /components/BlockWithTitle.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /components/ui/breadcrumb/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /components/ui/calendar/CalendarGridBody.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/calendar/CalendarGridHead.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/auto-form/AutoFormLabel.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuGroup.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /components/ui/card/CardContent.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /components/ui/tags-input/index.ts: -------------------------------------------------------------------------------- 1 | export { default as TagsInput } from './TagsInput.vue' 2 | export { default as TagsInputInput } from './TagsInputInput.vue' 3 | export { default as TagsInputItem } from './TagsInputItem.vue' 4 | export { default as TagsInputItemDelete } from './TagsInputItemDelete.vue' 5 | export { default as TagsInputItemText } from './TagsInputItemText.vue' 6 | -------------------------------------------------------------------------------- /components/ui/card/CardFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /components/ui/card/CardHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Card } from './Card.vue' 2 | export { default as CardHeader } from './CardHeader.vue' 3 | export { default as CardTitle } from './CardTitle.vue' 4 | export { default as CardDescription } from './CardDescription.vue' 5 | export { default as CardContent } from './CardContent.vue' 6 | export { default as CardFooter } from './CardFooter.vue' 7 | -------------------------------------------------------------------------------- /components/ui/card/CardDescription.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /components/ui/form/index.ts: -------------------------------------------------------------------------------- 1 | export { Form, Field as FormField } from 'vee-validate' 2 | export { default as FormItem } from './FormItem.vue' 3 | export { default as FormLabel } from './FormLabel.vue' 4 | export { default as FormControl } from './FormControl.vue' 5 | export { default as FormMessage } from './FormMessage.vue' 6 | export { default as FormDescription } from './FormDescription.vue' 7 | -------------------------------------------------------------------------------- /components/ui/breadcrumb/BreadcrumbItem.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /components/ui/skeleton/Skeleton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuShortcut.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /components/Container.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://shadcn-vue.com/schema.json", 3 | "style": "default", 4 | "typescript": true, 5 | "tailwind": { 6 | "config": "tailwind.config.js", 7 | "css": "./assets/css/tailwind.css", 8 | "baseColor": "slate", 9 | "cssVariables": true 10 | }, 11 | "framework": "nuxt", 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /components/ui/card/CardTitle.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /components/ui/breadcrumb/BreadcrumbList.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /components/ui/dialog/Dialog.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /components/ui/form/FormMessage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuTrigger.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /components/ui/tooltip/Tooltip.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /components/ui/badge/Badge.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /lib/firebase.ts: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import { getAuth } from "firebase/auth"; 3 | 4 | const firebaseConfig = { 5 | apiKey: import.meta.env.VITE_FIREBASE_API_KEY, 6 | authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, 7 | projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID, 8 | appId: import.meta.env.VITE_FIREBASE_APP_ID, 9 | }; 10 | const app = initializeApp(firebaseConfig); 11 | 12 | export const firebaseAuth = getAuth(app); 13 | -------------------------------------------------------------------------------- /components/ui/card/Card.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /components/ui/select/SelectLabel.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /components/ModerationPosts.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /components/ui/breadcrumb/BreadcrumbPage.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /components/ui/select/Select.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /components/ui/carousel/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Carousel } from './Carousel.vue' 2 | export { default as CarouselContent } from './CarouselContent.vue' 3 | export { default as CarouselItem } from './CarouselItem.vue' 4 | export { default as CarouselPrevious } from './CarouselPrevious.vue' 5 | export { default as CarouselNext } from './CarouselNext.vue' 6 | export { useCarousel } from './useCarousel' 7 | 8 | export type { 9 | EmblaCarouselType as CarouselApi, 10 | } from 'embla-carousel' 11 | -------------------------------------------------------------------------------- /components/ui/popover/Popover.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /components/ui/form/FormControl.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /pages/thread/[slug]/edit.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /components/ui/pagination/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | PaginationRoot as Pagination, 3 | PaginationList, 4 | PaginationListItem, 5 | } from 'radix-vue' 6 | export { default as PaginationEllipsis } from './PaginationEllipsis.vue' 7 | export { default as PaginationFirst } from './PaginationFirst.vue' 8 | export { default as PaginationLast } from './PaginationLast.vue' 9 | export { default as PaginationNext } from './PaginationNext.vue' 10 | export { default as PaginationPrev } from './PaginationPrev.vue' 11 | -------------------------------------------------------------------------------- /components/ui/breadcrumb/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Breadcrumb } from './Breadcrumb.vue' 2 | export { default as BreadcrumbEllipsis } from './BreadcrumbEllipsis.vue' 3 | export { default as BreadcrumbItem } from './BreadcrumbItem.vue' 4 | export { default as BreadcrumbLink } from './BreadcrumbLink.vue' 5 | export { default as BreadcrumbList } from './BreadcrumbList.vue' 6 | export { default as BreadcrumbPage } from './BreadcrumbPage.vue' 7 | export { default as BreadcrumbSeparator } from './BreadcrumbSeparator.vue' 8 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenu.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /components/ui/accordion/Accordion.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | import { useRequestURL } from "nuxt/app"; 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)); 7 | } 8 | 9 | export const useBuildTenantUrl = (tenant: string) => { 10 | const { protocol, host } = useRequestURL(); 11 | if (host.startsWith(tenant)) { 12 | return `${protocol}//${host}`; 13 | } 14 | return `${protocol}//${tenant ? `${tenant}.` : ""}${host}`; 15 | }; 16 | -------------------------------------------------------------------------------- /assets/workers/blurhash.js: -------------------------------------------------------------------------------- 1 | import { encode } from "blurhash"; 2 | 3 | addEventListener("message", (event) => { 4 | const { type, payload } = event.data; 5 | switch (type) { 6 | case "BLURHASH_ENCODING": { 7 | const blurHash = encode( 8 | payload.data, 9 | payload.width, 10 | payload.height, 11 | 4, 12 | 4 13 | ); 14 | postMessage({ 15 | type: "BLURHASH_ENCODED", 16 | payload: blurHash, 17 | }); 18 | break; 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /components/ui/form/FormDescription.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /components/ui/breadcrumb/BreadcrumbSeparator.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuSub.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /pages/[organization]/manage/posts/[id].vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /components/AskToLogin.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /components/ui/breadcrumb/BreadcrumbLink.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuRadioGroup.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /components/icons/GoogleLogo.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Dialog } from './Dialog.vue' 2 | export { default as DialogClose } from './DialogClose.vue' 3 | export { default as DialogTrigger } from './DialogTrigger.vue' 4 | export { default as DialogHeader } from './DialogHeader.vue' 5 | export { default as DialogTitle } from './DialogTitle.vue' 6 | export { default as DialogDescription } from './DialogDescription.vue' 7 | export { default as DialogContent } from './DialogContent.vue' 8 | export { default as DialogScrollContent } from './DialogScrollContent.vue' 9 | export { default as DialogFooter } from './DialogFooter.vue' 10 | -------------------------------------------------------------------------------- /components/ui/select/SelectSeparator.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /components/ui/select/SelectGroup.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /components/ui/toast/ToastTitle.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /components/ui/avatar/Avatar.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /components/ui/breadcrumb/BreadcrumbEllipsis.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /components/ui/carousel/CarouselItem.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | -------------------------------------------------------------------------------- /components/ui/carousel/interface.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | EmblaCarouselType as CarouselApi, 3 | EmblaOptionsType as CarouselOptions, 4 | EmblaPluginType as CarouselPlugin, 5 | } from 'embla-carousel' 6 | import type { HTMLAttributes, Ref } from 'vue' 7 | 8 | export interface CarouselProps { 9 | opts?: CarouselOptions | Ref 10 | plugins?: CarouselPlugin[] | Ref 11 | orientation?: 'horizontal' | 'vertical' 12 | } 13 | 14 | export interface CarouselEmits { 15 | (e: 'init-api', payload: CarouselApi): void 16 | } 17 | 18 | export interface WithClassAsProps { 19 | class?: HTMLAttributes['class'] 20 | } 21 | -------------------------------------------------------------------------------- /components/ui/form/FormItem.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | 21 | 26 | -------------------------------------------------------------------------------- /components/editor/slash-commands/commands.js: -------------------------------------------------------------------------------- 1 | import { Extension } from "@tiptap/core"; 2 | import Suggestion from "@tiptap/suggestion"; 3 | 4 | export default Extension.create({ 5 | name: "slashCommands", 6 | 7 | addOptions() { 8 | return { 9 | suggestion: { 10 | char: "/", 11 | command: ({ editor, range, props }) => { 12 | props.command({ editor, range }); 13 | }, 14 | }, 15 | }; 16 | }, 17 | 18 | addProseMirrorPlugins() { 19 | return [ 20 | Suggestion({ 21 | editor: this.editor, 22 | ...this.options.suggestion, 23 | }), 24 | ]; 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /components/ui/toast/ToastDescription.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /components/ui/form/FormLabel.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | -------------------------------------------------------------------------------- /components/ui/separator/Separator.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 21 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuSeparator.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /components/ui/toast/ToastViewport.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /components/ui/calendar/CalendarGridRow.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /components/ui/tags-input/TagsInputItemText.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /components/ui/tags-input/TagsInputInput.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /components/ui/accordion/AccordionItem.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /components/ui/label/Label.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 28 | -------------------------------------------------------------------------------- /components/ui/button/Button.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 27 | -------------------------------------------------------------------------------- /components/ui/calendar/CalendarGrid.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /components/ui/calendar/CalendarHeader.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /components/ui/carousel/CarouselContent.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 30 | -------------------------------------------------------------------------------- /components/ui/select/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Select } from './Select.vue' 2 | export { default as SelectValue } from './SelectValue.vue' 3 | export { default as SelectTrigger } from './SelectTrigger.vue' 4 | export { default as SelectContent } from './SelectContent.vue' 5 | export { default as SelectGroup } from './SelectGroup.vue' 6 | export { default as SelectItem } from './SelectItem.vue' 7 | export { default as SelectItemText } from './SelectItemText.vue' 8 | export { default as SelectLabel } from './SelectLabel.vue' 9 | export { default as SelectSeparator } from './SelectSeparator.vue' 10 | export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue' 11 | export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue' 12 | -------------------------------------------------------------------------------- /components/ui/calendar/CalendarHeadCell.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogDescription.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /components/ui/pagination/PaginationEllipsis.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /components/ui/toast/Toast.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 29 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: hieuhani 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: hieuhani 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /components/ColorPalette.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 31 | -------------------------------------------------------------------------------- /components/ui/dialog/DialogTitle.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu/DropdownMenuLabel.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /components/ui/radio-group/RadioGroup.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 26 | -------------------------------------------------------------------------------- /components/ui/accordion/AccordionContent.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /components/ui/avatar/index.ts: -------------------------------------------------------------------------------- 1 | import { type VariantProps, cva } from 'class-variance-authority' 2 | 3 | export { default as Avatar } from './Avatar.vue' 4 | export { default as AvatarImage } from './AvatarImage.vue' 5 | export { default as AvatarFallback } from './AvatarFallback.vue' 6 | 7 | export const avatarVariant = cva( 8 | 'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden', 9 | { 10 | variants: { 11 | size: { 12 | sm: 'h-10 w-10 text-xs', 13 | base: 'h-16 w-16 text-2xl', 14 | lg: 'h-32 w-32 text-5xl', 15 | }, 16 | shape: { 17 | circle: 'rounded-full', 18 | square: 'rounded-md', 19 | }, 20 | }, 21 | }, 22 | ) 23 | 24 | export type AvatarVariants = VariantProps 25 | -------------------------------------------------------------------------------- /components/ui/calendar/CalendarHeading.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 28 | -------------------------------------------------------------------------------- /components/ui/tags-input/TagsInputItemDelete.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /components/ui/tags-input/TagsInputItem.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /components/ui/calendar/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Calendar } from './Calendar.vue' 2 | export { default as CalendarCell } from './CalendarCell.vue' 3 | export { default as CalendarCellTrigger } from './CalendarCellTrigger.vue' 4 | export { default as CalendarGrid } from './CalendarGrid.vue' 5 | export { default as CalendarGridBody } from './CalendarGridBody.vue' 6 | export { default as CalendarGridHead } from './CalendarGridHead.vue' 7 | export { default as CalendarGridRow } from './CalendarGridRow.vue' 8 | export { default as CalendarHeadCell } from './CalendarHeadCell.vue' 9 | export { default as CalendarHeader } from './CalendarHeader.vue' 10 | export { default as CalendarHeading } from './CalendarHeading.vue' 11 | export { default as CalendarNextButton } from './CalendarNextButton.vue' 12 | export { default as CalendarPrevButton } from './CalendarPrevButton.vue' 13 | -------------------------------------------------------------------------------- /components/ui/tags-input/TagsInput.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /components/ui/select/SelectScrollUpButton.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /plugins/vee-validate.ts: -------------------------------------------------------------------------------- 1 | import { defineRule } from "vee-validate"; 2 | import { required, email, min, max } from "@vee-validate/rules"; 3 | 4 | defineRule("required", (value: string) => { 5 | if (required(value)) { 6 | return true; 7 | } 8 | return "Hãy nhập thông tin"; 9 | }); 10 | 11 | defineRule("email", (value: string) => { 12 | if (email(value)) { 13 | return true; 14 | } 15 | return "Email không hợp lệ"; 16 | }); 17 | 18 | defineRule("min", (value: string, params: [string | number]) => { 19 | if (min(value, params)) { 20 | return true; 21 | } 22 | 23 | return `Độ dài tối thiểu là ${params[0]} kí tự`; 24 | }); 25 | 26 | defineRule("max", (value: string, params: [string | number]) => { 27 | if (max(value, params)) { 28 | return true; 29 | } 30 | 31 | return `Độ dài tối đa là ${params[0]} kí tự`; 32 | }); 33 | export default defineNuxtPlugin(() => {}); 34 | -------------------------------------------------------------------------------- /components/ui/select/SelectScrollDownButton.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /components/FreezeLoading.vue: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /components/ui/pagination/PaginationPrev.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 30 | -------------------------------------------------------------------------------- /components/OrganizationCardLite.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 27 | -------------------------------------------------------------------------------- /components/PostRow.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 27 | -------------------------------------------------------------------------------- /components/ui/pagination/PaginationLast.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 30 | -------------------------------------------------------------------------------- /components/ui/pagination/PaginationNext.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 30 | -------------------------------------------------------------------------------- /components/ui/calendar/CalendarCell.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /components/ui/pagination/PaginationFirst.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 30 | -------------------------------------------------------------------------------- /plugins/auth.client.ts: -------------------------------------------------------------------------------- 1 | import { firebaseAuth } from "~/lib/firebase"; 2 | import { useGetMyOrganizations, useGetMyProfile } from "~/lib/publiz"; 3 | 4 | export default defineNuxtPlugin((nuxtApp) => { 5 | const { data: myProfileResponse, refresh: refreshGetMyProfile } = 6 | useGetMyProfile({ 7 | immediate: false, 8 | }); 9 | const { data: myOrganizationsResponse, refresh: refreshGetMyOrganizations } = 10 | useGetMyOrganizations({ 11 | immediate: false, 12 | }); 13 | 14 | firebaseAuth.onAuthStateChanged(() => { 15 | refreshGetMyProfile(); 16 | refreshGetMyOrganizations(); 17 | }); 18 | 19 | const currentUser = computed(() => myProfileResponse.value?.data); 20 | const myOrganizations = computed( 21 | () => myOrganizationsResponse.value?.data || [] 22 | ); 23 | 24 | return { 25 | provide: { 26 | currentUser, 27 | myOrganizations, 28 | refreshGetMyProfile, 29 | }, 30 | }; 31 | }); 32 | -------------------------------------------------------------------------------- /components/ui/carousel/CarouselNext.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 31 | -------------------------------------------------------------------------------- /components/ui/carousel/CarouselPrevious.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 31 | -------------------------------------------------------------------------------- /components/ui/auto-form/index.ts: -------------------------------------------------------------------------------- 1 | export { getObjectFormSchema, getBaseSchema, getBaseType } from './utils' 2 | export type { Config, ConfigItem, FieldProps } from './interface' 3 | 4 | export { default as AutoForm } from './AutoForm.vue' 5 | export { default as AutoFormField } from './AutoFormField.vue' 6 | export { default as AutoFormLabel } from './AutoFormLabel.vue' 7 | 8 | export { default as AutoFormFieldArray } from './AutoFormFieldArray.vue' 9 | export { default as AutoFormFieldBoolean } from './AutoFormFieldBoolean.vue' 10 | export { default as AutoFormFieldDate } from './AutoFormFieldDate.vue' 11 | export { default as AutoFormFieldEnum } from './AutoFormFieldEnum.vue' 12 | export { default as AutoFormFieldFile } from './AutoFormFieldFile.vue' 13 | export { default as AutoFormFieldInput } from './AutoFormFieldInput.vue' 14 | export { default as AutoFormFieldNumber } from './AutoFormFieldNumber.vue' 15 | export { default as AutoFormFieldObject } from './AutoFormFieldObject.vue' 16 | -------------------------------------------------------------------------------- /components/NewStoryButton.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 29 | -------------------------------------------------------------------------------- /components/ui/badge/index.ts: -------------------------------------------------------------------------------- 1 | import { type VariantProps, cva } from 'class-variance-authority' 2 | 3 | export { default as Badge } from './Badge.vue' 4 | 5 | export const badgeVariants = cva( 6 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 7 | { 8 | variants: { 9 | variant: { 10 | default: 11 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', 12 | secondary: 13 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 14 | destructive: 15 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', 16 | outline: 'text-foreground', 17 | }, 18 | }, 19 | defaultVariants: { 20 | variant: 'default', 21 | }, 22 | }, 23 | ) 24 | 25 | export type BadgeVariants = VariantProps 26 | -------------------------------------------------------------------------------- /components/ui/form/useFormField.ts: -------------------------------------------------------------------------------- 1 | import { FieldContextKey, useFieldError, useIsFieldDirty, useIsFieldTouched, useIsFieldValid } from 'vee-validate' 2 | import { inject } from 'vue' 3 | import { FORM_ITEM_INJECTION_KEY } from './FormItem.vue' 4 | 5 | export function useFormField() { 6 | const fieldContext = inject(FieldContextKey) 7 | const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY) 8 | 9 | const fieldState = { 10 | valid: useIsFieldValid(), 11 | isDirty: useIsFieldDirty(), 12 | isTouched: useIsFieldTouched(), 13 | error: useFieldError(), 14 | } 15 | 16 | if (!fieldContext) 17 | throw new Error('useFormField should be used within ') 18 | 19 | const { name } = fieldContext 20 | const id = fieldItemContext 21 | 22 | return { 23 | id, 24 | name, 25 | formItemId: `${id}-form-item`, 26 | formDescriptionId: `${id}-form-item-description`, 27 | formMessageId: `${id}-form-item-message`, 28 | ...fieldState, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /components/ui/textarea/Textarea.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 |