elliots / vuesearchpal

Search 'palette' component for React, loosely based on MacOS Spotlight Search. Styled to look great out-of-box - pass through your own theme & elements. Autocomplete, search previews & more. Set to find exact and/or fuzzy matches.
https://searchpal.elijahharry.com/
0 stars 0 forks source link

Sweep: Sweep: Port this react component to vue3 #3

Open sweep-ai[bot] opened 3 months ago

sweep-ai[bot] commented 3 months ago

Description

This pull request ports a set of React components to Vue 3, making significant changes to adapt the React-specific hooks and component patterns to their Vue counterparts. The conversion includes updates to hook implementations and component definitions to align with Vue 3's Composition API and component model.

Summary

This migration ensures compatibility with Vue 3 and leverages Vue's reactivity system and Composition API for more concise and maintainable code.

Fixes #2.


💡 To get Sweep to edit this pull request, you can:

This is an automated message generated by Sweep AI.

sweep-ai[bot] commented 3 months ago

Rollback Files For Sweep

This is an automated message generated by Sweep AI.

sweep-ai[bot] commented 3 months ago

Sweep: PR Review

lib/src/hooks/useAnimatedRender.ts

The code changes convert a React hook to a Vue composition function, replacing React-specific hooks with Vue's reactive references and watchers.

Sweep Found These Issues

  • The watch on duration (line 11) does not correctly track changes because it uses a function that always returns the initial value of duration.
  • https://github.com/elliots/vuesearchpal/blob/6b381868dc58c774fda60abcf4a5c355834eaede/lib%2Fsrc%2Fhooks%2FuseAnimatedRender.ts#L11-L13 [View Diff](https://github.com/elliots/vuesearchpal/pull/3/files#diff-b9b585b4541cc8181a8ba5eeaf57e2a760e6ea18d72df2c12c4b1f172a936686R11-R13)

lib/src/hooks/useChildComponents.ts

The code changes refactor the useChildComponents hook from React to Vue, updating imports, function signatures, and internal logic to work with Vue's VNode and FunctionalComponent.

Sweep Found These Issues

  • The hasChildren function assumes that obj.children is always an array, which may not be the case in all Vue component structures.
  • https://github.com/elliots/vuesearchpal/blob/6b381868dc58c774fda60abcf4a5c355834eaede/lib%2Fsrc%2Fhooks%2FuseChildComponents.ts#L38-L41 [View Diff](https://github.com/elliots/vuesearchpal/pull/3/files#diff-a382e32be97e83d73069146a566bea7375776a56edd1cd86b595161ebb3f6bf4R38-R41)

lib/types/components.ts

The changes involve migrating component types from React to Vue, updating import statements, and modifying type definitions to be compatible with Vue.

Sweep Found These Issues

  • The change from PropsWithChildren to AllowedComponentProps in LinkProps may not fully replicate the behavior of PropsWithChildren, potentially causing issues with child components.
  • https://github.com/elliots/vuesearchpal/blob/6b381868dc58c774fda60abcf4a5c355834eaede/lib%2Ftypes%2Fcomponents.ts#L4-L7 [View Diff](https://github.com/elliots/vuesearchpal/pull/3/files#diff-2ff533391572e923a0739eb2ddcd8b871dbcc6d3bd3f8a18de3c3de467ae8300R4-R7)
  • The change from ReactElement<any, any> to VNode in PreviewProps may cause runtime errors if the media property is not correctly handled as a Vue VNode.
  • https://github.com/elliots/vuesearchpal/blob/6b381868dc58c774fda60abcf4a5c355834eaede/lib%2Ftypes%2Fcomponents.ts#L25 [View Diff](https://github.com/elliots/vuesearchpal/pull/3/files#diff-2ff533391572e923a0739eb2ddcd8b871dbcc6d3bd3f8a18de3c3de467ae8300R25)

elliots commented 3 months ago
sweep-ai[bot] commented 3 months ago

🚀 Resolved via 00357c4

Here were the changes I made:

