📦 EqualifyEverything / equalify

📄 AuditRemoteCsvInput.tsx · 177 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177import { useEffect, useState } from "react";
import { useGlobalStore } from "../utils";
import style from "./AuditRemoteCsvInput.module.scss";
import { Card } from "./Card";
import { StyledLabeledInput } from "./StyledLabeledInput";
import { useDebouncedCallback } from "use-debounce";

import * as API from "aws-amplify/api";
import { MdCheckCircle, MdError } from "react-icons/md";
import { AuditPagesInputTable } from "./AuditPagesInputTable";

interface Page {
  url: string;
  type: "html" | "pdf";
  id?: string;
}
interface ChildProps {
  csvUrl: string
  setCsvUrl: (value: React.SetStateAction<string>) => void
  validCsv: boolean
  setValidCsv: (value: React.SetStateAction<boolean>) => void
  pages: Page[]
  setParentPages: (newValue: Page[]) => void; // Callback function prop

}

// test file: https://equalify.app/wp-content/uploads/2026/02/url-import-template.csv

export const AuditRemoteCsvInput: React.FC<ChildProps> = ({ csvUrl, setCsvUrl, validCsv, setValidCsv, setParentPages, pages
}) => {
  const { setAnnounceMessage } = useGlobalStore();
  const [error, setError] = useState<string | null>("");

  /**
   * Normalize a URL by removing trailing slashes from the path
   * This ensures URLs like "https://example.com/" and "https://example.com" are treated as the same
   */
  const normalizeUrl = (url: string): string => {
    // Remove trailing slash unless it's just the root path
    return url.replace(/\/+$/, '') || url;
  };

  const validateAndFormatUrl = (input: string): string | null => {
    // Trim whitespace
    let url = input.trim();
    if (!url) return null;

    // Add https:// if no protocol is specified
    if (!url.match(/^https?:\/\//i)) {
      url = "https://" + url;
    }

    // Validate URL format
    try {
      const urlObj = new URL(url);
      // Check if it's http or https
      if (!["http:", "https:"].includes(urlObj.protocol)) {
        setError("Only HTTP and HTTPS URLs are supported");
        return null;
      }
      setError(null);
      // Normalize the URL to remove trailing slashes
      return normalizeUrl(urlObj.href);
    } catch {
      setError(
        "Invalid URL format. Please enter a valid URL (e.g., example.com or https://example.com)"
      );
      return null;
    }
  };

  /**
   * Parse a line that may contain a URL and optional type separated by comma
   * Format: url,type (e.g., "https://example.com,html" or "https://example.com/doc.pdf,pdf")
   * If no type specified, defaults to "html"
   */
  const parseUrlWithType = (line: string): { url: string; type: "html" | "pdf" } => {
    const trimmedLine = line.trim();

    // Check if the line ends with ,html or ,pdf (case-insensitive)
    const htmlMatch = trimmedLine.match(/^(.+),\s*html\s*$/i);
    const pdfMatch = trimmedLine.match(/^(.+),\s*pdf\s*$/i);

    if (pdfMatch) {
      return { url: pdfMatch[1].trim(), type: "pdf" };
    }
    if (htmlMatch) {
      return { url: htmlMatch[1].trim(), type: "html" };
    }

    // No type specified, default to html
    return { url: trimmedLine, type: "html" };
  };

  const updatePages = (lines: Page[]) => {
    const newPages: Page[] = [];
    const errors = [];

    lines.forEach((line, index) => {

      // Parse line for URL and optional type (format: url,type)
      const { url: rawUrl, type: pageType } = line;

      // Validate and format URL
      const validUrl = validateAndFormatUrl(rawUrl);
      if (!validUrl) {
        errors.push(`Line ${index + 1}: Invalid URL format`);
        return;
      }

      newPages.push({ url: validUrl, type: pageType });
    });
    //console.log(newPages);
    setParentPages(newPages)
  }


  useEffect(() => {
    (async () => {
      if (csvUrl.length <= 5) {
        setError("");
        return;
      }
      console.log("Fetching and validating CSV...", csvUrl);
      const fetchedCsv = await fetchAndValidateCsv();
      setError(fetchedCsv.error);
      if (fetchedCsv.success) {
        setValidCsv(true);
        const updatedPages = updatePages(fetchedCsv.data);
      }
    })()
  }, [csvUrl])

  const fetchAndValidateCsv = async () => {

    const response = await API.get({
      apiName: "auth",
      path: "/fetchAndValidateRemoteCsv",
      options: {
        queryParams: { url: csvUrl.trim() },
      },
    }).response;
    const resp = (await response.body.json()) as any;
    console.log(resp);
    return resp;
  }

  return (
    <div className={style.AuditRemoteCsvInput}>
      <Card variant="inset-light">
        <h3>Use a Remote CSV Integration</h3>
        <p className="font-small">
          Provide a link to a hosted CSV to automatically sync your URL list before every scan.
        </p>
        <StyledLabeledInput>
          <label htmlFor="remote-csv-url">URL of Remote CSV</label>
          <input id="remote-csv-url" type="url" value={csvUrl} onChange={(e) => { setCsvUrl(e.target.value) }} />
        </StyledLabeledInput>
        {error &&
          <Card variant="short-error"><MdError className="icon-small" /><div className="font-small"><b>There was a problem with your CSV.</b>{error}</div></Card>
        }
        {validCsv &&
          <Card variant="short-success"><MdCheckCircle className="icon-small" /><div className="font-small"><b>CSV Found!</b></div></Card>
        }
        {pages.length > 0 &&
          <AuditPagesInputTable
            pages={pages}
            removePages={() => { }}
            isShared={true}
            updatePageType={() => { }}
          />
        }
      </Card>
    </div>
  );
};