aboutsummaryrefslogtreecommitdiff
path: root/apps/web/lib
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/lib')
-rw-r--r--apps/web/lib/fonts.ts55
-rw-r--r--apps/web/lib/url-helpers.ts190
2 files changed, 245 insertions, 0 deletions
diff --git a/apps/web/lib/fonts.ts b/apps/web/lib/fonts.ts
new file mode 100644
index 00000000..dd13c6b5
--- /dev/null
+++ b/apps/web/lib/fonts.ts
@@ -0,0 +1,55 @@
+import { DM_Mono, DM_Sans } from "next/font/google"
+import { cn } from "@lib/utils"
+
+// DM Sans font
+export const dmSansFont = DM_Sans({
+ subsets: ["latin"],
+ weight: ["400", "500", "700"],
+ variable: "--font-dm-sans",
+})
+
+// DM Mono font
+export const dmMonoFont = DM_Mono({
+ subsets: ["latin"],
+ weight: ["400"],
+ variable: "--font-dm-mono",
+})
+
+/**
+ * Utility function that combines dmSansFont.className with required typography styles
+ * (letter-spacing: -0.01em and line-height: 135%)
+ */
+export function dmSansClassName(additionalClasses?: string) {
+ return cn(
+ dmSansFont.className,
+ "tracking-[-0.01em]",
+ "leading-[135%]",
+ additionalClasses,
+ )
+}
+
+/**
+ * Utility function that combines dmSansFont.className with required typography styles
+ * (letter-spacing: -0.01em and line-height: 125%)
+ */
+export function dmSans125ClassName(additionalClasses?: string) {
+ return cn(
+ dmSansFont.className,
+ "tracking-[-0.01em]",
+ "leading-[125%]",
+ additionalClasses,
+ )
+}
+
+/**
+ * Utility function that combines dmMonoFont.className with required typography styles
+ * (letter-spacing: -0.01em and line-height: 135%)
+ */
+export function dmMonoClassName(additionalClasses?: string) {
+ return cn(
+ dmMonoFont.className,
+ "tracking-[-0.01em]",
+ "leading-[135%]",
+ additionalClasses,
+ )
+}
diff --git a/apps/web/lib/url-helpers.ts b/apps/web/lib/url-helpers.ts
new file mode 100644
index 00000000..e4147a05
--- /dev/null
+++ b/apps/web/lib/url-helpers.ts
@@ -0,0 +1,190 @@
+/**
+ * Validates if a string is a valid URL.
+ */
+export const isValidUrl = (url: string): boolean => {
+ try {
+ new URL(url)
+ return true
+ } catch {
+ return false
+ }
+}
+
+/**
+ * Normalizes a URL by adding https:// prefix if missing.
+ */
+export const normalizeUrl = (url: string): string => {
+ if (!url.trim()) return ""
+ if (url.startsWith("http://") || url.startsWith("https://")) {
+ return url
+ }
+ return `https://${url}`
+}
+
+/**
+ * Checks if a URL is a Twitter/X URL.
+ */
+export const isTwitterUrl = (url: string): boolean => {
+ const normalizedUrl = url.toLowerCase()
+ return (
+ normalizedUrl.includes("twitter.com") || normalizedUrl.includes("x.com")
+ )
+}
+
+/**
+ * Checks if a URL is a LinkedIn profile URL (not a company page).
+ */
+export const isLinkedInProfileUrl = (url: string): boolean => {
+ const normalizedUrl = url.toLowerCase()
+ return (
+ normalizedUrl.includes("linkedin.com/in/") &&
+ !normalizedUrl.includes("linkedin.com/company/")
+ )
+}
+
+/**
+ * Collects and validates URLs from LinkedIn profile and other links, excluding Twitter.
+ */
+export const collectValidUrls = (
+ linkedinProfile: string,
+ otherLinks: string[],
+): string[] => {
+ const urls: string[] = []
+
+ if (linkedinProfile.trim()) {
+ const normalizedLinkedIn = normalizeUrl(linkedinProfile.trim())
+ if (
+ isValidUrl(normalizedLinkedIn) &&
+ isLinkedInProfileUrl(normalizedLinkedIn)
+ ) {
+ urls.push(normalizedLinkedIn)
+ }
+ }
+
+ otherLinks
+ .filter((link) => link.trim())
+ .forEach((link) => {
+ const normalizedLink = normalizeUrl(link.trim())
+ if (isValidUrl(normalizedLink) && !isTwitterUrl(normalizedLink)) {
+ urls.push(normalizedLink)
+ }
+ })
+
+ return urls
+}
+
+/**
+ * Extracts X/Twitter handle from various input formats (URLs, handles with @, etc.).
+ */
+export function parseXHandle(input: string): string {
+ if (!input.trim()) return ""
+
+ let value = input.trim()
+
+ if (value.startsWith("@")) {
+ value = value.slice(1)
+ }
+
+ const lowerValue = value.toLowerCase()
+ if (lowerValue.includes("x.com") || lowerValue.includes("twitter.com")) {
+ try {
+ let url: URL
+ if (value.startsWith("http://") || value.startsWith("https://")) {
+ url = new URL(value)
+ } else {
+ url = new URL(`https://${value}`)
+ }
+
+ const pathSegments = url.pathname.split("/").filter(Boolean)
+ if (pathSegments.length > 0) {
+ const firstSegment = pathSegments[0]
+ if (firstSegment && firstSegment !== "status" && firstSegment !== "i") {
+ return firstSegment
+ }
+ }
+ } catch {
+ const match = value.match(/(?:x\.com|twitter\.com)\/([^/\s?#]+)/i)
+ const handle = match?.[1]
+ if (handle && handle !== "status") {
+ return handle
+ }
+ }
+ }
+
+ if (
+ value.includes("/") &&
+ !lowerValue.includes("x.com") &&
+ !lowerValue.includes("twitter.com")
+ ) {
+ const parts = value.split("/").filter(Boolean)
+ const firstPart = parts[0]
+ if (firstPart) {
+ return firstPart
+ }
+ }
+
+ return value
+}
+
+/**
+ * Extracts LinkedIn handle from various input formats (URLs, handles with @, etc.).
+ */
+export function parseLinkedInHandle(input: string): string {
+ if (!input.trim()) return ""
+
+ let value = input.trim()
+
+ if (value.startsWith("@")) {
+ value = value.slice(1)
+ }
+
+ const lowerValue = value.toLowerCase()
+ if (lowerValue.includes("linkedin.com")) {
+ try {
+ let url: URL
+ if (value.startsWith("http://") || value.startsWith("https://")) {
+ url = new URL(value)
+ } else {
+ url = new URL(`https://${value}`)
+ }
+
+ const pathMatch = url.pathname.match(/\/(in|pub)\/([^/\s?#]+)/i)
+ const handle = pathMatch?.[2]
+ if (handle) {
+ return handle
+ }
+ } catch {
+ const match = value.match(/linkedin\.com\/(?:in|pub)\/([^/\s?#]+)/i)
+ const handle = match?.[1]
+ if (handle) {
+ return handle
+ }
+ }
+ }
+
+ if (value.includes("/in/") || value.includes("/pub/")) {
+ const match = value.match(/\/(?:in|pub)\/([^/\s?#]+)/i)
+ const handle = match?.[1]
+ if (handle) {
+ return handle
+ }
+ }
+
+ return value
+}
+
+/**
+ * Converts X/Twitter handle to full profile URL.
+ */
+export function toXProfileUrl(handle: string): string {
+ if (!handle.trim()) return ""
+ return `https://x.com/${handle.trim()}`
+}
+
+/**
+ * Converts LinkedIn handle to full profile URL.
+ */
+export function toLinkedInProfileUrl(handle: string): string {
+ if (!handle.trim()) return ""
+ return `https://linkedin.com/in/${handle.trim()}`
+}