--- README.md
+++ README.md
--- 
+++ 
@@ -102,52 +102,68 @@
 ```tsx
 import { Search, Option, Detail } from "searchpal";

-const UsersSearch = ({ users, session }) => {
-  const [open, setOpen] = useState(false);
-
-  return (
-    <>
-      <button onClick={() => setOpen(true)}>Search for a user</button>
-      <Search
-        label="Search for a user..."
-        open={open}
-        onClose={() => setOpen(false)}
-        link={({ href, children }) => <a href={href}>{children}</a>}
+<template>
+  <div>
+    <button @click="open = true">Search for a user</button>
+    <Search
+      label="Search for a user..."
+      :open="open"
+      @close="open = false"
+      :link="link"
+    >
+      <Option
+        v-for="user in users"
+        :key="user.id"
+        :label="user.name"
+        :sublabel="user.email"
+        :img="{ src: user.avatar, alt: `${user.name} profile picture` }"
+        :href="`/users/${user.id}`"
+        :keywords="(getKeywords) => getKeywords(
+          user.email,
+          user.social && user.social.handle,
+          user.organizations.map(org => [
+            org.name,
+            org.locations.map(location => [
+              location.city,
+              location.state
+            ])
+          ])
+        )"
       >
-        {users.map((user) => (
-          <Option
-            label={user.name}
-            sublabel={user.email}
-            img={{ src: user.avatar, alt: `${user.name} profile picture` }}
-            href={`/users/${user.id}`}
-            keywords={(getKeywords) =>
-              getKeywords(
-                user.email,
-                user.social && user.social.handle,
-                user.organizations.map((org) => [
-                  org.name,
-                  org.locations.map((location) => [
-                    location.city,
-                    location.state,
-                  ]),
-                ])
-              )
-            }
-            key={user.id}
-          >
-            <Detail label="Joined" value={user.joined} />
-            {user.organizations.length && (
-              <Detail
-                label="Organizations"
-                value={<Organizations items={user.organizations} />}
-              />
-            )}
-          </Option>
-        ))}
-      </Search>
-    </>
-  );
-};
+        <Detail label="Joined" :value="user.joined" />
+        <Detail
+          v-if="user.organizations.length"
+          label="Organizations"
+          :value="$options.components.Organizations({ items: user.organizations })"
+        />
+      </Option>
+    </Search>
+  </div>
+</template>
+
+<script>
+import { ref } from 'vue';
+import { Search, Option, Detail } from 'searchpal';
+
+export default {
+  components: {
+    Search,
+    Option,
+    Detail
+  },
+  props: ['users'],
+  setup() {
+    const open = ref(false);
+    
+    const link = ({ href, children }) => h('a', { href }, children);
+
+    return {
+      open,
+      link
+    };
+  }
+};
+</script>

Custom Search

--- demo/src/components/Demos/Demos.tsx +++ demo/src/components/Demos/Demos.tsx

--- demo/src/components/Demo/Demo.tsx +++ demo/src/components/Demo/Demo.tsx

--- demo/src/context/Demos.tsx +++ demo/src/context/Demos.tsx


+++ @@ -1,11 +1,6 @@ -import React, {

-export type DemoId = "general" | "button" | "link"; +export type DemoId = 'general' | 'button' | 'link';

export interface DemoValues { demo: DemoId | null; @@ -13,38 +8,34 @@ closeDemo: () => void; }

-const Demos = createContext({

-export function DemosProvider({ children }: PropsWithChildren<{}>) {



*This is an automated message generated by [Sweep AI](https://sweep.dev).*
elliots commented 3 months ago

the components in lib/src/components will need to be updated

sweep-ai[bot] commented 3 months ago

🚀 Resolved via 895120f

Here were the changes I made:

--- lib/src/components/Detail/Detail.tsx
+++ lib/src/components/Detail/Detail.tsx
--- 
+++ 
@@ -1,22 +1,31 @@
 import React, { FunctionComponent } from "react";
+import { defineComponent } from 'vue';
 import { DetailProps } from "../../../types";
-
 import { Label, Value } from "./Detail.styled";
 import { Link } from "../Search/Link";

-export const Detail: FunctionComponent<DetailProps> = ({
-  label,
-  href,
-  value,
-}) => {
-  return (
-    <>
-      <Label>{label}</Label>
-      <Value>
-        <Link href={href} target="_blank">
-          {value}
-        </Link>
-      </Value>
-    </>
-  );
-};
+export default defineComponent({
+  props: {
+    label: {
+      type: String,
+      required: true
+    },
+    href: String,
+    value: {
+      type: [String, Number, Object],
+      required: true
+    }
+  },
+  setup(props) {
+    return () => (
+      <>
+        <Label>{props.label}</Label>
+        <Value>
+          <Link href={props.href} target="_blank">
+            {props.value}
+          </Link>  
+        </Value>
+      </>
+    );
+  }
+});

--- lib/src/components/Search/Field/Field.tsx
+++ lib/src/components/Search/Field/Field.tsx
--- 
+++ 
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef } from "react";
+import { defineComponent, onMounted, ref, watch } from "vue";

 import { useSearch } from "../../../context";
 import { useFocus } from "../../../hooks";
@@ -7,55 +7,57 @@
 import { Container, Input, InputContainer, SvgIcon } from "./Field.styled";
 import { Label } from "../../Typography";

-export const Field = () => {
-  const { show, options, active, query, setQuery, ids, disableHover, label } =
-    useSearch();
+export default defineComponent({
+  setup() {
+    const { show, options, active, query, setQuery, ids, disableHover, label } = useSearch();
+    
+    const inputRef = ref<HTMLInputElement | null>(null);

-  const inputRef = useRef<HTMLInputElement>(null);
+    onMounted(() => {
+      if (show.value && inputRef.value) {
+        inputRef.value.focus();
+      }
+    });

-  useEffect(() => {
-    if (show && inputRef.current) inputRef.current.focus();
-  }, [show]);
+    watch(show, (newShow) => {
+      if (newShow && inputRef.value) {
+        inputRef.value.focus();
+      }
+    });

-  const [focus, focusProps] = useFocus(() => disableHover());
+    const { focus, focusProps } = useFocus(() => disableHover());

-  return (
-    <Container>
-      <SvgIcon
-        viewBox="0 0 24 24"
-        aria-hidden="true"
-        xmlns="http://www.w3.org/2000/svg"
-      >
-        <path d="M19.023 16.977a35.13 35.13 0 0 1-1.367-1.384c-.372-.378-.596-.653-.596-.653l-2.8-1.337A6.962 6.962 0 0 0 16 9c0-3.859-3.14-7-7-7S2 5.141 2 9s3.14 7 7 7c1.763 0 3.37-.66 4.603-1.739l1.337 2.8s.275.224.653.596c.387.363.896.854 1.384 1.367l1.358 1.392.604.646 2.121-2.121-.646-.604c-.379-.372-.885-.866-1.391-1.36zM9 14c-2.757 0-5-2.243-5-5s2.243-5 5-5 5 2.243 5 5-2.243 5-5 5z" />
-      </SvgIcon>
-      <InputContainer>
-        {/* <InputMain> */}
-        <Label htmlFor={ids.search} sr-only>
-          {label}
-        </Label>
-        {focus && <Suggestion />}
-        <Input
-          type="text"
-          placeholder={label}
-          ref={inputRef}
-          role="combobox"
-          aria-expanded={options.length > 0}
-          aria-controls={ids.options}
-          id={ids.search}
-          aria-activedescendant={active ? ids.getOptionId(active) : undefined}
-          value={query}
-          onChange={(e) => {
-            disableHover();
-            setQuery(e.target.value);
-          }}
-          {...(options.length < 1 && query
-            ? { "aria-describedby": cn(ids.errors), "aria-invalid": true }
-            : {})}
-          {...focusProps}
-          autoComplete="off"
-        />
-        {/* </InputMain> */}
-      </InputContainer>
-    </Container>
-  );
-};
+    return () => (
+      <Container>
+        <SvgIcon viewBox="0 0 24 24" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
+          <path d="M19.023 16.977a35.13 35.13 0 0 1-1.367-1.384c-.372-.378-.596-.653-.596-.653l-2.8-1.337A6.962 6.962 0 0 0 16 9c0-3.859-3.14-7-7-7S2 5.141 2 9s3.14 7 7 7c1.763 0 3.37-.66 4.603-1.739l1.337 2.8s.275.224.653.596c.387.363.896.854 1.384 1.367l1.358 1.392.604.646 2.121-2.121-.646-.604c-.379-.372-.885-.866-1.391-1.36zM9 14c-2.757 0-5-2.243-5-5s2.243-5 5-5 5 2.243 5 5-2.243 5-5 5z" />
+        </SvgIcon>
+        <InputContainer>
+          <Label htmlFor={ids.search.value} sr-only>
+            {label.value}
+          </Label>
+          {focus.value && <Suggestion />}
+          <Input
+            type="text"
+            placeholder={label.value}
+            ref={inputRef}
+            role="combobox"
+            aria-expanded={options.value.length > 0}
+            aria-controls={ids.options.value}
+            id={ids.search.value}
+            aria-activedescendant={active.value ? ids.getOptionId(active.value) : undefined}
+            value={query.value}
+            onInput={(e: Event) => {
+              disableHover();
+              setQuery((e.target as HTMLInputElement).value);
+            }}
+            aria-describedby={options.value.length < 1 && query.value ? cn(ids.errors.value) : undefined}
+            aria-invalid={options.value.length < 1 && query.value ? true : undefined}
+            {...focusProps.value}
+            autoComplete="off"
+          />
+        </InputContainer>
+      </Container>
+    );
+  }
+});

--- lib/src/components/Search/Field/Suggestion/Suggestion.tsx
+++ lib/src/components/Search/Field/Suggestion/Suggestion.tsx
--- 
+++ 
@@ -1,37 +1,40 @@
+import { defineComponent, computed } from 'vue';
 import { useSearch } from "../../../../context";
-import React, { useMemo } from "react";
 import { Content, Text } from "./Suggestion.styled";

-export const Suggestion = () => {
-  const { suggestion, query } = useSearch();
+export default defineComponent({
+  setup() {
+    const { suggestion, query } = useSearch();

-  const suggest = useMemo(() => {
-    if (suggestion) {
-      const label = suggestion.label,
-        query_ = query.toLowerCase(),
-        label_ = label.toLowerCase();
+    const suggest = computed(() => {
+      if (suggestion.value) {
+        const label = suggestion.value.label;
+        const query_ = query.value.toLowerCase();
+        const label_ = label.toLowerCase();

-      if (label_.includes(query_) && label_.indexOf(query_) === 0) {
-        if (query.length === label.length) {
-          return null;
+        if (label_.includes(query_) && label_.indexOf(query_) === 0) {
+          if (query.value.length === label.length) {
+            return null;
+          }
+          return [query.value, label.slice(query.value.length)];
         }
-        return [query, label.slice(query.length)];
       }
-    }
-    return null;
-  }, [suggestion, query]);
+      return null;
+    });

-  if (suggest) {
-    return (
-      <Content aria-hidden>
-        {suggest.map((str, i) => (
-          <Text visible={i > 0} key={`${i}-${str}`}>
-            {str}
-          </Text>
-        ))}
-      </Content>
-    );
+    return () => {
+      if (suggest.value) {
+        return (
+          <Content aria-hidden>
+            {suggest.value.map((str, i) => (
+              <Text visible={i > 0} key={`${i}-${str}`}>
+                {str}
+              </Text>
+            ))}
+          </Content>
+        );
+      }
+      return null;
+    };
   }
-
-  return null;
-};
+});

--- lib/src/components/Search/Results/Preview/Button/Button.tsx
+++ lib/src/components/Search/Results/Preview/Button/Button.tsx
--- 
+++ 
@@ -1,25 +1,97 @@
-import React, { MouseEventHandler } from "react";
+import { defineComponent } from 'vue';
+import { useSearch } from "../../../../../context";
+import { ButtonComponent } from "../../../../../types";
 import styled from "styled-components";
-import { Renderable } from "../../../../../../types";
 import { getVar } from "../../../../../../utils/css";
-
 import { Span } from "../../../../Typography";

-export const Button = ({
-  onClick,
-  cta,
-}: {
-  onClick: MouseEventHandler<HTMLButtonElement>;
-  cta: Renderable;
-}) => {
-  return (
-    <ButtonBase type="button" onClick={onClick}>
-      <Label weight="bold" truncate align="center">
-        {cta}
-      </Label>
-    </ButtonBase>
-  );
-};
+const Label = styled(Span)`
+  position: relative;
+  z-index: 5;
+  /* text-transform: uppercase; */
+  display: inline-flex;
+  align-items: center;
+  color: ${getVar("accent-txt")};
+`;
+
+const ButtonBase = styled.button`
+  padding: 0.7rem 0.8rem;
+  width: 100%;
+  transition: 250ms;
+  background-size: 200% auto;
+  border-radius: 10px;
+  display: block;
+  border: 0px;
+  cursor: pointer;
+  user-select: none;
+  -webkit-user-select: none;
+  touch-action: manipulation;
+  position: relative;
+  background-color: ${getVar("accent")};
+  &::after {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    transition: 0.5s;
+    background: linear-gradient(
+      30deg,
+      transparent 0%,
+      transparent 20%,
+      #fff 60%,
+      transparent 100%
+    );
+    width: 100%;
+    height: 100%;
+    opacity: 0.1;
+  }
+  &:hover {
+    background-position: right center;
+    text-decoration: none;
+    &::after {
+      transform: scaleX(1.5) translateX(-10%);
+      opacity: 0.2;
+    }
+  }
+  &:active {
+    transform: scale(0.975);
+  }
+`;
+
+export default defineComponent({
+  props: {
+    cta: {
+      type: [String, Object],
+      required: true
+    },
+    onClick: {
+      type: Function,
+      required: true
+    },
+    label: {
+      type: String,
+      required: true
+    }
+  },
+  setup(props) {
+    const { link: Link } = useSearch();
+
+    const button = (
+      <ButtonBase onClick={props.onClick} type="button">
+        <Label weight="bold" truncate align="center">
+          {props.cta}
+        </Label>
+      </ButtonBase>
+    );
+
+    return () => {
+      if (Link) {
+        return <Link href={`/${props.label}`}>{button}</Link>;
+      }
+      return button;  
+    };
+  }
+}) as ButtonComponent;

 const Label = styled(Span)`
   position: relative;

