mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #3023 from omnivore-app/main
Web production deployment
This commit is contained in:
commit
1bede65407
145 changed files with 4810 additions and 3289 deletions
11
.github/workflows/run-tests.yaml
vendored
11
.github/workflows/run-tests.yaml
vendored
|
|
@ -11,10 +11,16 @@ on:
|
|||
paths-ignore:
|
||||
- 'apple/**'
|
||||
|
||||
env:
|
||||
NEXT_PUBLIC_APP_ENV: prod
|
||||
NEXT_PUBLIC_BASE_URL: http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_BASE_URL: http://localhost:4000
|
||||
NEXT_PUBLIC_HIGHLIGHTS_BASE_URL: http://localhost:3000
|
||||
|
||||
jobs:
|
||||
run-code-tests:
|
||||
name: Run Codebase tests
|
||||
runs-on: ubuntu-latest-m
|
||||
runs-on: ${{ github.repository_owner == 'omnivore-app' && 'ubuntu-latest-m' || 'ubuntu-latest' }}
|
||||
services:
|
||||
postgres:
|
||||
image: ankane/pgvector
|
||||
|
|
@ -59,8 +65,9 @@ jobs:
|
|||
yarn install --frozen-lockfile
|
||||
- name: Database Migration
|
||||
run: |
|
||||
psql -h localhost -p ${{ job.services.postgres.ports[5432] }} -U postgres -c "CREATE USER app_user WITH ENCRYPTED PASSWORD 'app_pass';"
|
||||
yarn workspace @omnivore/db migrate
|
||||
psql -h localhost -p ${{ job.services.postgres.ports[5432] }} -U postgres -c "CREATE USER app_user WITH ENCRYPTED PASSWORD 'app_pass';GRANT omnivore_user to app_user;"
|
||||
psql -h localhost -p ${{ job.services.postgres.ports[5432] }} -U postgres -c "GRANT omnivore_user to app_user;"
|
||||
env:
|
||||
PG_HOST: localhost
|
||||
PG_PORT: ${{ job.services.postgres.ports[5432] }}
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -69,3 +69,5 @@ data.json
|
|||
# android
|
||||
*.aab
|
||||
*.apk
|
||||
|
||||
tsconfig.tsbuildinfo
|
||||
|
|
|
|||
|
|
@ -237,13 +237,12 @@ import Utils
|
|||
let pageIndex = Int(event.pageIndex)
|
||||
if let totalPageCount = controller.document?.pageCount {
|
||||
let percent = min(100, max(0, ((Double(pageIndex) + 1.0) / Double(totalPageCount)) * 100.0))
|
||||
if percent > self.viewModel.pdfItem.readingProgress {
|
||||
self.viewModel.updateItemReadProgress(
|
||||
dataService: dataService,
|
||||
percent: percent,
|
||||
anchorIndex: pageIndex
|
||||
)
|
||||
}
|
||||
self.viewModel.updateItemReadProgress(
|
||||
dataService: dataService,
|
||||
percent: percent,
|
||||
anchorIndex: pageIndex,
|
||||
force: true
|
||||
)
|
||||
}
|
||||
}
|
||||
}.store(in: &subscriptions)
|
||||
|
|
|
|||
|
|
@ -76,11 +76,12 @@ final class PDFViewerViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func updateItemReadProgress(dataService: DataService, percent: Double, anchorIndex: Int) {
|
||||
func updateItemReadProgress(dataService: DataService, percent: Double, anchorIndex: Int, force: Bool = false) {
|
||||
dataService.updateLinkReadingProgress(
|
||||
itemID: pdfItem.itemID,
|
||||
readingProgress: percent,
|
||||
anchorIndex: anchorIndex
|
||||
anchorIndex: anchorIndex,
|
||||
force: force
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -356,34 +356,11 @@ import Views
|
|||
}
|
||||
|
||||
func markRead(dataService: DataService, item: LinkedItem) {
|
||||
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 100, anchorIndex: 0)
|
||||
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 100, anchorIndex: 0, force: true)
|
||||
}
|
||||
|
||||
func markUnread(dataService: DataService, item: LinkedItem) {
|
||||
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0, anchorIndex: 0)
|
||||
}
|
||||
|
||||
func snoozeUntil(dataService: DataService, linkId: String, until: Date, successMessage: String?) async {
|
||||
isLoading = true
|
||||
|
||||
if let itemIndex = items.firstIndex(where: { $0.id == linkId }) {
|
||||
items.remove(at: itemIndex)
|
||||
}
|
||||
|
||||
do {
|
||||
try await dataService.createReminder(
|
||||
reminderItemId: .link(id: linkId),
|
||||
remindAt: until
|
||||
)
|
||||
|
||||
if let message = successMessage {
|
||||
snackbar(message)
|
||||
}
|
||||
} catch {
|
||||
NSNotification.operationFailed(message: "Failed to snooze")
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0, anchorIndex: 0, force: true)
|
||||
}
|
||||
|
||||
private var queryContainsFilter: Bool {
|
||||
|
|
@ -408,12 +385,22 @@ import Views
|
|||
|
||||
if !selectedLabels.isEmpty {
|
||||
query.append(" label:")
|
||||
query.append(selectedLabels.map { $0.name != nil ? "\"\(String(describing: $0.name))\"" : "" }.joined(separator: ","))
|
||||
query.append(selectedLabels.compactMap { label in
|
||||
if let name = label.name {
|
||||
return "\"\(name)\""
|
||||
}
|
||||
return nil
|
||||
}.joined(separator: ","))
|
||||
}
|
||||
|
||||
if !negatedLabels.isEmpty {
|
||||
query.append(" !label:")
|
||||
query.append(negatedLabels.map { $0.name != nil ? "\"\(String(describing: $0.name))\"" : "" }.joined(separator: ","))
|
||||
query.append(negatedLabels.compactMap { label in
|
||||
if let name = label.name {
|
||||
return "\"\(name)\""
|
||||
}
|
||||
return nil
|
||||
}.joined(separator: ","))
|
||||
}
|
||||
|
||||
print("QUERY: `\(query)`")
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ import Views
|
|||
dataService.updateLinkReadingProgress(
|
||||
itemID: itemID,
|
||||
readingProgress: isItemRead ? 0 : 100,
|
||||
anchorIndex: 0
|
||||
anchorIndex: 0,
|
||||
force: false
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ struct WebReaderContainerView: View {
|
|||
)
|
||||
Button(
|
||||
action: {
|
||||
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0, anchorIndex: 0)
|
||||
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0, anchorIndex: 0, force: true)
|
||||
},
|
||||
label: { Label("Reset Read Location", systemImage: "arrow.counterclockwise.circle") }
|
||||
)
|
||||
|
|
@ -432,7 +432,7 @@ struct WebReaderContainerView: View {
|
|||
#endif
|
||||
.onAppear {
|
||||
if item.isUnread {
|
||||
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0.1, anchorIndex: 0)
|
||||
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0.1, anchorIndex: 0, force: false)
|
||||
}
|
||||
Task {
|
||||
await audioController.preload(itemIDs: [item.unwrappedID])
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ struct SafariWebLink: Identifiable {
|
|||
return
|
||||
}
|
||||
|
||||
dataService.updateLinkReadingProgress(itemID: itemID, readingProgress: readingProgress, anchorIndex: anchorIndex)
|
||||
dataService.updateLinkReadingProgress(itemID: itemID, readingProgress: readingProgress, anchorIndex: anchorIndex, force: false)
|
||||
replyHandler(["result": true], nil)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -740,7 +740,7 @@
|
|||
let anchorIndex = Int((player?.currentItem as? SpeechPlayerItem)?.speechItem.htmlIdx ?? "") ?? 0
|
||||
|
||||
if let itemID = itemAudioProperties?.itemID {
|
||||
dataService.updateLinkReadingProgress(itemID: itemID, readingProgress: percentProgress, anchorIndex: anchorIndex)
|
||||
dataService.updateLinkReadingProgress(itemID: itemID, readingProgress: percentProgress, anchorIndex: anchorIndex, force: true)
|
||||
}
|
||||
|
||||
if let itemID = itemAudioProperties?.itemID, let player = player, let currentItem = player.currentItem {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ public enum VoiceCategory: String, CaseIterable {
|
|||
case enIN = "English (India)"
|
||||
case enSG = "English (Singapore)"
|
||||
case enUK = "English (UK)"
|
||||
case frFR = "French (France)"
|
||||
case deDE = "German (Germany)"
|
||||
case hiIN = "Hindi (India)"
|
||||
case itIT = "Italian (Italy)"
|
||||
|
|
@ -70,6 +71,7 @@ public enum Voices {
|
|||
English,
|
||||
VoiceLanguage(key: "zh", name: "Chinese", defaultVoice: "zh-CN-XiaochenNeural", categories: [.zhCN]),
|
||||
VoiceLanguage(key: "de", name: "German", defaultVoice: "de-CH-JanNeural", categories: [.deDE]),
|
||||
VoiceLanguage(key: "fr", name: "French", defaultVoice: "fr-FR-HenriNeural", categories: [.frFR]),
|
||||
VoiceLanguage(key: "hi", name: "Hindi", defaultVoice: "hi-IN-MadhurNeural", categories: [.hiIN]),
|
||||
VoiceLanguage(key: "it", name: "Italian", defaultVoice: "it-IT-BenignoNeural", categories: [.itIT]),
|
||||
VoiceLanguage(key: "ja", name: "Japanese", defaultVoice: "ja-JP-NanamiNeural", categories: [.jaJP]),
|
||||
|
|
@ -88,6 +90,7 @@ public enum Voices {
|
|||
VoicePair(firstKey: "en-IE-ConnorNeural", secondKey: "en-IE-EmilyNeural", firstName: "Connor", secondName: "Emily", language: "en-IE", category: .enIE),
|
||||
VoicePair(firstKey: "en-IN-NeerjaNeural", secondKey: "en-IN-PrabhatNeural", firstName: "Neerja", secondName: "Prabhat", language: "en-IN", category: .enIN),
|
||||
VoicePair(firstKey: "en-SG-LunaNeural", secondKey: "en-SG-WayneNeural", firstName: "Luna", secondName: "Wayne", language: "en-SG", category: .enSG),
|
||||
VoicePair(firstKey: "fr-FR-HenriNeural", secondKey: "fr-FR-DeniseNeural", firstName: "Henri", secondName: "Denise", language: "en-FR", category: .frFR),
|
||||
VoicePair(firstKey: "zh-CN-XiaochenNeural", secondKey: "zh-CN-XiaohanNeural", firstName: "Xiaochen", secondName: "Xiaohan", language: "zh-CN", category: .zhCN),
|
||||
VoicePair(firstKey: "zh-CN-XiaoxiaoNeural", secondKey: "zh-CN-YunyangNeural", firstName: "Xiaoxiao", secondName: "Yunyang", language: "zh-CN", category: .zhCN),
|
||||
VoicePair(firstKey: "es-ES-AlvaroNeural", secondKey: "es-ES-ElviraNeural", firstName: "Alvaro", secondName: "Elvira", language: "es-ES", category: .esES),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,79 +1 @@
|
|||
import Foundation
|
||||
import Models
|
||||
import SwiftGraphQL
|
||||
|
||||
public enum ReminderItemId {
|
||||
case clientRequest(id: String)
|
||||
case link(id: String)
|
||||
|
||||
var linkId: String? {
|
||||
switch self {
|
||||
case .clientRequest:
|
||||
return nil
|
||||
case let .link(id):
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
var clientRequestId: String? {
|
||||
switch self {
|
||||
case .link:
|
||||
return nil
|
||||
case let .clientRequest(id):
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension DataService {
|
||||
func createReminder(
|
||||
reminderItemId: ReminderItemId,
|
||||
remindAt: Date
|
||||
) async throws {
|
||||
enum MutationResult {
|
||||
case complete(id: String)
|
||||
case error(errorCode: Enums.CreateReminderErrorCode)
|
||||
}
|
||||
|
||||
let selection = Selection<MutationResult, Unions.CreateReminderResult> {
|
||||
try $0.on(
|
||||
createReminderError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) },
|
||||
createReminderSuccess: .init {
|
||||
.complete(id: try $0.reminder(selection: Selection.Reminder { try $0.id() }))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let mutation = Selection.Mutation {
|
||||
try $0.createReminder(
|
||||
input: InputObjects.CreateReminderInput(
|
||||
archiveUntil: true,
|
||||
clientRequestId: OptionalArgument(reminderItemId.clientRequestId),
|
||||
linkId: OptionalArgument(reminderItemId.linkId),
|
||||
remindAt: DateTime(from: remindAt),
|
||||
sendNotification: true
|
||||
),
|
||||
selection: selection
|
||||
)
|
||||
}
|
||||
|
||||
let path = appEnvironment.graphqlPath
|
||||
let headers = networker.defaultHeaders
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
send(mutation, to: path, headers: headers) { queryResult in
|
||||
guard let payload = try? queryResult.get() else {
|
||||
continuation.resume(throwing: BasicError.message(messageText: "network error"))
|
||||
return
|
||||
}
|
||||
|
||||
switch payload.data {
|
||||
case .complete:
|
||||
continuation.resume()
|
||||
case let .error(errorCode: errorCode):
|
||||
continuation.resume(throwing: BasicError.message(messageText: errorCode.rawValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,15 @@ import Models
|
|||
import SwiftGraphQL
|
||||
|
||||
extension DataService {
|
||||
public func updateLinkReadingProgress(itemID: String, readingProgress: Double, anchorIndex: Int) {
|
||||
public func updateLinkReadingProgress(itemID: String, readingProgress: Double, anchorIndex: Int, force: Bool?) {
|
||||
backgroundContext.perform { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: self.backgroundContext) else { return }
|
||||
|
||||
if readingProgress != 0, readingProgress < linkedItem.readingProgress {
|
||||
return
|
||||
if let force = force, !force {
|
||||
if readingProgress != 0, readingProgress < linkedItem.readingProgress {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
print("updating reading progress: ", readingProgress, anchorIndex)
|
||||
|
|
@ -24,12 +26,13 @@ extension DataService {
|
|||
self.syncLinkReadingProgress(
|
||||
itemID: linkedItem.unwrappedID,
|
||||
readingProgress: readingProgress,
|
||||
anchorIndex: anchorIndex
|
||||
anchorIndex: anchorIndex,
|
||||
force: force
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func syncLinkReadingProgress(itemID: String, readingProgress: Double, anchorIndex: Int) {
|
||||
func syncLinkReadingProgress(itemID: String, readingProgress: Double, anchorIndex: Int, force: Bool?) {
|
||||
enum MutationResult {
|
||||
case saved(readAt: Date?)
|
||||
case error(errorCode: Enums.SaveArticleReadingProgressErrorCode)
|
||||
|
|
@ -49,8 +52,9 @@ extension DataService {
|
|||
let mutation = Selection.Mutation {
|
||||
try $0.saveArticleReadingProgress(
|
||||
input: InputObjects.SaveArticleReadingProgressInput(
|
||||
force: OptionalArgument(force),
|
||||
id: itemID,
|
||||
readingProgressAnchorIndex: anchorIndex,
|
||||
readingProgressAnchorIndex: OptionalArgument(anchorIndex),
|
||||
readingProgressPercent: readingProgress
|
||||
),
|
||||
selection: selection
|
||||
|
|
|
|||
|
|
@ -154,7 +154,8 @@ public extension DataService {
|
|||
syncLinkReadingProgress(
|
||||
itemID: item.unwrappedID,
|
||||
readingProgress: item.readingProgress,
|
||||
anchorIndex: Int(item.readingProgressAnchor)
|
||||
anchorIndex: Int(item.readingProgressAnchor),
|
||||
force: item.isPDF
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ extension DataService {
|
|||
createdAt: try $0.createdAt().value ?? Date(),
|
||||
savedAt: try $0.savedAt().value ?? Date(),
|
||||
readAt: try $0.readAt()?.value,
|
||||
updatedAt: try $0.updatedAt().value ?? Date(),
|
||||
updatedAt: try $0.updatedAt()?.value ?? Date(),
|
||||
state: try $0.state()?.rawValue.asArticleContentStatus ?? .succeeded,
|
||||
readingProgress: try $0.readingProgressPercent(),
|
||||
readingProgressAnchor: try $0.readingProgressAnchorIndex(),
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ private let libraryArticleSelection = Selection.Article {
|
|||
createdAt: try $0.createdAt().value ?? Date(),
|
||||
savedAt: try $0.savedAt().value ?? Date(),
|
||||
readAt: try $0.readAt()?.value,
|
||||
updatedAt: try $0.updatedAt().value ?? Date(),
|
||||
updatedAt: try $0.updatedAt()?.value ?? Date(),
|
||||
state: try $0.state()?.rawValue.asArticleContentStatus ?? .succeeded,
|
||||
readingProgress: try $0.readingProgressPercent(),
|
||||
readingProgressAnchor: try $0.readingProgressAnchorIndex(),
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ public extension DataService {
|
|||
status: try SubscriptionStatus.make(from: $0.status()),
|
||||
unsubscribeHttpUrl: try $0.unsubscribeHttpUrl(),
|
||||
unsubscribeMailTo: try $0.unsubscribeMailTo(),
|
||||
updatedAt: try $0.updatedAt().value,
|
||||
updatedAt: try $0.updatedAt()?.value ?? Date(),
|
||||
url: try $0.url(),
|
||||
icon: try $0.icon()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import Foundation
|
||||
import Models
|
||||
import SwiftGraphQL
|
||||
|
||||
|
|
@ -22,7 +23,7 @@ let highlightSelection = Selection.Highlight {
|
|||
patch: try $0.patch() ?? "",
|
||||
annotation: try $0.annotation(),
|
||||
createdAt: try $0.createdAt().value,
|
||||
updatedAt: try $0.updatedAt().value,
|
||||
updatedAt: try $0.updatedAt()?.value ?? Date(),
|
||||
createdByMe: try $0.createdByMe(),
|
||||
createdBy: try $0.user(selection: userProfileSelection),
|
||||
positionPercent: try $0.highlightPositionPercent(),
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -10,5 +10,8 @@
|
|||
"express": "^4.18.1",
|
||||
"express-graphql": "^0.12.0",
|
||||
"graphql": "^16.8.1"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
158
docs/guides/getting-started/hi/getting-started-guide.md
Normal file
158
docs/guides/getting-started/hi/getting-started-guide.md
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
# ओम्निवोर के साथ शुरुआत करना
|
||||
|
||||
ओम्निवोर एक ***रीड-इट-लेटर*** ऐप है जो आपको ऑनलाइन पढ़ी गई हर चीज को सहेजने और व्यवस्थित करने की सुविधा देता है।
|
||||
|
||||
यह मार्गदर्शिका आपको दिखाएगी कि ओम्निवोर के बुनियादी कार्यों और उन्नत सुविधाओं का उपयोग कैसे करें, जो चार मुख्य गतिविधियों में विभाजित हैं:
|
||||
|
||||
- सहेजा जा रहा है
|
||||
- पढ़ना
|
||||
- आयोजन
|
||||
- एकीकरण
|
||||
|
||||
लाइब्रेरी आपके सर्वभक्षी अनुभव का केंद्र है, जहां आप अपने द्वारा सहेजे गए किसी भी लिंक तक तुरंत पहुंच सकते हैं। सहेजे गए लिंक आपकी लाइब्रेरी में हमेशा के लिए रहते हैं जब तक कि आप उन्हें हटा नहीं देते।
|
||||
|
||||
## सहेजें
|
||||
|
||||
उन पृष्ठों या लेखों के लिंक सहेजने के पाँच मुख्य तरीके हैं जिन्हें आप बाद में पढ़ना चाहते हैं:
|
||||
|
||||
- आपकी सर्वभक्षी लाइब्रेरी से बचत
|
||||
- ब्राउज़र से सहेजा जा रहा है
|
||||
- फ़ोन या टैबलेट से बचत (iOS या Android)
|
||||
- ईमेल के माध्यम से न्यूज़लेटर सदस्यताएँ
|
||||
- मैक से पीडीएफ़ सहेजना
|
||||
|
||||
### आपकी सर्वभक्षी लाइब्रेरी से बचत
|
||||
1. अपनी लाइब्रेरी के ऊपरी दाएं कोने में, लिंक जोड़ें बटन पर टैप करें।
|
||||
2. वह यूआरएल दर्ज करें जिसे आप सहेजना चाहते हैं और लिंक जोड़ें पर टैप करें।
|
||||
3. अगली बार जब आप इसे रीफ्रेश करेंगे तो लिंक आपकी लाइब्रेरी में दिखाई देगा।
|
||||
|
||||
### ब्राउज़र से सहेजा जा रहा है
|
||||
|
||||
1. अपने ब्राउज़र के लिए ओम्निवोर एक्सटेंशन डाउनलोड और इंस्टॉल करें:
|
||||
|
||||
- [ क्रोम ](https://omnivore.app/install/chrome)
|
||||
- [ एज ](https://omnivore.app/install/edge)
|
||||
- [ फ़ायरफ़ॉक्स ](https://omnivore.app/install/firefox)
|
||||
- [ सफारी ](https://omnivore.app/install/safari)
|
||||
|
||||
2. उस पृष्ठ पर जाएँ जिसे आप सहेजना चाहते हैं और अपने ब्राउज़र के टूलबार या एक्सटेंशन मेनू में ओम्निवोर बटन पर टैप करें।
|
||||
3. वैकल्पिक रूप से, आप किसी भी हाइपरलिंक पर राइट-क्लिक (मैक पर कमांड+क्लिक) कर सकते हैं और मेनू से सेव टू ओम्निवोर का चयन कर सकते हैं।
|
||||
4. अगली बार जब आप इसे रीफ्रेश करेंगे तो लिंक आपकी लाइब्रेरी में दिखाई देगा।
|
||||
|
||||
### फ़ोन या टेबलेट से सहेजा जा रहा है
|
||||
|
||||
अपने मोबाइल डिवाइस से लिंक सहेजने का सबसे अच्छा तरीका ओम्निवोर ऐप है। आप यहां ऐप डाउनलोड कर सकते हैं:
|
||||
|
||||
- [ आईओएस (आईफोन या आईपैड) ](https://omnivore.app/install/ios)
|
||||
- एंड्रॉयड
|
||||
|
||||
एक बार मोबाइल ऐप इंस्टॉल हो जाए:
|
||||
|
||||
1. अपने ब्राउज़र में, उस पृष्ठ पर जाएँ जिसे आप सहेजना चाहते हैं और शेयर बटन पर टैप करें।
|
||||
2. शेयर मेनू में सर्वाहारी आइकन पर टैप करें।
|
||||
3. अगली बार जब आप इसे रीफ्रेश करेंगे तो लिंक आपकी लाइब्रेरी में दिखाई देगा।
|
||||
|
||||
### ईमेल के माध्यम से न्यूज़लेटर सदस्यताएँ
|
||||
|
||||
1. ओम्निवोर वेबसाइट या ऐप पर, प्रोफ़ाइल मेनू तक पहुंचने के लिए ऊपरी दाएं कोने में अपनी फोटो, नाम के पहले अक्षर या अवतार पर टैप करें। मेनू से ईमेल चुनें.
|
||||
2. सूची में नया ईमेल पता (उदा: username-123_abc@inbox.omnivore.app) जोड़ने के लिए नया ईमेल पता बनाएं पर टैप करें।
|
||||
3. ईमेल पते के आगे कॉपी आइकन पर क्लिक करें।
|
||||
4. जिस न्यूज़लेटर की आप सदस्यता लेना चाहते हैं उसके लिए साइनअप पृष्ठ पर जाएँ।
|
||||
5. ओम्निवोर ईमेल पते को साइनअप फॉर्म में चिपकाएँ।
|
||||
6. नए न्यूज़लेटर स्वचालित रूप से आपके ओमनिवोर इनबॉक्स में वितरित किए जाएंगे।
|
||||
|
||||
### मैक से पीडीएफ़ सहेजना
|
||||
|
||||
1. मैक ऐप इंस्टॉल करें। [मैक ऐप](https://omnivore.app/install/mac)
|
||||
2. अपने मैक पर, वह पीडीएफ ढूंढें जिसे आप सहेजना चाहते हैं और फ़ाइल नाम पर राइट-क्लिक करें या Ctrl+click करें।
|
||||
3. मेनू से शेयर चुनें और ओम्निवोर चुनें।
|
||||
4. गली बार जब आप इसे रीफ्रेश करेंगे तो लिंक आपकी लाइब्रेरी में दिखाई देगा।
|
||||
|
||||
## पढ़ना
|
||||
|
||||
रीडर दृश्य में प्रवेश करने के लिए अपनी लाइब्रेरी में सहेजे गए किसी भी लिंक पर क्लिक करें।
|
||||
|
||||
आसानी से पढ़ने और हाइलाइट करने के लिए, विकर्षण-मुक्त पढ़ने के लिए विज्ञापनों और अव्यवस्थाओं को हटाने के लिए ओमनिवोर पृष्ठों को प्रारूपित करता है। टेक्स्ट-केंद्रित दृश्य लेखों को छोटा और लोड करने में तेज़ बनाता है।
|
||||
|
||||
पढ़ते समय, आप यह कर सकते हैं:
|
||||
|
||||
- <span style="text-decoration:underline;">फ़ॉर्मेटिंग बदलें</span>
|
||||
- <span style="text-decoration:underline;">टेक्स्ट हाइलाइट करें</span>
|
||||
- <span style="text-decoration:underline;">नोट्स जोड़ें</span>
|
||||
- <span style="text-decoration:underline;">सभी सहेजे गए हाइलाइट्स और नोट्स देखें</span>
|
||||
- <span style="text-decoration:underline;">ट्रैक रीडिंग प्रोग्रेस</span>
|
||||
|
||||
### फ़ॉर्मेटिंग बदलें
|
||||
|
||||
1. **_थीम:_** प्रोफ़ाइल मेनू तक पहुंचने के लिए ऊपरी दाएं कोने में अपनी फ़ोटो, नाम के पहले अक्षर या अवतार पर टैप करें। लाइट या डार्क थीम चुनने के लिए सफेद या काले थंबनेल का चयन करें।
|
||||
2. **_टेक्स्ट फ़ॉर्मेटिंग:_** टेक्स्ट आकार, फ़ॉन्ट, मार्जिन और लाइन स्पेसिंग को समायोजित करने के लिए एए आइकन पर टैप करें।
|
||||
|
||||
### टेक्स्ट हाइलाइट करें
|
||||
|
||||
1. वह टेक्स्ट चुनें जिसे आप हाइलाइट करना चाहते हैं.
|
||||
2. **हाइलाइट** बटन पर टैप करें।
|
||||
3. अगली बार जब आप लेख देखेंगे तो पाठ हाइलाइट किया हुआ दिखाई देगा।
|
||||
|
||||
### नोट्स जोड़ें
|
||||
1. टेक्स्ट के उस भाग को हाइलाइट करें जहाँ आप नोट जोड़ना चाहते हैं।
|
||||
2. **नोट** बटन पर टैप करें, अपना नोट टाइप करें और सेव पर टैप करें।
|
||||
3. अगली बार जब आप यह लेख देखेंगे तो नोट आइकन दिखाई देगा।
|
||||
|
||||
### सभी सहेजे गए हाइलाइट्स और नोट्स देखें
|
||||
1. इस पृष्ठ पर आपके द्वारा जोड़े गए सभी हाइलाइट किए गए टेक्स्ट और नोट्स की सूची देखने के लिए हाइलाइट/नोट आइकन पर टैप करें।
|
||||
2. किसी नोट या हाइलाइट को हटाने के लिए, उसे सूची से चुनें और ट्रैश आइकन पर टैप करें।
|
||||
|
||||
### ट्रैक रीडिंग प्रोग्रेस
|
||||
|
||||
ओम्निवोर स्वचालित रूप से आपके विभिन्न उपकरणों पर आपकी पढ़ने की प्रगति पर नज़र रखता है ताकि आप आसानी से वहीं से शुरू कर सकें जहाँ आपने छोड़ा था। आपके पढ़ने शुरू करने के बाद आपकी लाइब्रेरी में प्रत्येक लिंक के शीर्ष पर एक प्रगति बार दिखाई देगा।
|
||||
|
||||
## आयोजन
|
||||
|
||||
डिफ़ॉल्ट रूप से, लाइब्रेरी इनबॉक्स आपके द्वारा सहेजे गए सभी लिंक प्रदर्शित करता है। आपकी सूची प्रबंधित करने और आपके पढ़ने को व्यवस्थित रखने के लिए, ओम्निवोर निम्नलिखित क्रियाएं प्रदान करता है:
|
||||
|
||||
- <span style="text-decoration:underline;">संग्रह</span>
|
||||
- <span style="text-decoration:underline;">लेबल</span>
|
||||
- <span style="text-decoration:underline;">खोज</span>
|
||||
- <span style="text-decoration:underline;">फिल्टर</span>
|
||||
|
||||
### संग्रह
|
||||
|
||||
1. जिस लिंक को आप संग्रहित करना चाहते हैं उसके बगल में स्थित मेनू आइकन पर टैप करें (मोबाइल ऐप पर, मेनू खोलने के लिए लिंक को देर तक दबाएं)।
|
||||
2. **आर्किव** चुनें.
|
||||
3. लिंक डिफ़ॉल्ट लाइब्रेरी दृश्य से गायब हो जाएगा, लेकिन यदि आप संग्रहीत फ़िल्टर का चयन करते हैं तो दिखाई देगा (नीचे फ़िल्टर देखें)।
|
||||
|
||||
### लेबल
|
||||
|
||||
1. किसी भी लिंक के आगे मेनू आइकन टैप करें और लेबल सेट करें चुनें।
|
||||
2. सूची से किसी मौजूदा लेबल का चयन करें या नया लेबल बनाने के लिए लेबल संपादित करें पर टैप करें।
|
||||
3. लेबल आपकी लाइब्रेरी में लिंक के बगल में दिखाई देगा। समान लेबल वाले सभी लिंक देखने के लिए इसे टैप करें।
|
||||
4. केवल ओमनिवोर मोबाइल ऐप: आपके द्वारा उपयोग किए गए सभी लेबलों की पूरी सूची देखने के लिए **लेबल** पर टैप करें; समान लेबल वाले सभी लिंक देखने के लिए एक टैप करें
|
||||
5. ध्यान दें: ओमनिवोर स्वचालित रूप से कुछ लेबल निर्दिष्ट करेगा, जैसे "न्यूज़लेटर्स।"
|
||||
|
||||
### खोज
|
||||
|
||||
1. अपने सभी सहेजे गए लिंक को खोजने के लिए, खोज बार में एक कीवर्ड या वाक्यांश दर्ज करें।
|
||||
2. आप अपनी खोज को और भी अधिक केंद्रित करने के लिए कीवर्ड को लेबल और फ़िल्टर के साथ जोड़ सकते हैं। उन्नत खोज के बारे में और जानें.
|
||||
|
||||
### फिल्टर
|
||||
|
||||
1. अपने लाइब्रेरी दृश्य को परिष्कृत करने के लिए **फ़िल्टर** मेनू का उपयोग करें (कुछ फ़िल्टर डिफ़ॉल्ट रूप से दिखाई दे सकते हैं)।
|
||||
2. न्यूज़लेटर्स को छोड़कर अपने सभी गैर-संग्रहीत लिंक की सूची देखने के लिए बाद में पढ़ें का चयन करें।
|
||||
3. आपके द्वारा अपने सभी सहेजे गए पृष्ठों में हाइलाइट किए गए टेक्स्ट चयन को देखने के लिए हाइलाइट्स का चयन करें।
|
||||
4. आज आपके द्वारा सहेजे गए लिंक की सूची देखने के लिए आज का चयन करें।
|
||||
5. अपने न्यूज़लेटर सदस्यता के माध्यम से सहेजे गए लिंक देखने के लिए न्यूज़लेटर्स का चयन करें।
|
||||
|
||||
## एकीकरण
|
||||
|
||||
ओमनिवोर ज्ञान आधारों और नोट लेने वाले ऐप्स के साथ एकीकरण की अनुमति देता है जिनमें शामिल हैं:
|
||||
|
||||
- लोगसेक
|
||||
- वेबहुक
|
||||
|
||||
### लॉगसेक
|
||||
|
||||
ओमनिवोर के लॉगसेक प्लगइन के साथ आप अपने सभी सहेजे गए लेख, हाइलाइट्स और नोट्स को एक लोकप्रिय ज्ञान आधार लॉगसेक में सिंक कर सकते हैं। लॉगसेक प्लगइन की स्थापना और उपयोग के बारे में जानकारी के लिए, कृपया इस सहायक को देखें [Omnivore for Logseq Plugin Guide](https://briansunter.com/graph/#/page/omnivore-logseq-guide).
|
||||
|
||||
### वेबहुक
|
||||
|
||||
जब आप कोई लिंक सहेजते हैं या जिस पृष्ठ को आप पढ़ रहे हैं उसमें हाइलाइट्स जोड़ते हैं तो ओम्निवोर वेबहुक को ट्रिगर कर सकता है। <span style="text-decoration:underline;">यह उदाहरण </span> Google ड्राइव पर संग्रहीत Google शीट स्प्रेडशीट में सभी सहेजे गए लिंक लिखने के लिए वेबहुक का उपयोग किया जा रहा है।
|
||||
|
|
@ -3,6 +3,5 @@
|
|||
"packages/*"
|
||||
],
|
||||
"version": "1.0.0",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true
|
||||
}
|
||||
"npmClient": "yarn"
|
||||
}
|
||||
19
package.json
19
package.json
|
|
@ -6,16 +6,16 @@
|
|||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"license": "UNLICENSED",
|
||||
"license": "AGPL-3.0-only",
|
||||
"scripts": {
|
||||
"test": "lerna run --no-bail test --ignore @omnivore/web",
|
||||
"lint": "lerna run lint --ignore @omnivore/web",
|
||||
"build": "lerna run build --ignore @omnivore/web",
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"test": "lerna run --no-bail test",
|
||||
"lint": "lerna run lint",
|
||||
"build": "lerna run build",
|
||||
"test:scoped:example": "lerna run test --scope={@omnivore/pdf-handler,@omnivore/web}",
|
||||
"gql-typegen": "graphql-codegen",
|
||||
"deploy:web": "vercel --prod"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@ardatan/aggregate-error": "^0.0.6",
|
||||
"@graphql-codegen/cli": "^2.6.2",
|
||||
|
|
@ -31,13 +31,12 @@
|
|||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"graphql": "^15.3.0",
|
||||
"graphql-tag": "^2.11.0",
|
||||
"lerna": "^4.0.0",
|
||||
"lerna": "^7.4.1",
|
||||
"prettier": "^2.5.1",
|
||||
"typescript": "^4.4.3"
|
||||
"typescript": "4.5.2"
|
||||
},
|
||||
"volta": {
|
||||
"node": "18.16.1",
|
||||
"yarn": "1.22.19"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
FROM node:18.16-alpine as builder
|
||||
FROM node:18.16 as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
|
||||
RUN apk add g++ make python3
|
||||
RUN apt-get update && apt-get install -y g++ make python3
|
||||
|
||||
COPY package.json .
|
||||
COPY yarn.lock .
|
||||
|
|
@ -32,7 +32,9 @@ RUN rm -rf /app/packages/api/node_modules
|
|||
RUN rm -rf /app/node_modules
|
||||
RUN yarn install --pure-lockfile --production
|
||||
|
||||
FROM node:18.16-alpine as runner
|
||||
FROM node:18.16 as runner
|
||||
|
||||
RUN apt-get update && apt-get install -y netcat-openbsd
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"start": "node dist/server.js",
|
||||
"lint": "eslint src --ext ts,js,tsx,jsx",
|
||||
"lint:fix": "eslint src --fix --ext ts,js,tsx,jsx",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"test": "nyc mocha -r ts-node/register --config mocha-config.json --timeout 10000",
|
||||
"copy-files": "copyfiles -u 1 src/**/*.html dist/"
|
||||
},
|
||||
|
|
@ -145,7 +146,6 @@
|
|||
"node": "18.16.1"
|
||||
},
|
||||
"volta": {
|
||||
"node": "18.16.1",
|
||||
"yarn": "1.22.19"
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import * as httpContext from 'express-http-context2'
|
|||
import { EntityManager, EntityTarget, Repository } from 'typeorm'
|
||||
import { appDataSource } from '../data_source'
|
||||
import { Claims } from '../resolvers/types'
|
||||
import { SetClaimsRole } from '../utils/dictionary'
|
||||
|
||||
export const getColumns = <T>(repository: Repository<T>): (keyof T)[] => {
|
||||
return repository.metadata.columns.map(
|
||||
|
|
@ -12,8 +13,10 @@ export const getColumns = <T>(repository: Repository<T>): (keyof T)[] => {
|
|||
export const setClaims = async (
|
||||
manager: EntityManager,
|
||||
uid = '00000000-0000-0000-0000-000000000000',
|
||||
dbRole = 'omnivore_user'
|
||||
userRole = 'user'
|
||||
): Promise<unknown> => {
|
||||
const dbRole =
|
||||
userRole === SetClaimsRole.ADMIN ? 'omnivore_admin' : 'omnivore_user'
|
||||
return manager.query('SELECT * from omnivore.set_claims($1, $2)', [
|
||||
uid,
|
||||
dbRole,
|
||||
|
|
|
|||
70
packages/api/src/routers/svc/user.ts
Normal file
70
packages/api/src/routers/svc/user.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import cors from 'cors'
|
||||
import express from 'express'
|
||||
import { LessThan } from 'typeorm'
|
||||
import { StatusType } from '../../entity/user'
|
||||
import { readPushSubscription } from '../../pubsub'
|
||||
import { deleteUsers } from '../../services/user'
|
||||
import { corsConfig } from '../../utils/corsConfig'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
type CleanupMessage = {
|
||||
subDays: number
|
||||
}
|
||||
|
||||
const isCleanupMessage = (obj: any): obj is CleanupMessage =>
|
||||
'subDays' in obj && !isNaN(obj.subDays)
|
||||
|
||||
const getCleanupMessage = (msgStr: string): CleanupMessage => {
|
||||
try {
|
||||
const obj = JSON.parse(msgStr) as unknown
|
||||
if (isCleanupMessage(obj)) {
|
||||
return obj
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('error deserializing event: ', { msgStr, err })
|
||||
}
|
||||
|
||||
return {
|
||||
subDays: 0, // default to 0
|
||||
}
|
||||
}
|
||||
|
||||
export function userServiceRouter() {
|
||||
const router = express.Router()
|
||||
|
||||
router.post('/prune', cors<express.Request>(corsConfig), async (req, res) => {
|
||||
logger.info('prune soft deleted users')
|
||||
|
||||
const { message: msgStr, expired } = readPushSubscription(req)
|
||||
|
||||
if (!msgStr) {
|
||||
return res.status(200).send('Bad Request')
|
||||
}
|
||||
|
||||
if (expired) {
|
||||
logger.info('discarding expired message')
|
||||
return res.status(200).send('Expired')
|
||||
}
|
||||
|
||||
const cleanupMessage = getCleanupMessage(msgStr)
|
||||
const subTime = cleanupMessage.subDays * 1000 * 60 * 60 * 24 // convert days to milliseconds
|
||||
|
||||
try {
|
||||
const result = await deleteUsers({
|
||||
status: StatusType.Deleted,
|
||||
updatedAt: LessThan(new Date(Date.now() - subTime)), // subDays ago
|
||||
})
|
||||
logger.info('prune result', result)
|
||||
|
||||
return res.sendStatus(200)
|
||||
} catch (error) {
|
||||
logger.error('error prune users', error)
|
||||
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
})
|
||||
|
||||
return router
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ import { newsletterServiceRouter } from './routers/svc/newsletters'
|
|||
// import { remindersServiceRouter } from './routers/svc/reminders'
|
||||
import { rssFeedRouter } from './routers/svc/rss_feed'
|
||||
import { uploadServiceRouter } from './routers/svc/upload'
|
||||
import { userServiceRouter } from './routers/svc/user'
|
||||
import { webhooksServiceRouter } from './routers/svc/webhooks'
|
||||
import { textToSpeechRouter } from './routers/text_to_speech'
|
||||
import { userRouter } from './routers/user_router'
|
||||
|
|
@ -121,6 +122,7 @@ export const createApp = (): {
|
|||
app.use('/svc/pubsub/webhooks', webhooksServiceRouter())
|
||||
app.use('/svc/pubsub/integrations', integrationsServiceRouter())
|
||||
app.use('/svc/pubsub/rss-feed', rssFeedRouter())
|
||||
app.use('/svc/pubsub/user', userServiceRouter())
|
||||
// app.use('/svc/reminders', remindersServiceRouter())
|
||||
app.use('/svc/email-attachment', emailAttachmentRouter())
|
||||
|
||||
|
|
|
|||
|
|
@ -148,6 +148,8 @@ export class ReadwiseIntegration extends IntegrationService {
|
|||
)
|
||||
return response.status === 200
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.status === 429 && retryCount < 3) {
|
||||
logger.info('Readwise API rate limit exceeded, retrying...')
|
||||
|
|
@ -157,10 +159,6 @@ export class ReadwiseIntegration extends IntegrationService {
|
|||
await wait(parseInt(retryAfter, 10) * 1000)
|
||||
return this.syncWithReadwise(token, highlights, retryCount + 1)
|
||||
}
|
||||
|
||||
logger.error(error.message)
|
||||
} else {
|
||||
logger.error(error)
|
||||
}
|
||||
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { DeepPartial, FindOptionsWhere, In } from 'typeorm'
|
||||
import { StatusType, User } from '../entity/user'
|
||||
import { authTrx } from '../repository'
|
||||
import { userRepository } from '../repository/user'
|
||||
import { SetClaimsRole } from '../utils/dictionary'
|
||||
|
||||
export const deleteUser = async (userId: string) => {
|
||||
await authTrx(
|
||||
|
|
@ -20,6 +22,28 @@ export const updateUser = async (userId: string, update: Partial<User>) => {
|
|||
)
|
||||
}
|
||||
|
||||
export const findUser = async (id: string): Promise<User | null> => {
|
||||
export const findActiveUser = async (id: string): Promise<User | null> => {
|
||||
return userRepository.findOneBy({ id, status: StatusType.Active })
|
||||
}
|
||||
|
||||
export const findUsersById = async (ids: string[]): Promise<User[]> => {
|
||||
return userRepository.findBy({ id: In(ids) })
|
||||
}
|
||||
|
||||
export const deleteUsers = async (criteria: FindOptionsWhere<User>) => {
|
||||
return authTrx(
|
||||
async (t) => t.getRepository(User).delete(criteria),
|
||||
undefined,
|
||||
undefined,
|
||||
SetClaimsRole.ADMIN
|
||||
)
|
||||
}
|
||||
|
||||
export const createUsers = async (users: DeepPartial<User>[]) => {
|
||||
return authTrx(
|
||||
async (t) => t.getRepository(User).save(users),
|
||||
undefined,
|
||||
undefined,
|
||||
SetClaimsRole.ADMIN
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
UpdateUserProfileErrorCode,
|
||||
} from '../../src/generated/graphql'
|
||||
import { findProfile } from '../../src/services/profile'
|
||||
import { deleteUser, findUser } from '../../src/services/user'
|
||||
import { deleteUser, findActiveUser } from '../../src/services/user'
|
||||
import { hashPassword } from '../../src/utils/auth'
|
||||
import { createTestUser } from '../db'
|
||||
import { generateFakeUuid, graphqlRequest, request } from '../util'
|
||||
|
|
@ -98,7 +98,7 @@ describe('User API', () => {
|
|||
|
||||
it('updates user and responds with status code 200', async () => {
|
||||
const response = await graphqlRequest(query, authToken).expect(200)
|
||||
const user = await findUser(response.body.data.updateUser.user.id)
|
||||
const user = await findActiveUser(response.body.data.updateUser.user.id)
|
||||
expect(user?.name).to.eql(name)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
65
packages/api/test/routers/user.test.ts
Normal file
65
packages/api/test/routers/user.test.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { expect } from 'chai'
|
||||
import 'mocha'
|
||||
import { In } from 'typeorm'
|
||||
import { StatusType } from '../../src/entity/user'
|
||||
import {
|
||||
createUsers,
|
||||
deleteUsers,
|
||||
findUsersById,
|
||||
} from '../../src/services/user'
|
||||
import { request } from '../util'
|
||||
|
||||
describe('User Service Router', () => {
|
||||
const token = process.env.PUBSUB_VERIFICATION_TOKEN || ''
|
||||
|
||||
describe('prune', () => {
|
||||
let toDeleteUserIds: string[] = []
|
||||
|
||||
before(async () => {
|
||||
// create test users
|
||||
const users = await createUsers([
|
||||
{
|
||||
name: 'user_1',
|
||||
email: 'user_1@omnivore.app',
|
||||
status: StatusType.Deleted,
|
||||
updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2), // 2 days ago
|
||||
source: 'GOOGLE',
|
||||
sourceUserId: '123',
|
||||
},
|
||||
{
|
||||
name: 'user_2',
|
||||
email: 'user_2@omnivore.app',
|
||||
status: StatusType.Deleted,
|
||||
updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2), // 2 days ago
|
||||
source: 'GOOGLE',
|
||||
sourceUserId: '456',
|
||||
},
|
||||
])
|
||||
toDeleteUserIds = users.map((u) => u.id)
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// delete test users
|
||||
await deleteUsers({ id: In(toDeleteUserIds) })
|
||||
})
|
||||
|
||||
it('prunes soft deleted users a day ago', async () => {
|
||||
const data = {
|
||||
message: {
|
||||
data: Buffer.from(
|
||||
JSON.stringify({ subDays: 1 }) // 1 day ago
|
||||
).toString('base64'),
|
||||
publishTime: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
|
||||
await request
|
||||
.post('/svc/pubsub/user/prune?token=' + token)
|
||||
.send(data)
|
||||
.expect(200)
|
||||
|
||||
const deletedUsers = await findUsersById(toDeleteUserIds)
|
||||
expect(deletedUsers.length).to.equal(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -32,7 +32,6 @@
|
|||
"webpack-dev-server": "^4.7.4"
|
||||
},
|
||||
"volta": {
|
||||
"node": "18.16.0",
|
||||
"yarn": "1.22.10"
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,11 @@ const config: Configuration = {
|
|||
],
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
fallback: {
|
||||
stream: false,
|
||||
fs: false,
|
||||
zlib: false,
|
||||
}
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'build'),
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
FROM node:18.16-alpine
|
||||
FROM node:18.16
|
||||
|
||||
# Installs latest Chromium (92) package.
|
||||
RUN apk add --no-cache \
|
||||
RUN apt-get update && apt-get install -y \
|
||||
chromium \
|
||||
nss \
|
||||
freetype \
|
||||
harfbuzz \
|
||||
ca-certificates \
|
||||
ttf-freefont \
|
||||
nodejs \
|
||||
yarn \
|
||||
g++ \
|
||||
|
|
@ -16,7 +12,7 @@ RUN apk add --no-cache \
|
|||
|
||||
WORKDIR /app
|
||||
|
||||
ENV CHROMIUM_PATH /usr/bin/chromium-browser
|
||||
ENV CHROMIUM_PATH /usr/bin/chromium
|
||||
ENV LAUNCH_HEADLESS=true
|
||||
|
||||
COPY package.json .
|
||||
|
|
|
|||
|
|
@ -20,5 +20,8 @@
|
|||
"start_gcf": "npx functions-framework --port=9090 --target=puppeteer",
|
||||
"start_preview": "npx functions-framework --target=preview",
|
||||
"test": "mocha test/*.js"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src --ext ts,js,tsx,jsx",
|
||||
"compile": "tsc",
|
||||
"build": "tsc"
|
||||
|
|
@ -38,5 +39,8 @@
|
|||
"redis": "^4.3.1",
|
||||
"underscore": "^1.13.6",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ const getTweetIds = async (
|
|||
timeout: 60000, // 60 seconds
|
||||
})
|
||||
|
||||
return (await page.evaluate(async (author) => {
|
||||
return await page.evaluate(async (author) => {
|
||||
/**
|
||||
* Wait for `ms` amount of milliseconds
|
||||
* @param {number} ms
|
||||
|
|
@ -278,7 +278,7 @@ const getTweetIds = async (
|
|||
}
|
||||
|
||||
return ids
|
||||
}, author)) as string[]
|
||||
}, author)
|
||||
} catch (error) {
|
||||
console.error('Error getting tweets', error)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
},
|
||||
"devDependencies": {},
|
||||
"volta": {
|
||||
"node": "18.16.1",
|
||||
"yarn": "1.22.10"
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:18.16-alpine
|
||||
FROM node:18.16
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
|
@ -8,7 +8,9 @@ COPY tsconfig.json .
|
|||
|
||||
COPY /packages/db/package.json ./packages/db/package.json
|
||||
|
||||
RUN apk --no-cache --virtual build-dependencies add postgresql uuidgen
|
||||
RUN apt-get update && apt-get install -y \
|
||||
postgresql \
|
||||
uuid-runtime
|
||||
|
||||
RUN yarn install
|
||||
|
||||
|
|
|
|||
9
packages/db/migrations/0141.do.add_index_for_cleanup_to_user.sql
Executable file
9
packages/db/migrations/0141.do.add_index_for_cleanup_to_user.sql
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
-- Type: DO
|
||||
-- Name: add_index_for_cleanup_to_user
|
||||
-- Description: Add index of status and updated_at to omnivore.user table for cleanup of deleted users
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS user_status_updated_at_idx ON omnivore.user (status, updated_at);
|
||||
|
||||
COMMIT;
|
||||
9
packages/db/migrations/0141.undo.add_index_for_cleanup_to_user.sql
Executable file
9
packages/db/migrations/0141.undo.add_index_for_cleanup_to_user.sql
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
-- Type: UNDO
|
||||
-- Name: add_index_for_cleanup_to_user
|
||||
-- Description: Add index of status and updated_at to omnivore.user table for cleanup of deleted users
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS user_status_updated_at_idx;
|
||||
|
||||
COMMIT;
|
||||
19
packages/db/migrations/0142.do.create_omnivore_admin_role.sql
Executable file
19
packages/db/migrations/0142.do.create_omnivore_admin_role.sql
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
-- Type: DO
|
||||
-- Name: create_omnivore_admin_role
|
||||
-- Description: Create omnivore_admin role with admin permissions
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE ROLE omnivore_admin;
|
||||
|
||||
GRANT omnivore_admin TO app_user;
|
||||
|
||||
GRANT ALL PRIVILEGES ON SCHEMA omnivore TO omnivore_admin;
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA omnivore TO omnivore_admin;
|
||||
|
||||
CREATE POLICY user_admin_policy on omnivore.user
|
||||
FOR ALL
|
||||
TO omnivore_admin
|
||||
USING (true);
|
||||
|
||||
COMMIT;
|
||||
16
packages/db/migrations/0142.undo.create_omnivore_admin_role.sql
Executable file
16
packages/db/migrations/0142.undo.create_omnivore_admin_role.sql
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
-- Type: UNDO
|
||||
-- Name: create_omnivore_admin_role
|
||||
-- Description: Create omnivore_admin role with admin permissions
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP POLICY user_admin_policy ON omnivore.user;
|
||||
|
||||
REVOKE ALL PRIVILEGES on omnivore.user from omnivore_admin;
|
||||
REVOKE ALL PRIVILEGES on SCHEMA omnivore from omnivore_admin;
|
||||
|
||||
DROP OWNED BY omnivore_admin;
|
||||
|
||||
DROP ROLE IF EXISTS omnivore_admin;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
"description": "",
|
||||
"scripts": {
|
||||
"migrate": "ts-node ./migrate.ts",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"generate": "plop"
|
||||
},
|
||||
"author": "",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src --ext ts,js,tsx,jsx",
|
||||
"compile": "tsc",
|
||||
"build": "tsc && yarn copy-files",
|
||||
|
|
@ -52,5 +53,8 @@
|
|||
"unzip-stream": "^0.3.1",
|
||||
"urlsafe-base64": "^1.0.0",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"keywords": [],
|
||||
"scripts": {
|
||||
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src --ext ts,js,tsx,jsx",
|
||||
"compile": "tsc",
|
||||
"build": "tsc",
|
||||
|
|
@ -42,5 +43,8 @@
|
|||
"parse-multipart-data": "^1.2.1",
|
||||
"rfc2047": "^4.0.1",
|
||||
"showdown": "^2.1.0"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"keywords": [],
|
||||
"scripts": {
|
||||
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src --ext ts,js,tsx,jsx",
|
||||
"compile": "tsc",
|
||||
"build": "tsc",
|
||||
|
|
@ -33,5 +34,8 @@
|
|||
"axios": "^0.27.2",
|
||||
"concurrently": "^7.0.0",
|
||||
"pdfjs-dist": "^2.9.359"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,5 +25,8 @@
|
|||
},
|
||||
"scripts": {
|
||||
"test": "mocha test/*.js"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src --ext ts,js,tsx,jsx",
|
||||
"compile": "tsc",
|
||||
"build": "tsc",
|
||||
|
|
@ -28,5 +29,8 @@
|
|||
"axios": "^1.4.0",
|
||||
"dotenv": "^16.0.1",
|
||||
"jsonwebtoken": "^8.5.1"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,5 +41,8 @@
|
|||
"dependencies": {
|
||||
"html-entities": "^2.3.2",
|
||||
"parse-srcset": "^1.0.2"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src --ext ts,js,tsx,jsx",
|
||||
"compile": "tsc",
|
||||
"build": "tsc",
|
||||
|
|
@ -28,5 +29,8 @@
|
|||
"dotenv": "^16.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"rss-parser": "^3.13.0"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src --ext ts,js,tsx,jsx",
|
||||
"compile": "tsc",
|
||||
"build": "tsc",
|
||||
|
|
@ -26,5 +27,8 @@
|
|||
"dotenv": "^16.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"search-query-parser": "^1.6.0"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"keywords": [],
|
||||
"scripts": {
|
||||
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src --ext ts,js,tsx,jsx",
|
||||
"compile": "tsc",
|
||||
"build": "tsc",
|
||||
|
|
@ -45,5 +46,8 @@
|
|||
"natural": "^6.2.0",
|
||||
"redis": "^4.3.1",
|
||||
"underscore": "^1.13.4"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src --ext ts,js,tsx,jsx",
|
||||
"compile": "tsc",
|
||||
"build": "tsc",
|
||||
|
|
@ -30,5 +31,8 @@
|
|||
"jsonwebtoken": "^8.5.1",
|
||||
"linkedom": "^0.14.26",
|
||||
"urlsafe-base64": "^1.0.0"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ ENV NEXT_PUBLIC_BASE_URL=$BASE_URL
|
|||
ENV NEXT_PUBLIC_SERVER_BASE_URL=$SERVER_BASE_URL
|
||||
ENV NEXT_PUBLIC_HIGHLIGHTS_BASE_URL=$HIGHLIGHTS_BASE_URL
|
||||
|
||||
RUN apk add g++ make python3
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json .
|
||||
|
|
|
|||
|
|
@ -28,11 +28,9 @@ export const Button = styled('button', {
|
|||
color: '#3D3D3D',
|
||||
bg: '#FFEA9F',
|
||||
p: '10px 15px',
|
||||
'&:hover': {
|
||||
'&:hover, &:focus': {
|
||||
bg: '$omnivoreCtaYellow',
|
||||
},
|
||||
'&:focus': {
|
||||
border: '1px solid $omnivoreCtaYellow',
|
||||
outline: '1px solid $omnivoreCtaYellow',
|
||||
},
|
||||
},
|
||||
cancelGeneric: {
|
||||
|
|
@ -45,18 +43,13 @@ export const Button = styled('button', {
|
|||
border: '1px solid transparent',
|
||||
p: '10px 15px',
|
||||
bg: 'transparent',
|
||||
'&:hover': {
|
||||
'&:hover, &:focus': {
|
||||
bg: '#EBEBEB',
|
||||
},
|
||||
'&:focus': {
|
||||
outline: 'none !important',
|
||||
border: '1px solid $omnivoreCtaYellow',
|
||||
outline: '1px solid $omnivoreCtaYellow',
|
||||
},
|
||||
},
|
||||
ctaOutlineYellow: {
|
||||
boxSizing: 'border-box',
|
||||
'-moz-box-sizing': 'border-box',
|
||||
'-webkit-box-sizing': 'border-box',
|
||||
borderColor: 'unset',
|
||||
border: '1px solid $omnivoreCtaYellow',
|
||||
fontSize: '14px',
|
||||
|
|
@ -159,7 +152,6 @@ export const Button = styled('button', {
|
|||
outlineColor: 'rgba(0, 0, 0, 0)',
|
||||
border: '1px solid rgba(0, 0, 0, 0.06)',
|
||||
cursor: 'pointer',
|
||||
'&:focus': { outline: 'none' },
|
||||
},
|
||||
ctaModal: {
|
||||
height: '32px',
|
||||
|
|
@ -172,7 +164,6 @@ export const Button = styled('button', {
|
|||
border: '1px solid $grayBorder',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '8px',
|
||||
'&:focus': { outline: 'none' },
|
||||
},
|
||||
ctaSecondary: {
|
||||
color: '$grayText',
|
||||
|
|
@ -245,7 +236,6 @@ export const Button = styled('button', {
|
|||
'&:hover': {
|
||||
opacity: 0.7,
|
||||
},
|
||||
'&:focus': { outline: 'none' },
|
||||
},
|
||||
highlightBarIcon: {
|
||||
p: '0px',
|
||||
|
|
@ -257,7 +247,6 @@ export const Button = styled('button', {
|
|||
opacity: 0.5,
|
||||
},
|
||||
|
||||
'&:focus': { outline: 'none' },
|
||||
},
|
||||
articleActionIcon: {
|
||||
bg: 'transparent',
|
||||
|
|
|
|||
|
|
@ -11,37 +11,35 @@ type HeaderNavLinkProps = {
|
|||
|
||||
export function HeaderNavLink(props: HeaderNavLinkProps): JSX.Element {
|
||||
return (
|
||||
<Link passHref href={props.href}>
|
||||
<a style={{ textDecoration: 'none' }}>
|
||||
<VStack
|
||||
alignment="center"
|
||||
distribution="center"
|
||||
<Link passHref href={props.href} style={{ textDecoration: 'none' }}>
|
||||
<VStack
|
||||
alignment="center"
|
||||
distribution="center"
|
||||
css={{
|
||||
cursor: 'pointer',
|
||||
px: '$3',
|
||||
color: 'inherit',
|
||||
textDecoration: 'inherit',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: '100%',
|
||||
}}
|
||||
>
|
||||
<StyledText style="navLink">{props.text}</StyledText>
|
||||
<Box
|
||||
css={{
|
||||
cursor: 'pointer',
|
||||
px: '$3',
|
||||
color: 'inherit',
|
||||
textDecoration: 'inherit',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: '100%',
|
||||
width: '100%',
|
||||
bg: 'rgb(255, 210, 52)',
|
||||
height: '2px',
|
||||
opacity: props.isActive ? 1 : 0,
|
||||
display: props.isActive ? 'unset' : 'none',
|
||||
mt: '4px',
|
||||
animation: `${expandWidthAnim('0%', '100%')} 0.2s ease-out`,
|
||||
'&:hover': {
|
||||
opacity: props.isActive ? 0.7 : 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<StyledText style="navLink">{props.text}</StyledText>
|
||||
<Box
|
||||
css={{
|
||||
width: '100%',
|
||||
bg: 'rgb(255, 210, 52)',
|
||||
height: '2px',
|
||||
opacity: props.isActive ? 1 : 0,
|
||||
display: props.isActive ? 'unset' : 'none',
|
||||
mt: '4px',
|
||||
animation: `${expandWidthAnim('0%', '100%')} 0.2s ease-out`,
|
||||
'&:hover': {
|
||||
opacity: props.isActive ? 0.7 : 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</VStack>
|
||||
</a>
|
||||
/>
|
||||
</VStack>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useRef, useState } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { styled } from '../tokens/stitches.config'
|
||||
import { Box, HStack } from './LayoutPrimitives'
|
||||
import { StyledText } from './StyledText'
|
||||
|
|
@ -6,9 +6,14 @@ import {
|
|||
LabelColorDropdownProps,
|
||||
LabelOptionProps,
|
||||
} from '../../utils/settings-page/labels/types'
|
||||
import { TwitterPicker } from 'react-color'
|
||||
import { TwitterPicker as TwitterPicker_, TwitterPickerProps } from 'react-color'
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||
|
||||
// TwitterPicker is a Class component, but the types are broken in React 18.
|
||||
// TODO: Maybe move away from this component, since it hasn't been updated for 3 years.
|
||||
// https://github.com/casesandberg/react-color/issues/883
|
||||
const TwitterPicker = TwitterPicker_ as unknown as React.FunctionComponent<TwitterPickerProps>
|
||||
|
||||
const DropdownMenuContent = styled(DropdownMenuPrimitive.Content, {
|
||||
borderRadius: 6,
|
||||
backgroundColor: '$grayBg',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import AutosizeInput from 'react-input-autosize'
|
||||
import AutosizeInput_, { AutosizeInputProps } from 'react-input-autosize'
|
||||
import { Box, SpanBox } from './LayoutPrimitives'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Label } from '../../lib/networking/fragments/labelFragment'
|
||||
|
|
@ -8,6 +8,11 @@ import { EditLabelChip } from './EditLabelChip'
|
|||
import { LabelsDispatcher } from '../../lib/hooks/useSetPageLabels'
|
||||
import { EditLabelChipStack } from './EditLabelChipStack'
|
||||
|
||||
// AutosizeInput is a Class component, but the types are broken in React 18.
|
||||
// TODO: Maybe move away from this component, since it hasn't been updated for 3 years.
|
||||
// https://github.com/JedWatson/react-input-autosize/issues
|
||||
const AutosizeInput = AutosizeInput_ as unknown as React.FunctionComponent<AutosizeInputProps>
|
||||
|
||||
const MaxUnstackedLabels = 7
|
||||
|
||||
type LabelsPickerProps = {
|
||||
|
|
|
|||
|
|
@ -40,14 +40,14 @@ const InternalOrExternalLink = (props: InternalOrExternalLinkProps) => {
|
|||
}}
|
||||
>
|
||||
{!isExternal ? (
|
||||
<Link href={props.link}>{props.children}</Link>
|
||||
<Link href={props.link} legacyBehavior>{props.children}</Link>
|
||||
) : (
|
||||
<a href={props.link} target="_blank" rel="noreferrer">
|
||||
{props.children}
|
||||
</a>
|
||||
)}
|
||||
</SpanBox>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export const SuggestionBox = (props: SuggestionBoxProps) => {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export const TooltipContent = StyledContent
|
|||
export const TooltipArrow = StyledArrow
|
||||
|
||||
type TooltipWrappedProps = {
|
||||
children: React.ReactNode;
|
||||
tooltipContent: string;
|
||||
active?: boolean;
|
||||
tooltipSide?: TooltipPrimitive.TooltipContentProps['side']
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Image from 'next/image'
|
||||
|
||||
export function ChromeIcon(): JSX.Element {
|
||||
return <Image src="/static/icons/chrome@2x.png" width="24" height="24" />
|
||||
return <Image src="/static/icons/chrome@2x.png" width="24" height="24" alt="" />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Image from 'next/image'
|
||||
|
||||
export function EdgeIcon(): JSX.Element {
|
||||
return <Image src="/static/icons/edge@2x.png" width="24" height="24" />
|
||||
return <Image src="/static/icons/edge@2x.png" width="24" height="24" alt="" />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Image from 'next/image'
|
||||
|
||||
export function FirefoxIcon(): JSX.Element {
|
||||
return <Image src="/static/icons/firefox@2x.png" width="24" height="24" />
|
||||
return <Image src="/static/icons/firefox@2x.png" width="24" height="24" alt="" />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,20 +14,22 @@ export function OmnivoreFestiveLogo(
|
|||
const href = props.href || '/home'
|
||||
|
||||
return (
|
||||
<Link passHref href={href}>
|
||||
<a
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src="/static/images/omnivore-logo-santa.png"
|
||||
width="27"
|
||||
height="27"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
(<Link
|
||||
passHref
|
||||
href={href}
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
|
||||
<Image
|
||||
src="/static/images/omnivore-logo-santa.png"
|
||||
width="27"
|
||||
height="27"
|
||||
alt=""
|
||||
/>
|
||||
|
||||
</Link>)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ReactChildren } from 'react'
|
||||
import { config } from '../../tokens/stitches.config'
|
||||
|
||||
export type OmnivoreLogoBaseProps = {
|
||||
color?: string
|
||||
href?: string
|
||||
|
|
@ -15,23 +12,25 @@ export function OmnivoreLogoBase(props: OmnivoreLogoBaseProps): JSX.Element {
|
|||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<Link passHref href={href}>
|
||||
<a
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
onClick={(event) => {
|
||||
const query = window.sessionStorage.getItem('q')
|
||||
if (query) {
|
||||
router.push(`/home?${query}`)
|
||||
event.preventDefault()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</a>
|
||||
<Link
|
||||
passHref
|
||||
href={href}
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
onClick={(event) => {
|
||||
const query = window.sessionStorage.getItem('q')
|
||||
if (query) {
|
||||
router.push(`/home?${query}`)
|
||||
event.preventDefault()
|
||||
}
|
||||
}}
|
||||
tabIndex={-1}
|
||||
aria-label="Omnivore logo"
|
||||
>
|
||||
{props.children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Image from 'next/image'
|
||||
|
||||
export function SafariIcon(): JSX.Element {
|
||||
return <Image src="/static/icons/safari@2x.png" width="24" height="24" />
|
||||
return <Image src="/static/icons/safari@2x.png" width="24" height="24" alt="" />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ type NoteSectionProps = {
|
|||
|
||||
export function ArticleNotes(props: NoteSectionProps): JSX.Element {
|
||||
const saveText = useCallback(
|
||||
(text) => {
|
||||
(text: string) => {
|
||||
props.saveText(text)
|
||||
},
|
||||
[props]
|
||||
|
|
@ -78,7 +78,7 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
|
|||
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
|
||||
|
||||
const saveText = useCallback(
|
||||
(text) => {
|
||||
(text: string) => {
|
||||
;(async () => {
|
||||
const success = await updateHighlightMutation({
|
||||
annotation: text,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
|
|||
const [errorSaving, setErrorSaving] = useState<string | undefined>(undefined)
|
||||
|
||||
const saveText = useCallback(
|
||||
(text, updateTime, interactive) => {
|
||||
(text: string, updateTime: Date, interactive: boolean) => {
|
||||
;(async () => {
|
||||
const success = await updateHighlightMutation({
|
||||
annotation: text,
|
||||
|
|
|
|||
|
|
@ -30,10 +30,10 @@ export function LibraryHighlightGridCard(
|
|||
props: LibraryHighlightGridCardProps
|
||||
): JSX.Element {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const higlightCount = props.item.highlights?.length ?? 0
|
||||
const highlightCount = props.item.highlights?.length ?? 0
|
||||
const router = useRouter()
|
||||
const viewInReader = useCallback(
|
||||
(highlightId) => {
|
||||
(highlightId: string) => {
|
||||
if (!router || !router.isReady || !props.viewer) {
|
||||
showErrorToast('Error navigating to highlight')
|
||||
return
|
||||
|
|
@ -197,7 +197,7 @@ export function LibraryHighlightGridCard(
|
|||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
{`View ${higlightCount} highlight${higlightCount > 1 ? 's' : ''}`}
|
||||
{`View ${highlightCount} highlight${highlightCount > 1 ? 's' : ''}`}
|
||||
<CaretDown
|
||||
size={10}
|
||||
weight="bold"
|
||||
|
|
|
|||
|
|
@ -33,13 +33,14 @@ export function About(props: AboutProps): JSX.Element {
|
|||
}}
|
||||
>
|
||||
<Box
|
||||
as="p"
|
||||
css={{
|
||||
fontWeight: '700',
|
||||
color: '#3D3D3D',
|
||||
fontSize: 45,
|
||||
lineHeight: '53px',
|
||||
padding: '10px',
|
||||
paddingBottom: '0px',
|
||||
padding: '10px 10px 0',
|
||||
margin: 0,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
|
|
@ -49,9 +50,11 @@ export function About(props: AboutProps): JSX.Element {
|
|||
readers.`}
|
||||
</Box>
|
||||
<Box
|
||||
as="p"
|
||||
css={{
|
||||
color: 'rgb(125, 125, 125)',
|
||||
padding: '10px',
|
||||
margin: 0,
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
fontWeight: '600',
|
||||
|
|
@ -64,9 +67,11 @@ export function About(props: AboutProps): JSX.Element {
|
|||
</Box>
|
||||
|
||||
<Box
|
||||
as="p"
|
||||
css={{
|
||||
color: 'rgb(125, 125, 125)',
|
||||
padding: '10px',
|
||||
margin: 0,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -32,11 +32,11 @@ export function ErrorLayout(props: ErrorLayoutProps): JSX.Element {
|
|||
</StyledText>
|
||||
</HStack>
|
||||
<SpanBox css={{ height: '64px' }} />
|
||||
<Link passHref href={viewerData?.me ? '/home' : '/login'}>
|
||||
<Link passHref href={viewerData?.me ? '/home' : '/login'} legacyBehavior>
|
||||
<Button style="ctaDarkYellow">
|
||||
{viewerData?.me ? 'Go Home' : 'Login'}
|
||||
</Button>
|
||||
</Link>
|
||||
</VStack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,21 +50,21 @@ export function LoginForm(props: LoginFormProps): JSX.Element {
|
|||
>
|
||||
Save articles and read them later in our distraction-free reader.
|
||||
</StyledText>
|
||||
<Link passHref href="/about">
|
||||
<a style={{ textDecoration: 'none' }}>
|
||||
<StyledText
|
||||
css={{
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '120%',
|
||||
m: '0px',
|
||||
color: '$omnivoreGray',
|
||||
}}
|
||||
>
|
||||
Learn More ->
|
||||
</StyledText>
|
||||
</a>
|
||||
<Link passHref href="/about" style={{ textDecoration: 'none' }}>
|
||||
|
||||
<StyledText
|
||||
css={{
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '120%',
|
||||
m: '0px',
|
||||
color: '$omnivoreGray',
|
||||
}}
|
||||
>
|
||||
Learn More ->
|
||||
</StyledText>
|
||||
|
||||
</Link>
|
||||
|
||||
<SpanBox css={{ height: '24px' }} />
|
||||
|
|
@ -103,7 +103,7 @@ export function LoginForm(props: LoginFormProps): JSX.Element {
|
|||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Link href="/auth/email-login" passHref>
|
||||
<Link href="/auth/email-login" passHref legacyBehavior>
|
||||
<StyledTextSpan
|
||||
style="actionLink"
|
||||
css={{ color: '$omnivoreGray', pt: '12px' }}
|
||||
|
|
@ -114,7 +114,7 @@ export function LoginForm(props: LoginFormProps): JSX.Element {
|
|||
</VStack>
|
||||
<TermAndConditionsFooter />
|
||||
</VStack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function GoogleAuthButton() {
|
||||
|
|
@ -154,17 +154,17 @@ export function TermAndConditionsFooter(): JSX.Element {
|
|||
}}
|
||||
>
|
||||
By signing up, you agree to Omnivore’s{' '}
|
||||
<Link href="/terms" passHref>
|
||||
<Link href="/terms" passHref legacyBehavior>
|
||||
<StyledTextSpan style="captionLink" css={{ color: '$omnivoreGray' }}>
|
||||
Terms of Service
|
||||
</StyledTextSpan>
|
||||
</Link>{' '}
|
||||
and{' '}
|
||||
<Link href="/privacy" passHref>
|
||||
<Link href="/privacy" passHref legacyBehavior>
|
||||
<StyledTextSpan style="captionLink" css={{ color: '$omnivoreGray' }}>
|
||||
Privacy Policy
|
||||
</StyledTextSpan>
|
||||
</Link>
|
||||
</StyledText>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { unstable_getImgProps as getImgProps } from 'next/image'
|
||||
import {
|
||||
Box,
|
||||
HStack,
|
||||
|
|
@ -9,6 +10,9 @@ import type { LoginFormProps } from './LoginForm'
|
|||
import { OmnivoreNameLogo } from '../elements/images/OmnivoreNameLogo'
|
||||
import { theme } from '../tokens/stitches.config'
|
||||
|
||||
import featureFullWidthImage from '../../public/static/images/login/login-feature-image-full.png'
|
||||
import featureHalfWidthImage from '../../public/static/images/login/login-feature-image-half.png'
|
||||
|
||||
export function LoginLayout(props: LoginFormProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
|
|
@ -86,7 +90,30 @@ function MediumLoginLayout(props: LoginFormProps) {
|
|||
)
|
||||
}
|
||||
|
||||
const srcSetToImageSet = (srcFallback: string, srcSet?: string): string => {
|
||||
if (!srcSet) return `url(${srcFallback})`
|
||||
|
||||
return `image-set( ${srcSet
|
||||
.split(', ')
|
||||
.map((subSrc) => {
|
||||
const [src, resolution] = subSrc.split(' ')
|
||||
return `url("${decodeURIComponent(src)}") ${resolution}`
|
||||
})
|
||||
.join(',')}
|
||||
)`
|
||||
}
|
||||
|
||||
function OmnivoreIllustration() {
|
||||
const { props: halfWidthImgProps } = getImgProps({
|
||||
src: featureHalfWidthImage,
|
||||
alt: '',
|
||||
})
|
||||
|
||||
const { props: fullWidthImgProps } = getImgProps({
|
||||
src: featureFullWidthImage,
|
||||
alt: '',
|
||||
})
|
||||
|
||||
return (
|
||||
<Box
|
||||
css={{
|
||||
|
|
@ -95,14 +122,12 @@ function OmnivoreIllustration() {
|
|||
marginLeft: 'auto',
|
||||
backgroundSize: 'cover',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundImage: `-webkit-image-set(
|
||||
url('/static/images/landingPage-feature@1x.png') 1x,
|
||||
url('/static/landing/landingPage-feature@2x.png') 2x
|
||||
)`,
|
||||
'background-image': `image-set(
|
||||
url('/static/images/landingPage-feature@1x.png') 1x,
|
||||
url('/static/landing/landingPage-feature@2x.png') 2x
|
||||
)`,
|
||||
backgroundPosition: 'left',
|
||||
backgroundImage: srcSetToImageSet(halfWidthImgProps.src, halfWidthImgProps.srcSet),
|
||||
|
||||
'@media (min-aspect-ratio: 2/1)': {
|
||||
backgroundImage: srcSetToImageSet(fullWidthImgProps.src, fullWidthImgProps.srcSet),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ function SettingsButton(props: SettingsButtonProps): JSX.Element {
|
|||
}, [props, router])
|
||||
|
||||
return (
|
||||
<Link href={props.destination} passHref title={props.name}>
|
||||
<Link href={props.destination} passHref title={props.name} legacyBehavior>
|
||||
<SpanBox
|
||||
css={{
|
||||
mx: '10px',
|
||||
|
|
@ -249,5 +249,5 @@ function SettingsButton(props: SettingsButtonProps): JSX.Element {
|
|||
{props.name}
|
||||
</SpanBox>
|
||||
</Link>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ export function UploadModal(props: UploadModalProps): JSX.Element {
|
|||
const dropzoneRef = useRef<DropzoneRef | null>(null)
|
||||
|
||||
const openDialog = useCallback(
|
||||
(event) => {
|
||||
(event: React.MouseEvent) => {
|
||||
if (dropzoneRef.current) {
|
||||
dropzoneRef.current.open()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
}, [highlights])
|
||||
|
||||
const handleSaveNoteText = useCallback(
|
||||
(text) => {
|
||||
(text: string) => {
|
||||
const changeTime = new Date()
|
||||
|
||||
setLastChanged(changeTime)
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
|
|||
props.onClose(allAnnotations ?? [], deletedHighlights ?? [])
|
||||
}, [props, allAnnotations])
|
||||
|
||||
const handleAnnotationsChange = useCallback((allAnnotations) => {
|
||||
const handleAnnotationsChange = useCallback((allAnnotations: Highlight[]) => {
|
||||
setAllAnnotations(allAnnotations)
|
||||
}, [])
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
|
|||
}, [allAnnotations])
|
||||
|
||||
const viewInReader = useCallback(
|
||||
(highlightId) => {
|
||||
(highlightId: string) => {
|
||||
props.viewHighlightInReader(highlightId)
|
||||
handleClose()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -292,7 +292,7 @@ function FontControls(props: FontControlsProps): JSX.Element {
|
|||
})
|
||||
|
||||
const handleFontSizeChange = useCallback(
|
||||
(value) => {
|
||||
(value: number) => {
|
||||
readerSettings.actionHandler('setFontSize', value)
|
||||
},
|
||||
[readerSettings]
|
||||
|
|
@ -399,7 +399,7 @@ function LayoutControls(props: LayoutControlsProps): JSX.Element {
|
|||
const { readerSettings } = props
|
||||
|
||||
const handleMarginWidthChange = useCallback(
|
||||
(value) => {
|
||||
(value: number) => {
|
||||
readerSettings.setMarginWidth(value)
|
||||
},
|
||||
[readerSettings]
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ export function EmailLogin(): JSX.Element {
|
|||
}}
|
||||
>
|
||||
Don't have an account?{' '}
|
||||
<Link href="/auth/email-signup" passHref>
|
||||
<Link href="/auth/email-signup" passHref legacyBehavior>
|
||||
<StyledTextSpan style="actionLink" css={{ color: '$omnivoreGray' }}>
|
||||
Sign up
|
||||
</StyledTextSpan>
|
||||
|
|
@ -140,7 +140,7 @@ export function EmailLogin(): JSX.Element {
|
|||
}}
|
||||
>
|
||||
Forgot your password?{' '}
|
||||
<Link href="/auth/forgot-password" passHref>
|
||||
<Link href="/auth/forgot-password" passHref legacyBehavior>
|
||||
<StyledTextSpan style="actionLink" css={{ color: '$omnivoreGray' }}>
|
||||
Click here
|
||||
</StyledTextSpan>
|
||||
|
|
@ -148,5 +148,5 @@ export function EmailLogin(): JSX.Element {
|
|||
</StyledText>
|
||||
</VStack>
|
||||
</form>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ export function EmailSignup(): JSX.Element {
|
|||
}}
|
||||
>
|
||||
Already have an account?{' '}
|
||||
<Link href="/auth/email-login" passHref>
|
||||
<Link href="/auth/email-login" passHref legacyBehavior>
|
||||
<StyledTextSpan style="actionLink" css={{ color: '$omnivoreGray' }}>
|
||||
Login instead
|
||||
</StyledTextSpan>
|
||||
|
|
@ -215,5 +215,5 @@ export function EmailSignup(): JSX.Element {
|
|||
<TermAndConditionsFooter />
|
||||
</VStack>
|
||||
</form>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element {
|
|||
<DropdownSeparator />
|
||||
<Link
|
||||
href={`/${props.viewer.profile.username}/${props.item.slug}#${props.highlight.id}`}
|
||||
>
|
||||
legacyBehavior>
|
||||
<StyledLinkItem
|
||||
onClick={(event) => {
|
||||
console.log('event.ctrlKey: ', event.ctrlKey, event.metaKey)
|
||||
|
|
@ -129,7 +129,7 @@ export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element {
|
|||
</Link>
|
||||
</Dropdown>
|
||||
</VStack>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const sortHighlights = (highlights: Highlight[]) => {
|
||||
|
|
|
|||
|
|
@ -368,7 +368,7 @@ function HighlightList(props: HighlightListProps): JSX.Element {
|
|||
}, [props.item.node.highlights])
|
||||
|
||||
const viewInReader = useCallback(
|
||||
(highlightId) => {
|
||||
(highlightId: string) => {
|
||||
if (!router || !router.isReady || !props.viewer) {
|
||||
showErrorToast('Error navigating to highlight')
|
||||
return
|
||||
|
|
|
|||
|
|
@ -351,7 +351,7 @@ export function HomeFeedContainer(): JSX.Element {
|
|||
}, [libraryItems, activeCardId])
|
||||
|
||||
const getItem = useCallback(
|
||||
(itemId) => {
|
||||
(itemId: string) => {
|
||||
return libraryItems.find((item) => item.node.id === itemId)
|
||||
},
|
||||
[libraryItems]
|
||||
|
|
|
|||
|
|
@ -553,7 +553,7 @@ type EditButtonProps = {
|
|||
|
||||
function EditButton(props: EditButtonProps): JSX.Element {
|
||||
return (
|
||||
<Link href={props.destination} passHref>
|
||||
<Link href={props.destination} passHref legacyBehavior>
|
||||
<SpanBox
|
||||
css={{
|
||||
ml: '10px',
|
||||
|
|
@ -584,5 +584,5 @@ function EditButton(props: EditButtonProps): JSX.Element {
|
|||
{props.title}
|
||||
</SpanBox>
|
||||
</Link>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ export function Webhooks(): JSX.Element {
|
|||
}}
|
||||
>
|
||||
<h3>{item.method}</h3>
|
||||
<p>{item.createdAt}</p>
|
||||
<p>{item.createdAt?.toLocaleDateString()}</p>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { HStack, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Box, HStack, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { styled } from '../../tokens/stitches.config'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
|
||||
|
|
@ -30,11 +30,29 @@ export function LandingFooter(): JSX.Element {
|
|||
textDecoration: 'underline',
|
||||
},
|
||||
},
|
||||
'@mdDown': {
|
||||
columns: 2,
|
||||
width: '100%',
|
||||
marginTop: "8px",
|
||||
marginBottom: "30px",
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<HStack css={containerStyles}>
|
||||
<HStack css={{ width: '100%', maxWidth: '1224px' }}>
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
maxWidth: '1024px',
|
||||
'@md': {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<VStack>
|
||||
<StyledText style="aboutFooter">Install</StyledText>
|
||||
<FooterList>
|
||||
|
|
@ -77,7 +95,7 @@ export function LandingFooter(): JSX.Element {
|
|||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="mailto:feedback@omnivore.app">Contact us via email</a>
|
||||
<a href="mailto:feedback@omnivore.app">Contact us via email</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://discord.gg/h2z5rppzz9">
|
||||
|
|
@ -112,7 +130,7 @@ export function LandingFooter(): JSX.Element {
|
|||
</li>
|
||||
</FooterList>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</Box>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
import { Box } from '../../elements/LayoutPrimitives'
|
||||
import { OmnivoreNameLogo } from '../../elements/images/OmnivoreNameLogo'
|
||||
import { Button } from '../../elements/Button'
|
||||
import Link from 'next/link'
|
||||
|
||||
const LoginButton = (): JSX.Element => {
|
||||
return (
|
||||
<Button
|
||||
as={Link}
|
||||
href="/login"
|
||||
style="ctaDarkYellow"
|
||||
css={{
|
||||
display: 'flex',
|
||||
marginLeft: 'auto',
|
||||
borderRadius: 4,
|
||||
border: 'unset',
|
||||
background: 'unset',
|
||||
color: '#3D3D3D',
|
||||
height: '42px',
|
||||
|
|
@ -19,10 +21,8 @@ const LoginButton = (): JSX.Element => {
|
|||
fontWeight: 'normal',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
document.location.href = '/login'
|
||||
e.preventDefault()
|
||||
textDecoration: "none",
|
||||
transition: "all ease-in 50ms"
|
||||
}}
|
||||
>
|
||||
Login
|
||||
|
|
|
|||
|
|
@ -1,50 +1,11 @@
|
|||
import { HStack, VStack, Box } from '../../elements/LayoutPrimitives'
|
||||
|
||||
type LandingSectionProps = {
|
||||
export interface LandingSectionProps {
|
||||
titleText: string
|
||||
descriptionText: React.ReactElement
|
||||
descriptionText: React.ReactElement | string | number
|
||||
icon?: React.ReactElement
|
||||
image: React.ReactElement
|
||||
}
|
||||
|
||||
const titleTextStyles = {
|
||||
fontWeight: '700',
|
||||
color: '#3D3D3D',
|
||||
lineHeight: 1.25,
|
||||
'@mdDown': {
|
||||
fontSize: 24,
|
||||
},
|
||||
'@md': {
|
||||
fontSize: '$5',
|
||||
},
|
||||
'@xl': {
|
||||
fontSize: 45,
|
||||
},
|
||||
}
|
||||
|
||||
const imageContainerStyles = {
|
||||
display: 'flex',
|
||||
width: '49%',
|
||||
alignSelf: 'center',
|
||||
justifyContent: 'center',
|
||||
'@md': {
|
||||
marginBottom: '60px',
|
||||
},
|
||||
'@mdDown': {
|
||||
width: '100%',
|
||||
},
|
||||
}
|
||||
|
||||
const layoutStyles = {
|
||||
width: '49%',
|
||||
alignSelf: 'start',
|
||||
'@mdDown': {
|
||||
width: '100%',
|
||||
paddingTop: '30px',
|
||||
},
|
||||
paddingLeft: '30px',
|
||||
paddingRight: '30px',
|
||||
paddingBottom: '30px',
|
||||
imagePosition?: 'left' | 'right'
|
||||
}
|
||||
|
||||
export function LandingSection(props: LandingSectionProps): JSX.Element {
|
||||
|
|
@ -53,24 +14,72 @@ export function LandingSection(props: LandingSectionProps): JSX.Element {
|
|||
css={{
|
||||
width: '100%',
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'row-reverse',
|
||||
flexDirection: (props?.imagePosition ?? 'left') === 'left' ? 'row-reverse' : 'row',
|
||||
marginBottom: 20,
|
||||
'@mdDown': {
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<VStack distribution="center" alignment={'center'} css={layoutStyles}>
|
||||
<Box css={titleTextStyles}>{props.titleText}</Box>
|
||||
<VStack
|
||||
distribution="center"
|
||||
alignment="center"
|
||||
css={{
|
||||
width: '49%',
|
||||
alignSelf: 'start',
|
||||
'@mdDown': {
|
||||
width: '100%',
|
||||
paddingTop: '30px',
|
||||
},
|
||||
paddingLeft: '30px',
|
||||
paddingRight: '30px',
|
||||
paddingBottom: '30px',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
as="h2"
|
||||
css={{
|
||||
color: 'rgb(125, 125, 125)',
|
||||
fontWeight: '700',
|
||||
color: '#3D3D3D',
|
||||
lineHeight: 1.25,
|
||||
'@mdDown': {
|
||||
fontSize: 24,
|
||||
},
|
||||
'@md': {
|
||||
fontSize: '$5',
|
||||
},
|
||||
'@xl': {
|
||||
fontSize: 45,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.titleText}
|
||||
</Box>
|
||||
<Box
|
||||
as="p"
|
||||
css={{
|
||||
color: '#666',
|
||||
}}
|
||||
>
|
||||
{props.descriptionText}
|
||||
</Box>
|
||||
</VStack>
|
||||
<Box css={imageContainerStyles}>{props.image}</Box>
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
width: '49%',
|
||||
alignSelf: 'center',
|
||||
justifyContent: 'center',
|
||||
'@md': {
|
||||
marginBottom: '60px',
|
||||
},
|
||||
'@mdDown': {
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.image}
|
||||
</Box>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,35 @@
|
|||
import Image from 'next/image'
|
||||
import { VStack, Box } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { LandingSection } from './LandingSection'
|
||||
|
||||
type GetStartedButtonProps = {
|
||||
lang: 'en' | 'zh'
|
||||
}
|
||||
import landingPageHeroImage from '../../../public/static/images/landing/landing-00-hero.png'
|
||||
import landingSection1Image from '../../../public/static/images/landing/landing-01-save-it-now.png'
|
||||
import landingSection2Image from '../../../public/static/images/landing/landing-02-newsletters.png'
|
||||
import landingSection3Image from '../../../public/static/images/landing/landing-03-organisation.png'
|
||||
import landingSection4Image from '../../../public/static/images/landing/landing-04-highlights-and-notes.png'
|
||||
import landingSection5Image from '../../../public/static/images/landing/landing-05-sync.png'
|
||||
import landingSection6Image from '../../../public/static/images/landing/landing-06-tts.png'
|
||||
import landingSection7Image from '../../../public/static/images/landing/landing-07-oss.png'
|
||||
import Link from 'next/link'
|
||||
|
||||
export function GetStartedButton(props: GetStartedButtonProps): JSX.Element {
|
||||
export function GetStartedButton(props: { lang: 'en' | 'zh' }): JSX.Element {
|
||||
return (
|
||||
<Button
|
||||
as={Link}
|
||||
href="/login"
|
||||
style="ctaDarkYellow"
|
||||
css={{
|
||||
display: 'flex',
|
||||
borderRadius: 4,
|
||||
background: 'rgb(255, 210, 52)',
|
||||
background: '$omnivoreCtaYellow',
|
||||
padding: '12px 25px',
|
||||
color: '#3D3D3D',
|
||||
width: '172px',
|
||||
height: '42px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: '600',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
document.location.href = '/login'
|
||||
e.preventDefault()
|
||||
textDecoration: 'none',
|
||||
transition: 'background-color ease-out 50ms',
|
||||
'&:hover': {
|
||||
backgroundColor: '$omnivoreYellow',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.lang == 'zh' ? `免费注册` : `Sign Up for Free`}
|
||||
|
|
@ -50,38 +56,6 @@ const containerStyles = {
|
|||
},
|
||||
}
|
||||
|
||||
const callToActionStyles = {
|
||||
background: 'white',
|
||||
borderRadius: '24px',
|
||||
boxSizing: 'border-box',
|
||||
border: '1px solid #D8D7D5',
|
||||
boxShadow:
|
||||
'0px 7px 8px rgba(32, 31, 29, 0.03), 0px 18px 24px rgba(32, 31, 29, 0.03)',
|
||||
padding: 40,
|
||||
marginTop: 64,
|
||||
minheight: 330,
|
||||
width: 'inherit',
|
||||
|
||||
'@md': {
|
||||
width: '100%',
|
||||
},
|
||||
'@xl': {
|
||||
width: '95%',
|
||||
},
|
||||
}
|
||||
|
||||
const callToActionText = {
|
||||
color: '#3D3D3D',
|
||||
fontWeight: '700',
|
||||
fontSize: 64,
|
||||
lineHeight: '1.25',
|
||||
textAlign: 'center',
|
||||
paddingBottom: '20px',
|
||||
'@mdDown': {
|
||||
fontSize: '32px',
|
||||
},
|
||||
}
|
||||
|
||||
type LandingSectionsContainerProps = {
|
||||
lang: 'en' | 'zh'
|
||||
}
|
||||
|
|
@ -98,7 +72,8 @@ const sections = [
|
|||
titleText: `先保存,后阅读`,
|
||||
descriptionText: `看到有趣的内容,但没时间阅读?不论是文章、PDF或是推特线程,只需将它们保存下来,等稍后有空再阅读。Omnivore 应用程序适用于iOS、Android 和主要网络浏览扩展程序。`,
|
||||
},
|
||||
imageIdx: `03`,
|
||||
image: landingSection1Image,
|
||||
imageAlt: '',
|
||||
},
|
||||
{
|
||||
en: {
|
||||
|
|
@ -111,7 +86,8 @@ const sections = [
|
|||
titleText: `集合邮件订阅`,
|
||||
descriptionText: `您不再需要到不同收件箱提取订阅的邮件,只要将它们发送到 Omnivore Library,即可在同一处随心阅读,不受其他电子邮件或 substack 的干扰。`,
|
||||
},
|
||||
imageIdx: `04`,
|
||||
image: landingSection2Image,
|
||||
imageAlt: '',
|
||||
},
|
||||
{
|
||||
en: {
|
||||
|
|
@ -125,7 +101,8 @@ const sections = [
|
|||
titleText: `按喜好组织阅读系统`,
|
||||
descriptionText: `我们不会限定您如何组织系统,只提供您所需的工具,如标签、过滤器和完整的文本索引搜索,让您按自己的喜好和需求设定组织规则。`,
|
||||
},
|
||||
imageIdx: `05`,
|
||||
image: landingSection3Image,
|
||||
imageAlt: '',
|
||||
},
|
||||
{
|
||||
en: {
|
||||
|
|
@ -139,7 +116,8 @@ const sections = [
|
|||
titleText: `添加高亮和注释`,
|
||||
descriptionText: `想提高阅读效率?积极动用大脑,为关键的段落添加高亮或注释,能提高您阅读记忆的保留。这些标注将永久保存在文件里,方便您随时搜索使用。`,
|
||||
},
|
||||
imageIdx: `06`,
|
||||
image: landingSection4Image,
|
||||
imageAlt: '',
|
||||
},
|
||||
{
|
||||
en: {
|
||||
|
|
@ -152,7 +130,8 @@ const sections = [
|
|||
titleText: `与您的“第二大脑”同步`,
|
||||
descriptionText: `Omnivore 应用程序能与个人知识管理系统如 Logseq 和 Obsidian 同步,让您轻而易举地综合所有保存文章、高亮和注释。`,
|
||||
},
|
||||
imageIdx: `07`,
|
||||
image: landingSection5Image,
|
||||
imageAlt: '',
|
||||
},
|
||||
{
|
||||
en: {
|
||||
|
|
@ -165,8 +144,8 @@ const sections = [
|
|||
titleText: `使用 text-to-speech 功能聆听阅读`,
|
||||
descriptionText: `使用TTS逼真、自然的人工智能声音为您阅读待读列表中的读物,让眼睛好好休息一下。这便是我们iOS 版Omnivore 应用程序的独家功能。`,
|
||||
},
|
||||
imageIdx: `08`,
|
||||
maxWidth: '85%',
|
||||
image: landingSection6Image,
|
||||
imageAlt: '',
|
||||
},
|
||||
{
|
||||
en: {
|
||||
|
|
@ -180,7 +159,8 @@ const sections = [
|
|||
titleText: `开源软件给予您控制权`,
|
||||
descriptionText: `阅读是终身的活动,不应担心失去自己多年辛苦建立的图书馆。我们的开源平台,就是为了确保您的阅读不会受限于任何专有系统。`,
|
||||
},
|
||||
imageIdx: `09`,
|
||||
image: landingSection7Image,
|
||||
imageAlt: '',
|
||||
},
|
||||
]
|
||||
export function LandingSectionsContainer(
|
||||
|
|
@ -198,42 +178,69 @@ export function LandingSectionsContainer(
|
|||
},
|
||||
}}
|
||||
>
|
||||
<img
|
||||
height="647"
|
||||
width="1015"
|
||||
srcSet="/static/landing/landingPage-feature@1x.png,
|
||||
/static/landing/landingPage-feature@2x.png 2x,
|
||||
/static/landing/landingPage-feature@3x.png 3x"
|
||||
alt="landingHero-1"
|
||||
<Image
|
||||
src={landingPageHeroImage}
|
||||
alt="Hero image"
|
||||
sizes="(max-width: 1024px) 100vw"
|
||||
style={{
|
||||
width: '85%',
|
||||
maxWidth: '85%',
|
||||
height: 'auto',
|
||||
}}
|
||||
priority
|
||||
blurDataURL="/static/images/landing/landing-00-hero-blurred.png"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{sections.map((section) => {
|
||||
{sections.map((section, sectionIndex) => {
|
||||
return (
|
||||
<LandingSection
|
||||
key={section.imageIdx}
|
||||
key={sectionIndex}
|
||||
titleText={section[props.lang].titleText}
|
||||
descriptionText={<p>{section[props.lang].descriptionText}</p>}
|
||||
descriptionText={section[props.lang].descriptionText}
|
||||
imagePosition={sectionIndex % 2 ? 'left' : 'right'}
|
||||
image={
|
||||
<img
|
||||
srcSet={`/static/landing/landingPage-${section.imageIdx}@1x.png,
|
||||
/static/landing/landingPage-${section.imageIdx}@2x.png 2x,
|
||||
/static/landing/landingPage-${section.imageIdx}@3x.png 3x`}
|
||||
alt={`landing-${section.imageIdx}`}
|
||||
style={{ maxWidth: section.maxWidth ?? '100%' }}
|
||||
<Image
|
||||
alt={section.imageAlt}
|
||||
src={section.image}
|
||||
sizes="(max-width: 512px) 50vw, (max-width: 512px) 100vw"
|
||||
style={{ maxWidth: '100%', height: 'auto' }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
<VStack alignment="center" css={callToActionStyles}>
|
||||
<VStack
|
||||
alignment="center"
|
||||
css={{
|
||||
width: '100vw',
|
||||
backgroundColor: '#fff',
|
||||
paddingBottom: '40px',
|
||||
marginTop: '40px',
|
||||
borderTop: '1px solid var(--colors-omnivoreYellow)',
|
||||
borderBottom: '1px solid var(--colors-omnivoreYellow)',
|
||||
'@md': {
|
||||
marginTop: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.lang == 'en' && (
|
||||
<Box css={callToActionText}>Get Started With Omnivore Today</Box>
|
||||
<Box
|
||||
as="p"
|
||||
css={{
|
||||
color: '#3D3D3D',
|
||||
fontWeight: '700',
|
||||
fontSize: '2.5rem',
|
||||
lineHeight: '1.25',
|
||||
textAlign: 'center',
|
||||
marginBottom: '40px',
|
||||
'@mdDown': {
|
||||
fontSize: '2rem',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Get Started With Omnivore Today
|
||||
</Box>
|
||||
)}
|
||||
<GetStartedButton lang={props.lang} />
|
||||
</VStack>
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ const darkThemeSpec = {
|
|||
|
||||
labelButtonsBg: '#5F5E58',
|
||||
|
||||
// New theme, special naming to keep things straigh
|
||||
// New theme, special naming to keep things straight
|
||||
// once all switch over, we will rename
|
||||
// DARK
|
||||
colorScheme: 'dark',
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ function disableWordSnap(str: string): boolean {
|
|||
}
|
||||
|
||||
const isWhitespace = (c?: string): boolean => {
|
||||
return !!c && /\u2014|\u2013|,|\s/.test(c)
|
||||
return !!c && /\u2014|\u2013|,|\s/.test(c);
|
||||
}
|
||||
|
||||
function findNextWord(
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export function useSelection(
|
|||
)
|
||||
|
||||
const handleFinishTouch = useCallback(
|
||||
async (mouseEvent) => {
|
||||
async (mouseEvent: any) => {
|
||||
let wasDragEvent = false
|
||||
const tapAttributes = {
|
||||
tapX: mouseEvent.clientX,
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export const useKeyboardShortcuts = (commands: KeyboardCommand[]): void => {
|
|||
)
|
||||
|
||||
const keydownListener = useCallback(
|
||||
(keydownEvent) => {
|
||||
(keydownEvent: any) => {
|
||||
const { target } = keydownEvent
|
||||
if (!keydownEvent.key) return
|
||||
const key = keydownEvent.key.toLowerCase()
|
||||
|
|
@ -129,7 +129,7 @@ export const useKeyboardShortcuts = (commands: KeyboardCommand[]): void => {
|
|||
)
|
||||
|
||||
const keyupListener = useCallback(
|
||||
(keyupEvent) => {
|
||||
(keyupEvent: any) => {
|
||||
if (!keyupEvent.key) return
|
||||
const key = keyupEvent.key.toLowerCase()
|
||||
if (keys[key] === undefined) return
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const ContentSecurityPolicy = `
|
|||
|
||||
const moduleExports = {
|
||||
images: {
|
||||
formats: ['image/avif', 'image/webp'],
|
||||
domains: [
|
||||
'proxy-demo.omnivore-image-cache.app',
|
||||
'proxy-dev.omnivore-image-cache.app',
|
||||
|
|
@ -56,7 +57,7 @@ const moduleExports = {
|
|||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
];
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:build": "jest && next build",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"upgrade-psdpdfkit": "cp -R '../../node_modules/pspdfkit/dist/pspdfkit-lib' public/pspdfkit-lib",
|
||||
"storybook": "start-storybook -p 6006 -s ./public",
|
||||
"build-storybook": "build-storybook -s public"
|
||||
|
|
@ -32,7 +33,6 @@
|
|||
"@radix-ui/react-tooltip": "^0.1.7",
|
||||
"@sentry/nextjs": "^7.42.0",
|
||||
"@stitches/react": "^1.2.5",
|
||||
"@types/react-input-autosize": "^2.2.1",
|
||||
"antd": "4.24.3",
|
||||
"axios": "^1.2.0",
|
||||
"color2k": "^2.0.0",
|
||||
|
|
@ -47,16 +47,16 @@
|
|||
"markdown-it": "^13.0.1",
|
||||
"match-sorter": "^6.3.1",
|
||||
"nanoid": "^3.1.29",
|
||||
"next": "^12.1.0",
|
||||
"next": "^13.5.6",
|
||||
"node-html-markdown": "^1.3.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"phosphor-react": "^1.4.0",
|
||||
"posthog-js": "^1.78.2",
|
||||
"pspdfkit": "^2022.2.3",
|
||||
"react": "^17.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-colorful": "^5.5.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hot-toast": "^2.1.1",
|
||||
"react-input-autosize": "^3.0.0",
|
||||
|
|
@ -68,6 +68,7 @@
|
|||
"react-super-responsive-table": "^5.2.1",
|
||||
"react-topbar-progress-indicator": "^4.1.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sharp": "^0.32.6",
|
||||
"swr": "^1.0.1",
|
||||
"uuid": "^8.3.2",
|
||||
"yet-another-react-lightbox": "^3.12.0"
|
||||
|
|
@ -93,14 +94,14 @@
|
|||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/markdown-it": "^12.2.3",
|
||||
"@types/papaparse": "^5.3.7",
|
||||
"@types/react": "17.0.2",
|
||||
"@types/react-color": "^3.0.6",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-color": "^3.0.9",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@types/react-input-autosize": "^2.2.1",
|
||||
"@types/uuid": "^8.3.1",
|
||||
"babel-jest": "^27.4.5",
|
||||
"babel-loader": "^8.2.3",
|
||||
"eslint-config-next": "12.0.7",
|
||||
"eslint-config-next": "^13.5.6",
|
||||
"eslint-plugin-functional": "^4.0.2",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"graphql": "^15.6.1",
|
||||
|
|
@ -108,7 +109,6 @@
|
|||
"storybook-addon-next-router": "^3.1.1"
|
||||
},
|
||||
"volta": {
|
||||
"node": "18.16.1",
|
||||
"yarn": "1.22.10"
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ import { locale, timeZone } from '../../lib/dateFormatting'
|
|||
import { SaveResponseData } from '../../lib/networking/mutations/saveUrlMutation'
|
||||
import { ssrFetcher } from '../../lib/networking/networkHelpers'
|
||||
|
||||
type Request = NextApiRequest & { cookies: { [key: string]: string } }
|
||||
type Response = NextApiResponse
|
||||
|
||||
const saveUrl = async (
|
||||
req: NextApiRequest,
|
||||
req: Request,
|
||||
url: URL,
|
||||
labels: string[] | undefined,
|
||||
state: string | undefined,
|
||||
|
|
@ -53,11 +56,7 @@ const saveUrl = async (
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default async (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
): Promise<void> => {
|
||||
export default async function handler(req: Request, res: Response) {
|
||||
const urlStr = req.query['url']
|
||||
if (req.query['labels'] && typeof req.query['labels'] === 'string') {
|
||||
req.query['labels'] = [req.query['labels']]
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export default function InvitePage(): JSX.Element {
|
|||
}, [isLoading, router, viewerData, viewerDataError])
|
||||
|
||||
const acceptClicked = useCallback(
|
||||
(event) => {
|
||||
(event: any) => {
|
||||
event?.stopPropagation()
|
||||
|
||||
if (!router.isReady) {
|
||||
|
|
@ -63,108 +63,106 @@ export default function InvitePage(): JSX.Element {
|
|||
[router, inviteCode]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMetaData title="Accept Invite - Omnivore" path="/invite" />
|
||||
<ProfileLayout>
|
||||
<VStack
|
||||
alignment="center"
|
||||
return <>
|
||||
<PageMetaData title="Accept Invite - Omnivore" path="/invite" />
|
||||
<ProfileLayout>
|
||||
<VStack
|
||||
alignment="center"
|
||||
css={{
|
||||
padding: '16px',
|
||||
background: 'white',
|
||||
minWidth: '340px',
|
||||
width: '70vw',
|
||||
maxWidth: '576px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #3D3D3D',
|
||||
boxShadow: '#B1B1B1 9px 9px 9px -9px',
|
||||
}}
|
||||
>
|
||||
<StyledText style="subHeadline" css={{ color: '$omnivoreGray' }}>
|
||||
You're invited
|
||||
</StyledText>
|
||||
|
||||
<StyledText
|
||||
style="action"
|
||||
css={{
|
||||
padding: '16px',
|
||||
background: 'white',
|
||||
minWidth: '340px',
|
||||
width: '70vw',
|
||||
maxWidth: '576px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #3D3D3D',
|
||||
boxShadow: '#B1B1B1 9px 9px 9px -9px',
|
||||
mt: '0px',
|
||||
pt: '4px',
|
||||
width: '100%',
|
||||
color: '$omnivoreLightGray',
|
||||
textAlign: 'center',
|
||||
whiteSpace: 'normal',
|
||||
}}
|
||||
>
|
||||
<StyledText style="subHeadline" css={{ color: '$omnivoreGray' }}>
|
||||
You're invited
|
||||
</StyledText>
|
||||
|
||||
You have been invited to join a recommendation group on Omnivore.
|
||||
Recommendation groups allow you to share articles with other group
|
||||
members.
|
||||
</StyledText>
|
||||
{errorMessage && (
|
||||
<StyledText
|
||||
style="action"
|
||||
style="error"
|
||||
css={{
|
||||
mt: '0px',
|
||||
pt: '4px',
|
||||
width: '100%',
|
||||
color: '$omnivoreLightGray',
|
||||
textAlign: 'center',
|
||||
whiteSpace: 'normal',
|
||||
}}
|
||||
>
|
||||
You have been invited to join a recommendation group on Omnivore.
|
||||
Recommendation groups allow you to share articles with other group
|
||||
members.
|
||||
{errorMessage}
|
||||
</StyledText>
|
||||
{errorMessage && (
|
||||
<StyledText
|
||||
style="error"
|
||||
css={{
|
||||
mt: '0px',
|
||||
pt: '4px',
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
whiteSpace: 'normal',
|
||||
}}
|
||||
>
|
||||
{errorMessage}
|
||||
</StyledText>
|
||||
)}
|
||||
)}
|
||||
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="center"
|
||||
css={{
|
||||
gap: '10px',
|
||||
width: '100%',
|
||||
height: '80px',
|
||||
}}
|
||||
>
|
||||
{viewerData?.me ? (
|
||||
<Button
|
||||
type="submit"
|
||||
style={'ctaDarkYellow'}
|
||||
onClick={acceptClicked}
|
||||
>
|
||||
Accept Invite
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="submit"
|
||||
style={'ctaDarkYellow'}
|
||||
onClick={() => router.push('/login')}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
<StyledText
|
||||
style="action"
|
||||
css={{
|
||||
m: '0px',
|
||||
pt: '16px',
|
||||
width: '100%',
|
||||
color: '$omnivoreLightGray',
|
||||
textAlign: 'center',
|
||||
whiteSpace: 'normal',
|
||||
}}
|
||||
>
|
||||
Don't have an Omnivore account?{' '}
|
||||
<Link href="/login" passHref>
|
||||
<StyledTextSpan
|
||||
style="actionLink"
|
||||
css={{ color: '$omnivoreGray' }}
|
||||
>
|
||||
Signup
|
||||
</StyledTextSpan>
|
||||
</Link>
|
||||
</StyledText>
|
||||
</VStack>
|
||||
<div data-testid="invite-page-tag" />
|
||||
</ProfileLayout>
|
||||
</>
|
||||
)
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="center"
|
||||
css={{
|
||||
gap: '10px',
|
||||
width: '100%',
|
||||
height: '80px',
|
||||
}}
|
||||
>
|
||||
{viewerData?.me ? (
|
||||
<Button
|
||||
type="submit"
|
||||
style={'ctaDarkYellow'}
|
||||
onClick={acceptClicked}
|
||||
>
|
||||
Accept Invite
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="submit"
|
||||
style={'ctaDarkYellow'}
|
||||
onClick={() => router.push('/login')}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
<StyledText
|
||||
style="action"
|
||||
css={{
|
||||
m: '0px',
|
||||
pt: '16px',
|
||||
width: '100%',
|
||||
color: '$omnivoreLightGray',
|
||||
textAlign: 'center',
|
||||
whiteSpace: 'normal',
|
||||
}}
|
||||
>
|
||||
Don't have an Omnivore account?{' '}
|
||||
<Link href="/login" passHref legacyBehavior>
|
||||
<StyledTextSpan
|
||||
style="actionLink"
|
||||
css={{ color: '$omnivoreGray' }}
|
||||
>
|
||||
Signup
|
||||
</StyledTextSpan>
|
||||
</Link>
|
||||
</StyledText>
|
||||
</VStack>
|
||||
<div data-testid="invite-page-tag" />
|
||||
</ProfileLayout>
|
||||
</>;
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue