mirror of
https://github.com/hilmanski/contentswift.git
synced 2026-08-24 10:05:45 -05:00
make navigation easier
This commit is contained in:
@@ -177,6 +177,7 @@ def _scrape_article(link, lang):
|
||||
article.parse()
|
||||
content = article.text
|
||||
content_html = article.article_html
|
||||
content_title = article.title
|
||||
|
||||
# Current logic to detect if it's blocked
|
||||
# is by check if text results is very short
|
||||
@@ -246,6 +247,7 @@ def _scrape_article(link, lang):
|
||||
"link": link,
|
||||
"totalWords": totalWords,
|
||||
"keywords": keywords,
|
||||
"title": content_title,
|
||||
"headings": headings,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Terms from "@/components/Terms";
|
||||
import TextEditor from "@/components/TextEditor";
|
||||
import { useEffect, useState } from "react";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
async function getData(id: string) {
|
||||
const res = await fetch(`http://localhost:8000/posts/${id}`)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch data')
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
|
||||
export default async function Editor({
|
||||
export default function Editor({
|
||||
children, params
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
@@ -21,164 +14,64 @@ export default async function Editor({
|
||||
id: string;
|
||||
}
|
||||
}) {
|
||||
const data = await getData(params.id);
|
||||
const searchResult = data.search_result
|
||||
const [data, setData] = useState({})
|
||||
const [searchResult, setSearchResult] = useState({})
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const res = await fetch(`http://localhost:8000/posts/${params.id}`)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch data')
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
setLoading(false)
|
||||
setData(data)
|
||||
setSearchResult(data.search_result)
|
||||
}
|
||||
|
||||
fetchData()
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="my-10 max-w-7xl mx-auto">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="my-10 max-w-7xl mx-auto">
|
||||
|
||||
<div className="flex justify-between mb-5">
|
||||
<p className="text-xl">
|
||||
Title: {data.title}
|
||||
Text Editor
|
||||
</p>
|
||||
|
||||
<Link href="/" className="underline">
|
||||
Home
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
|
||||
<section className="fixed">
|
||||
<p>Menu</p>
|
||||
|
||||
<div className="text-sm">
|
||||
<a className="block mt-2" href="#answer-box">#Google Results</a>
|
||||
<a className="block mt-2" href="#related-questions">#Related Questions</a>
|
||||
<a className="block mt-2" href="#autocomplete">#Autocomplete</a>
|
||||
<a className="block mt-2" href="#related-searches">#Related Searches</a>
|
||||
<a className="block mt-2" href="#terms">#Terms</a>
|
||||
<a className="block mt-2" href="#outline">#Outline</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex justify-between">
|
||||
|
||||
<section className="ml-[200px] w-3/12 max-h-screen overflow-y-scroll">
|
||||
|
||||
|
||||
{
|
||||
searchResult.answer_box && (
|
||||
<section
|
||||
id="answer-box"
|
||||
className="my-5 p-3 border border-emerald-600 text-sm">
|
||||
<details>
|
||||
<summary>
|
||||
<h3 className="inline-block text-emerald-700 ">Answer Box (Featued Snippet)</h3>
|
||||
</summary>
|
||||
<p className="italic">
|
||||
{
|
||||
searchResult.answer_box.snippet
|
||||
}
|
||||
</p>
|
||||
|
||||
{
|
||||
searchResult.answer_box.list && (
|
||||
<div>
|
||||
<p className="text-bold italic">Info: Features snippet is a list</p>
|
||||
{
|
||||
searchResult.answer_box.list.map((item, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<p>{item}</p>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<p>
|
||||
<a className="text-sky-700"
|
||||
href={searchResult.answer_box.link}>
|
||||
source
|
||||
</a>
|
||||
</p>
|
||||
</details>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
searchResult.related_questions.length > 0 && (
|
||||
<section
|
||||
id="related-questions"
|
||||
className="my-5 p-3 border border-emerald-600 text-sm">
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<h3 className="inline-block text-emerald-700 mb-2">Related Questions</h3>
|
||||
</summary>
|
||||
{
|
||||
searchResult.related_questions.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className="mb-1">
|
||||
<p className="font-bold"> {item.question} </p>
|
||||
<p className="italic"> {item.snippet} </p>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</details>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
searchResult.autocomplete.length > 0 && (
|
||||
<section
|
||||
id="autocomplete"
|
||||
className="my-5 p-3 border border-emerald-600 text-sm">
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<h3 className="inline-block text-emerald-700 mb-2">Autocomplete</h3>
|
||||
</summary>
|
||||
{
|
||||
searchResult.autocomplete.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className="mb-1">
|
||||
{item.value}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</details>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
searchResult.related_searches.length > 0 && (
|
||||
<section
|
||||
id="related-searches"
|
||||
className="my-5 p-3 border border-emerald-600 text-sm">
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<h3 className="inline-block text-emerald-700 mb-2">Related Searches</h3>
|
||||
</summary>
|
||||
|
||||
{
|
||||
searchResult.related_searches.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className="mb-1">
|
||||
<p className=""> {item.query} </p>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</details>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
<Terms id={params.id} />
|
||||
</section>
|
||||
|
||||
<section className="flex justify-betwee">
|
||||
|
||||
<TextEditor
|
||||
id={params.id}
|
||||
prevContent={data.content} />
|
||||
prevContent={data.content}
|
||||
title={data.title} />
|
||||
|
||||
|
||||
<Sidebar
|
||||
id={params.id}
|
||||
searchResult={searchResult}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
export default function Outline({result} : {result: any}) {
|
||||
|
||||
return (
|
||||
<section className="my-5">
|
||||
|
||||
{
|
||||
result.contentInfo && (
|
||||
<div
|
||||
className="my-5 text-sm">
|
||||
<h3 className="mb-5 text-xl font-bold">Competitor reference</h3>
|
||||
<p> Average words length: {result.averageWords} </p>
|
||||
<div className="my-5">
|
||||
{
|
||||
result.contentInfo.map((site: string, index: number) => {
|
||||
return (
|
||||
<div key={index}
|
||||
className="border-b-2 pb-5">
|
||||
<p className="border-l-2 border-l-black pl-2 my-2">
|
||||
<a className="text-sky-700"
|
||||
href={site.link}>
|
||||
{site.title}...
|
||||
</a>
|
||||
</p>
|
||||
<p className="mb-2 text-gray-600">
|
||||
src: {site.link}
|
||||
</p>
|
||||
<p>
|
||||
Words: {site.totalWords}
|
||||
</p>
|
||||
|
||||
{
|
||||
site.headings.length > 0 && (
|
||||
<div>
|
||||
<p>Headings</p>
|
||||
{
|
||||
site.headings.map((heading: string, index: number) => {
|
||||
return (
|
||||
<p key={index}>
|
||||
{heading.tag}: {heading.text}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
site.keywords.length > 0 && (
|
||||
<div>
|
||||
<p>keywords</p>
|
||||
{
|
||||
site.keywords.map((keyword: string, index: number) => {
|
||||
return (
|
||||
<li key={index}
|
||||
className="inline-block bg-gray-200 mb-1 rounded-full px-3 py-1 text-sm text-gray-700 mr-2">
|
||||
|
||||
{
|
||||
Array.isArray(keyword.word) ? keyword.word.join(' ') : keyword.word
|
||||
} :
|
||||
{keyword.frequency}
|
||||
</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
"use client"
|
||||
import { useEffect, useState } from "react";
|
||||
import Terms from "./Terms";
|
||||
import Outline from "./Outline";
|
||||
|
||||
export default function Sidebar({
|
||||
id, searchResult
|
||||
}: {
|
||||
id: string,
|
||||
searchResult: any
|
||||
}) {
|
||||
const [menu, setMenu] = useState<string>('terms')
|
||||
|
||||
const [result, setResult] = useState({})
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const res = await fetch(`http://localhost:8000/scrape/${id}`)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch data')
|
||||
}
|
||||
|
||||
const _data = await res.json()
|
||||
setResult(_data.data)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
fetchData()
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
}, [])
|
||||
|
||||
|
||||
return (
|
||||
<section className="ml-5 w-3/12 bg-gray-100 p-5">
|
||||
|
||||
<section className="flex space-x-4 border-b-2 pb-3">
|
||||
{/* <p>Terms</p> */}
|
||||
<p onClick={() => {setMenu('terms')}}
|
||||
className={`
|
||||
cursor-pointer hover:text-emerald-500
|
||||
${menu === 'terms' ? 'text-emerald-700 border-b-2 border-b-emerald-700' : ''}
|
||||
`}>Terms</p>
|
||||
<p onClick={() => {setMenu('research')}}
|
||||
className={`
|
||||
cursor-pointer hover:text-emerald-500
|
||||
${menu === 'research' ? 'text-emerald-700 border-b-2 border-b-emerald-700' : ''}
|
||||
`}>
|
||||
Research</p>
|
||||
<p onClick={() => {setMenu('outline')}}
|
||||
className={`
|
||||
cursor-pointer hover:text-emerald-500
|
||||
${menu === 'outline' ? 'text-emerald-700 border-b-2 border-b-emerald-700' : ''}
|
||||
`}>Outline</p>
|
||||
</section>
|
||||
|
||||
{
|
||||
menu == 'research' && (
|
||||
<section>
|
||||
{
|
||||
searchResult.answer_box && (
|
||||
<section className="my-5 text-sm">
|
||||
<h3 className="inline-block text-emerald-700 ">Answer Box (Featued Snippet)</h3>
|
||||
<p className="italic">
|
||||
{
|
||||
searchResult.answer_box.snippet
|
||||
}
|
||||
</p>
|
||||
|
||||
{
|
||||
searchResult.answer_box.list && (
|
||||
<div>
|
||||
<p className="text-bold italic">Info: Features snippet is a list</p>
|
||||
{
|
||||
searchResult.answer_box.list.map((item, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<p>{item}</p>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<p>
|
||||
<a className="text-sky-700"
|
||||
href={searchResult.answer_box.link}>
|
||||
source
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
searchResult.related_questions.length > 0 && (
|
||||
<section className="my-5 text-sm">
|
||||
|
||||
<h3 className="inline-block text-emerald-700 mb-2">Related Questions</h3>
|
||||
|
||||
{
|
||||
searchResult.related_questions.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className="mb-1">
|
||||
<details>
|
||||
<summary>
|
||||
<p className="inline"> {item.question} </p>
|
||||
</summary>
|
||||
<p className="italic"> {item.snippet} </p>
|
||||
</details>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
searchResult.autocomplete.length > 0 && (
|
||||
<section className="my-5 text-sm">
|
||||
|
||||
<h3 className="inline-block text-emerald-700 mb-2">Autocomplete</h3>
|
||||
|
||||
{
|
||||
searchResult.autocomplete.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className="mb-1">
|
||||
{item.value}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
searchResult.related_searches.length > 0 && (
|
||||
<section className="my-5 text-sm">
|
||||
|
||||
<h3 className="inline-block text-emerald-700 mb-2">Related Searches</h3>
|
||||
{
|
||||
searchResult.related_searches.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className="mb-1">
|
||||
<p className=""> {item.query} </p>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{
|
||||
menu == 'terms' && (
|
||||
<Terms result={result} />
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
menu == 'outline' && (
|
||||
<Outline result={result} />
|
||||
)
|
||||
}
|
||||
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,41 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Terms({id} : {id: string}) {
|
||||
|
||||
const [result, setResult] = useState({} as any)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
getData(id).then((data) => {
|
||||
setResult(data.data)
|
||||
})
|
||||
}, [])
|
||||
|
||||
async function getData(id: string) {
|
||||
const res = await fetch(`http://localhost:8000/scrape/${id}`)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch data')
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="my-5">
|
||||
<h2>Terms (Take up to 1 minute)</h2>
|
||||
Loading...
|
||||
</section>
|
||||
)
|
||||
}
|
||||
export default function Terms({result} : {result: any}) {
|
||||
|
||||
return (
|
||||
<section className="my-5">
|
||||
|
||||
{
|
||||
result.topKeywords && (
|
||||
<div
|
||||
@@ -57,77 +23,6 @@ export default function Terms({id} : {id: string}) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
result.contentInfo && (
|
||||
<div
|
||||
id="outline"
|
||||
className="my-5 border p-5 border-green-700 text-sm">
|
||||
<h3 className="mb-5 text-xl font-bold">Competitor reference</h3>
|
||||
<p> Average words length: {result.averageWords} </p>
|
||||
<div className="flex justify-between space-x-5 overflow-x-scroll">
|
||||
{
|
||||
result.contentInfo.map((site: string, index: number) => {
|
||||
return (
|
||||
<div key={index}
|
||||
className="border border-green-200 p-2">
|
||||
<p>
|
||||
Site:
|
||||
<a className="text-sky-700"
|
||||
href={site.link}>
|
||||
{site.link.substring(0, 30)}...
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
Words: {site.totalWords}
|
||||
</p>
|
||||
|
||||
{
|
||||
site.headings.length > 0 && (
|
||||
<div>
|
||||
<p>Headings</p>
|
||||
{
|
||||
site.headings.map((heading: string, index: number) => {
|
||||
return (
|
||||
<p key={index}>
|
||||
{heading.tag}: {heading.text}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
site.keywords.length > 0 && (
|
||||
<div>
|
||||
<p>keywords</p>
|
||||
{
|
||||
site.keywords.map((keyword: string, index: number) => {
|
||||
return (
|
||||
<li key={index}
|
||||
className="inline-block bg-gray-200 mb-1 rounded-full px-3 py-1 text-sm text-gray-700 mr-2">
|
||||
|
||||
{
|
||||
Array.isArray(keyword.word) ? keyword.word.join(' ') : keyword.word
|
||||
} :
|
||||
{keyword.frequency}
|
||||
</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
export default function TextEditor({id, prevContent}: {id: string, prevContent: string}) {
|
||||
export default function TextEditor({id, prevContent, title}: {id: string, prevContent: string, title: string}) {
|
||||
|
||||
const [content, setContent] = useState<string>(prevContent)
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function TextEditor({id, prevContent}: {id: string, prevContent:
|
||||
return (
|
||||
<section className="ml-5 w-8/12">
|
||||
<div className="flex justify-between items-center mb-5">
|
||||
<p className="font-bold text-xl">Text Editor</p>
|
||||
<p className="font-bold text-xl">Title: {title}</p>
|
||||
<button
|
||||
onClick={() => {saveContent()}}
|
||||
className="bg-sky-400 hover:bg-sky-600 p-3 rounded-lg text-white">
|
||||
|
||||
Reference in New Issue
Block a user