--- lib/src/components/Search/Results/Preview/PreviewMedia/PreviewMedia.tsx
+++ lib/src/components/Search/Results/Preview/PreviewMedia/PreviewMedia.tsx
--- 
+++ 
@@ -1,53 +1,49 @@
-import React from "react";
+import { defineComponent } from 'vue';
+import { MediaComponent } from "../../../../../types";
 import styled from "styled-components";
 import { classes, mediaQuery } from "../../../../../../utils/css";
-import { Searchable } from "../../../../../../types";
 import { H2, Paragraph } from "../../../../Typography";
-import { isFunction } from "../../../../../../utils";

-export const PreviewMedia = ({
-  media: CustomMedia,
-  img,
-  label,
-  sublabel,
-}: Pick<Searchable, "media" | "img" | "label" | "sublabel">) => {
-  return (
-    <Container>
-      {(CustomMedia || img) && (
-        <Media>
-          {CustomMedia ? (
-            isFunction(CustomMedia) ? (
-              <CustomMedia
-                img={img}
-                label={label}
-                active={false}
-                hovered={false}
-                focused={false}
-              />
-            ) : (
-              CustomMedia
-            )
-          ) : (
-            img && (
-              <Image
-                src={img?.src}
-                {...(img?.alt ? { alt: img.alt } : { "aria-hidden": true })}
-              />
-            )
-          )}
-        </Media>
-      )}
-      <Title weight="medium" size="md" align="center">
-        {label}
-      </Title>
-      {sublabel && (
-        <Subtitle size="sm" fade align="center">
-          {sublabel}
-        </Subtitle>
-      )}
-    </Container>
-  );
-};
+const Media = styled.div`
+  margin-bottom: 0.375rem;
+  height: 3.5rem;
+  width: 3.5rem;
+  overflow: hidden;
+  border-radius: 9999px;
+  background-color: rgb(243, 244, 246);
+  ${mediaQuery("md")} {
+    margin-bottom: 0.5rem;
+    height: 4rem;
+    width: 4rem;
+  }
+`;
+
+const Image = styled.img`
+  height: 100%;
+  width: 100%;
+  -o-object-fit: cover;
+  object-fit: cover;
+  -o-object-position: center;
+  object-position: center;
+`;
+
+export default defineComponent({
+  props: {
+    img: Object,
+    media: Object,
+    label: {
+      type: String,
+      required: true
+    }
+  },
+  setup(props) {
+    return () => {
+      if (props.media) return props.media;
+      if (props.img) return <Media src={props.img.src} alt={props.img.alt || props.label} />;
+      return null;
+    };
+  }
+}) as MediaComponent;

 const Container = styled.div`
   display: flex;

This is an automated message generated by Sweep AI.

elliots commented 3 months ago

the dependencies need updating lib/src/hooks needs updating

search for all files containing the word "react" to see which needs updating to vue

sweep-ai[bot] commented 3 months ago

🚀 Resolved via c5f6dbf

Here were the changes I made:

--- package.json
+++ package.json

--- lib/src/hooks/useChildComponents.ts
+++ lib/src/hooks/useChildComponents.ts

--- lib/types/components.ts
+++ lib/types/components.ts

This is an automated message generated by Sweep AI.