diff --git a/package.json b/package.json index 7755a2e..e033cde 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,9 @@ "vite-plugin-singlefile": "^0.13.2" }, "dependencies": { + "@citation-js/core": "^0.7.1", + "@citation-js/plugin-bibtex": "^0.7.1", + "@citation-js/plugin-csl": "^0.7.14", "@iconify/json": "^2.2.15", "@json-editor/json-editor": "^2.9.0-beta.1", "ajv": "^8.12.0", diff --git a/sample/sample.cv.prof.json b/sample/sample.cv.prof.json new file mode 100644 index 0000000..e604c7e --- /dev/null +++ b/sample/sample.cv.prof.json @@ -0,0 +1,758 @@ +{ + "$schema": "https://raw.githubusercontent.com/cuihantao/jsoncv/refs/heads/master/schema/jsoncv.schema.prof.json", + "basics": { + "name": "Hantao Cui", + "label": "Associate Professor", + "image": "", + "email": "hcui9@ncsu.edu", + "phone": "(919) 513-2024", + "url": "https://cui.eecps.com", + "summary": "Research focuses on computational and analytical techniques for power systems: modeling, simulation, stability analysis, control, and software engineering", + "location": { + "address": "21 Keystone Science Center, 1791 Varsity Dr.", + "postalCode": "27606", + "city": "Raleigh", + "region": "NC", + "countryCode": "US" + }, + "profiles": [ + { + "network": "Google Scholar", + "username": "Hantao Cui", + "url": "https://scholar.google.com/citations?user=yvsfeQEAAAAJ" + }, + { + "network": "LinkedIn", + "username": "Hantao Cui", + "url": "https://www.linkedin.com/in/hantao-cui/" + } + ] + }, + "work": [ + { + "name": "North Carolina State University", + "location": "Raleigh, NC", + "description": "Department of Electrical and Computer Engineering", + "position": "Associate Professor", + "url": "https://www.ncsu.edu", + "startDate": "2024-08-16", + "summary": "Research in power systems modeling and simulation, high-performance computing, and machine learning applications" + }, + { + "name": "Oklahoma State University", + "location": "Stillwater, OK", + "description": "School of Electrical and Computer Engineering", + "position": "Assistant Professor", + "url": "https://www.okstate.edu", + "startDate": "2021-08-01", + "endDate": "2024-08-15", + "summary": "Research and teaching in power systems and scientific computing" + }, + { + "name": "University of Tennessee, Knoxville", + "location": "Knoxville, TN", + "description": "Department of Electrical Engineering and Computer Science", + "position": "Research Assistant Professor", + "url": "https://www.utk.edu", + "startDate": "2019-04-01", + "endDate": "2021-07-31", + "summary": "Research in power system simulation and open-source software development" + }, + { + "name": "University of Tennessee, Knoxville", + "location": "Knoxville, TN", + "description": "Center for Ultra-Wide-Area Resilient Electric Transmission Networks (CURENT)", + "position": "Research Associate", + "url": "https://www.utk.edu", + "startDate": "2017-01-01", + "endDate": "2019-04-30", + "summary": "Research in large-scale power system simulation and cyber-physical systems" + } + ], + "volunteer": [ + { + "organization": "2027 North American Power Symposium", + "location": "Raleigh, NC", + "position": "Co-Chair", + "url": "", + "startDate": "2024", + "endDate": "2027", + "summary": "Co-chairing the organization of the 2027 North American Power Symposium", + "highlights": [] + }, + { + "organization": "NSF CSSI-CyberTraining PI Meeting", + "location": "Charlotte, NC", + "position": "Organizing Committee Member", + "url": "", + "startDate": "2024-04-01", + "endDate": "2024-08-31", + "summary": "Served on the organizing committee for the NSF CSSI-CyberTraining PI Meeting", + "highlights": [] + }, + { + "organization": "IEEE Power and Energy Society", + "position": "Vice Chair and TCPC", + "url": "", + "startDate": "2023-01-01", + "summary": "Vice Chair and TCPC of Computer and Analytical Methods Subcommittee (CAMS)", + "highlights": [] + }, + { + "organization": "IEEE Power and Energy Society", + "position": "Secretary", + "url": "", + "startDate": "2018-08-01", + "endDate": "2023-01-01", + "summary": "Secretary of Ultra-Wide-Area HVDC Overlay Studies Task Force", + "highlights": [] + } + ], + "education": [ + { + "institution": "University of Tennessee, Knoxville", + "location": "Tennessee, USA", + "url": "https://www.utk.edu", + "area": "Electrical Engineering and Computer Science", + "studyType": "Ph.D.", + "startDate": "2013-08-20", + "endDate": "2018-12-18", + "score": "", + "dissertation": "Large-Scale Simulation of Modern Electric Power Systems", + "courses": [] + }, + { + "institution": "Southeast University", + "location": "Nanjing, China", + "url": "https://www.seu.edu.cn", + "area": "Electrical Engineering", + "studyType": "M.S.", + "startDate": "2011-09-01", + "endDate": "2013-06-30", + "score": "", + "courses": [] + }, + { + "institution": "Southeast University", + "location": "Nanjing, China", + "url": "https://www.seu.edu.cn", + "area": "Electrical Engineering | Chien-Shiung Wu Honor College", + "studyType": "B.S.", + "startDate": "2007-09-01", + "endDate": "2011-06-30", + "score": "", + "courses": [] + } + ], + "awards": [ + { + "title": "IEEE PES PSOPE Committee Prize Paper Award", + "date": "2024-02-01", + "awarder": "IEEE Power and Energy Society" + }, + { + "title": "National Science Foundation CAREER Award", + "date": "2024-01-01", + "awarder": "National Science Foundation" + }, + { + "title": "Best Conference Paper, 3rd Place", + "date": "2023-10-01", + "awarder": "North American Power Symposium (NAPS)" + }, + { + "title": "Best Conference Paper", + "date": "2022-07-01", + "awarder": "IEEE PES General Meeting" + }, + { + "title": "Outstanding Reviewer for 2020", + "date": "2021-01-30", + "awarder": "IEEE Transactions on Power Systems" + }, + { + "title": "R&D 100 Award for CURENT Large-Scale Testbed", + "date": "2020-09-30", + "awarder": "R&D Magazine" + }, + { + "title": "Outstanding Reviewer for 2019", + "date": "2020-03-01", + "awarder": "IEEE Transactions on Power Systems" + }, + { + "title": "Highly Cited Paper Award", + "date": "2019-07-01", + "awarder": "Applied Energy" + }, + { + "title": "Top Peer Reviewer (Top 1% in Engineering)", + "date": "2018-09-30", + "awarder": "Publons.com" + }, + { + "title": "ESI Highly Cited Papers", + "date": "2018-03-01", + "awarder": "Essential Science Indicators" + }, + { + "title": "Chancellor's Citation on Extraordinary Professional Promise", + "date": "2017-04-18", + "awarder": "University of Tennessee, Knoxville" + }, + { + "title": "Best Conference Paper", + "date": "2016-07-01", + "awarder": "IEEE PES General Meeting" + } + ], + "certificates": [ + { + "name": "IEEE Senior Member", + "date": "2020-12-20", + "issuer": "IEEE", + "url": "" + } + ], + "publications": [ + { + "name": "Evaluating the Equivalent Inertia of Grid-Following and Grid-Forming Inverter-Based Resources", + "publisher": "IEEE Transactions on Energy Conversion", + "releaseDate": "2024", + "url": "https://doi.org/10.1109/TEC.2024.3520383", + "summary": "Novel method for estimating equivalent inertia in modern power systems with inverter-based resources" + }, + { + "name": "Bus Admittance Matrix Revisited: Performance Challenges on Modern Computers", + "publisher": "IEEE Open Access Journal of Power and Energy", + "releaseDate": "2024", + "url": "https://doi.org/10.1109/OAJPE.2024.3366117", + "summary": "Analysis of computational performance challenges in power system matrix operations on modern computing architectures" + }, + { + "name": "Virtual Inertia Scheduling (VIS) for Microgrids with Static and Dynamic Security Constraints", + "publisher": "IEEE Transactions on Sustainable Energy", + "releaseDate": "2024", + "url": "https://doi.org/10.1109/TSTE.2024.3481239", + "summary": "Novel approach to scheduling virtual inertia in microgrids considering both static and dynamic security constraints" + }, + { + "name": "Hybrid Symbolic-Numeric Framework for Power System Modeling and Analysis", + "publisher": "IEEE Transactions on Power Systems", + "releaseDate": "2020", + "url": "https://doi.org/10.1109/TPWRS.2020.3017019", + "summary": "Innovative framework combining symbolic and numerical methods for power system modeling and simulation" + }, + { + "name": "Power Electronics-Interfaced Cyber-Physical Power Systems: A Review on Modeling, Simulation, and Cybersecurity", + "publisher": "WIREs Energy and Environment", + "releaseDate": "2022", + "url": "", + "summary": "Comprehensive review of modeling, simulation, and cybersecurity aspects in modern power systems with power electronic interfaces" + } + ], + "skills": [ + { + "name": "Power Systems", + "level": "Expert", + "summary": "Expert in power system modeling, simulation, stability analysis, and control systems", + "keywords": [ + "Modeling and Simulation", + "Stability Analysis", + "Control Systems", + "High Performance Computing", + "Software Engineering", + "Machine Learning Applications" + ] + }, + { + "name": "Software Development", + "level": "Expert", + "summary": "Extensive experience in scientific computing and high-performance software development", + "keywords": [ + "Python", + "Julia", + "High Performance Computing", + "Open Source Development", + "Symbolic Computing" + ] + } + ], + "languages": [ + { + "language": "English", + "fluency": "Professional", + "summary": "Professional working proficiency in academic and technical contexts" + }, + { + "language": "Chinese", + "fluency": "Native", + "summary": "Native speaker" + } + ], + "interests": [ + { + "name": "Power Systems Research", + "keywords": [ + "Grid Modeling", + "Stability Analysis", + "Control Systems", + "Renewable Integration", + "High Performance Computing" + ] + }, + { + "name": "Software Development", + "keywords": [ + "Open Source", + "Power System Simulation", + "High Performance Computing", + "Machine Learning" + ] + } + ], + "projects": [ + { + "name": "ANDES", + "description": "Python Software for Power System Modeling and Simulation", + "summary": "Python Software for Power System Modeling and Simulation. A unique hybrid symbolic-numeric framework that enables descriptive DAE modeling, numerical code generation, and just-in-time compilation for simulation.", + "highlights": [ + "Over 4,000 code commits", + "170+ GitHub stars", + "80+ forks", + "Used in multiple NSF and DOE projects" + ], + "keywords": ["Python", "Power Systems", "Simulation"], + "startDate": "2015", + "endDate": "Present", + "url": "https://github.com/curent/andes", + "roles": ["Lead Developer"], + "entity": "CURENT", + "type": "Software" + }, + { + "name": "NSF CAREER Award", + "description": "Multi-Timescale Dynamics Modeling, Simulation, and Analysis of Converter-Dominated Power Systems", + "summary": "Multi-Timescale Dynamics Modeling, Simulation, and Analysis of Converter-Dominated Power Systems", + "highlights": [ + "Principal Investigator", + "Research on power system dynamics" + ], + "keywords": ["Power Systems", "Dynamics", "Simulation"], + "startDate": "2024-09", + "endDate": "2029-08", + "url": "", + "roles": ["Principal Investigator"], + "entity": "NSF", + "type": "Research", + "projectType": "research", + "fundingDetails": { + "role": "PI", + "fundingAgency": "NSF", + "amount": "$500,000", + "status": "active" + } + }, + { + "name": "POSE", + "description": "Toward an Open-Source Ecosystem for Power Systems Research, Education, and Industry Applications", + "summary": "Toward an Open-Source Ecosystem for Power Systems Research, Education, and Industry Applications", + "highlights": [ + "NSF TIP funded project", + "Principal Investigator", + "Open-source power system software" + ], + "keywords": ["Open Source", "Power Systems", "Education"], + "startDate": "2024-09", + "endDate": "2025-08", + "url": "", + "roles": ["Principal Investigator"], + "entity": "NSF", + "type": "Research", + "projectType": "research", + "fundingDetails": { + "role": "PI", + "fundingAgency": "NSF", + "amount": "$300,000", + "status": "active" + } + }, + { + "name": "HVDC-Learn", + "description": "Modular Education & Workforce Training in High Voltage Direct Current Electric Transmission", + "summary": "Modular Education & Workforce Training in High Voltage Direct Current Electric Transmission", + "highlights": [ + "Co-Principal Investigator", + "Multi-university collaboration", + "Education and workforce development" + ], + "keywords": ["Education", "HVDC", "Power Systems"], + "startDate": "2024-01", + "endDate": "2026-12", + "url": "", + "roles": ["Co-Principal Investigator"], + "entity": "DOE", + "type": "Education", + "projectType": "education", + "fundingDetails": { + "role": "co-PI", + "fundingAgency": "DOE", + "amount": "$48,875", + "status": "active" + } + }, + { + "name": "PowerCyber", + "description": "CyberTraining: Pilot: PowerCyber: Computational Training for Power Engineering Researchers", + "summary": "CyberTraining: Pilot: PowerCyber: Computational Training for Power Engineering Researchers", + "highlights": [ + "Lead Principal Investigator", + "Computational training development" + ], + "keywords": ["Education", "Power Systems", "Computation"], + "startDate": "2024-01", + "endDate": "2025-12", + "url": "", + "roles": ["Lead Principal Investigator"], + "entity": "NSF", + "type": "Education", + "projectType": "education", + "fundingDetails": { + "role": "PI", + "fundingAgency": "NSF", + "amount": "$180,000", + "status": "active" + } + }, + { + "name": "High-Performance Power System Simulation", + "description": "High-Performance Transient Stability Simulation of Power Systems on Modern Parallel Computing Hardware", + "summary": "High-Performance Transient Stability Simulation of Power Systems on Modern Parallel Computing Hardware", + "highlights": [ + "Principal Investigator", + "Advanced computing research" + ], + "keywords": ["HPC", "Power Systems", "Simulation"], + "startDate": "2022-09", + "endDate": "2025-08", + "url": "", + "roles": ["Principal Investigator"], + "entity": "NSF", + "type": "Research", + "projectType": "research", + "fundingDetails": { + "role": "PI", + "fundingAgency": "NSF", + "amount": "$320,000", + "status": "active" + } + } + ], + "mentoring": { + "current_students": [ + { + "name": "Ahmad Ali", + "level": "Ph.D.", + "startDate": "2022-01", + "url": "", + "dissertation": "High-Performance Computing for Power System Dynamic Simulation" + } + ], + "committee_service": [ + { + "name": "Zaid Ibn Mahmood", + "institution": "Oklahoma State University", + "status": "Current" + } + ], + "past_students": [ + { + "name": "Haya Monawwar", + "level": "Ph.D. Student", + "period": "Fall 23, Spring 24", + "institution": "Oklahoma State University", + "url": "" + }, + { + "name": "Karter Caves", + "level": "Undergraduate", + "period": "Fall 23, Spring 24", + "program": "Freshman Research Scholar", + "institution": "Oklahoma State University", + "url": "" + }, + { + "name": "Evelyn Wilson", + "level": "Undergraduate", + "period": "Spring 22, Fall 23", + "institution": "Oklahoma State University" + }, + { + "name": "Paul Magna", + "level": "Undergraduate", + "period": "Fall 21", + "institution": "Oklahoma State University" + } + ] + }, + "teaching": [ + { + "courseCode": "ECEN 5113", + "courseName": "Power System Analysis by Computer Methods", + "description": "Advanced course covering numerical methods for power system analysis, including power flow, optimal power flow, and dynamic simulation. Emphasis on high-performance computing and modern software development practices.", + "institution": "Oklahoma State University", + "terms": ["S'23", "F'21"] + }, + { + "courseCode": "ECEN 4153", + "courseName": "Power System Analysis and Design", + "description": "Fundamentals of power system analysis, including per-unit systems, power flow studies, economic operation, and symmetrical components. Introduction to power system stability and protection.", + "institution": "Oklahoma State University", + "terms": ["F'22", "S'24"] + }, + { + "courseCode": "ECEN 4283", + "courseName": "Computer Networks", + "institution": "Oklahoma State University", + "terms": ["S'22", "F'23"] + }, + { + "courseCode": "ECE 421", + "courseName": "Electric Energy Systems (co-instructor)", + "institution": "University of Tennessee, Knoxville", + "terms": ["F'19"] + }, + { + "courseCode": "ECE 496/691", + "courseName": "Power and Energy Systems Seminar", + "institution": "University of Tennessee, Knoxville", + "terms": ["F'19", "S'21"] + } + ], + "researchAreas": [ + { + "name": "Advanced Grid Modeling, Simulation, and Stability Analysis", + "contributions": [ + "Proposed a symbolic-numeric framework for rapid prototyping of dynamic models", + "Proposed a communication-in-the-loop testbed architecture for wide-area control", + "Reviewed power electronics control and cybersecurity challenges", + "Studied disturbance propagation in systems with grid-following and grid-forming converters" + ] + }, + { + "name": "High-Performance Computing", + "contributions": [ + "Effective parallelism for accelerating equation and Jacobian evaluation in power flow", + "Revisit the computational performance of the Ybus method on modern computers with data parallelism", + "Understanding the performance of preconditioned iterative methods for fast-decoupled power flow on GPUs" + ] + }, + { + "name": "Emerging Learning Techniques for Control", + "contributions": [ + "Development of open-source Andes-Gym environment to support reinforcement learning-based control in power systems", + "Using machine learning to integrate dynamic response in a unit-commitment problem", + "Transmission and distribution co-simulation and control" + ] + } + ], + "services": { + "editorialBoards": [ + { + "role": "Associate Editor", + "journal": "IET Energy Conversion and Economics", + "period": "2012-Present" + }, + { + "role": "Associate Editor", + "journal": "Protection and Control of Modern Power Systems", + "period": "2012-Present" + }, + { + "role": "Associate Editor", + "journal": "Journal of Modern Power Systems and Clean Energy", + "period": "2019-Present" + }, + { + "role": "Guest Editor", + "journal": "Frontiers in Energy Research", + "period": "2022", + "details": "Special Issue: Advances in Distributed Energy Resources Aggregation for the Low Carbon Future" + }, + { + "role": "Guest Editor", + "journal": "IET Generation, Transmission and Distribution", + "period": "2021" + } + ], + "societyServices": [ + { + "role": "Vice Chair and TCPC", + "organization": "Computer and Analytical Methods Subcommittee (CAMS), IEEE Power and Energy Society", + "period": "2023-Present" + }, + { + "role": "Secretary", + "organization": "Ultra-Wide-Area HVDC Overlay Studies Task Force, IEEE Power and Energy Society", + "period": "2018-Present" + }, + { + "role": "Member", + "organization": "Power System Stability Control Subcommittee", + "period": "2022-Present" + } + ], + "reviewServices": [ + { + "type": "Proposal Review", + "organization": "National Science Foundation", + "year": "2024", + "details": "Reviewer for three panels" + }, + { + "type": "Proposal Review", + "organization": "Department of Energy", + "year": "2023", + "details": "Panel Reviewer for two panels" + } + ], + "departmental": [ + { + "role": "Member", + "committee": "Colloquium Committee", + "period": "2024-Present" + } + ] + }, + "presentations": { + "invitedTalks": [ + { + "venue": "IEEE PES Young Professional Phoenix Chapter", + "type": "Webinar", + "date": "2024-05", + "title": "The Pursuit for a Generalized Simulation Framework for Power System Transients and Stability" + }, + { + "venue": "Stony Brook University", + "type": "Webinar", + "date": "2024-05", + "title": "Hybrid-Symbolic Numeric Framework for Power System Dynamics: ANDES Package and Future Directions" + }, + { + "venue": "NC State University", + "type": "Seminar", + "date": "2024-03", + "title": "The New Dynamics: Computational Solutions to Modeling and Simulation of Sustainable Power Systems" + }, + { + "venue": "Clarkson University", + "type": "Webinar", + "date": "2024-03", + "title": "Hybrid-Symbolic Numeric Framework forPower System Dynamics: ANDES Package and Future Directions" + }, + { + "venue": "SUNY Buffalo", + "type": "Seminar", + "date": "2024-03", + "title": "The New Dynamics: Computational Solutions to Modeling and Simulation of Sustainable Power Systems" + }, + { + "venue": "Southern Methodist University", + "type": "Seminar", + "date": "2024-03", + "title": "The New Dynamics: Computational Solutions to Modeling and Simulation of Sustainable Power Systems" + }, + { + "venue": "Rowan University", + "type": "Seminar", + "date": "2024-02", + "title": "The New Dynamics: Computational Solutions to Modeling and Simulation of Sustainable Power Systems" + }, + { + "venue": "University of Georgia", + "type": "Seminar", + "date": "2023-02", + "title": "Advancing Cyber-Physical Power Systems Research Using Modern Computer Technologies" + }, + { + "venue": "University of Houston", + "type": "Webinar", + "date": "2022-11", + "title": "Symbolics-Assisted Modeling for High-Performance Power System Simulation" + }, + { + "venue": "Washington State University", + "type": "Webinar", + "date": "2022-10", + "title": "Composable, Expressive and Portable Modeling for High-Performance Power System Simulation" + }, + { + "venue": "CURENT Industry Members", + "type": "Presentation", + "date": "2022-06", + "title": "Disturbance Propagation in Power Grids With High Converter Penetration" + } + ], + "conferences": [ + { + "title": "Andes_gym: A Versatile Environment for Deep Reinforcement Learning in Power Systems", + "venue": "2022 IEEE PESGM", + "location": "Denver", + "date": "2022-06", + "type": "Best Paper Presentation", + "description": "Presented a novel open-source environment for applying reinforcement learning to power system control problems, featuring integration with ANDES for high-fidelity power system simulation.", + "url": "" + }, + { + "title": "Application of Electric Grid Simulators for Education", + "venue": "2021 IEEE PESGM", + "location": "Online", + "date": "2021-07", + "type": "Panel Session Presentation", + "description": "Discussed innovative approaches to using power system simulators in engineering education, focusing on hands-on learning and real-world applications.", + "url": "" + }, + { + "title": "HVDC Overlays in Testbeds", + "venue": "2019 IEEE PESGM", + "location": "Atlanta", + "date": "2019-08", + "type": "Panel Session Presentation", + "url": "" + } + ], + "outreach": [ + { + "activity": "Faculty mentor for OSU CEAT Summer Bridge", + "date": "2023-07", + "description": "Mentored incoming engineering students during the summer bridge program, introducing them to power systems engineering and research opportunities in renewable energy integration." + }, + { + "activity": "Organizer, Student visit to Central Rural Electric Coop, Stillwater", + "date": "2022-11", + "description": "Organized and led a field trip for undergraduate students to learn about electric cooperative operations, smart grid technologies, and career opportunities in power distribution." + }, + { + "activity": "Co-Chaired Panel Session at 2022 IEEE PESGM: Combining physics-based and data-driven modeling and simulation for power systems", + "date": "2022-08" + }, + { + "activity": "Faculty mentor for OSU CEAT Summer Bridge", + "date": "2022-07" + }, + { + "activity": "Faculty mentor for OSU Discovery Day", + "date": "2022-04" + } + ] + }, + "meta": { + "canonical": "https://raw.githubusercontent.com/cuihantao/jsoncv/refs/heads/master/schema/jsoncv.schema.prof.json", + "lastModified": "2025-01-02T00:00:00Z", + "colorPrimary": "#CC0000", + "citationStyles": { + "owner": "bold", + "mentee": "plus" + } + } +} \ No newline at end of file diff --git a/schema/jsoncv.schema.json b/schema/jsoncv.schema.json index 5fe30dd..29fc814 100644 --- a/schema/jsoncv.schema.json +++ b/schema/jsoncv.schema.json @@ -284,30 +284,60 @@ }, "publications": { "type": "array", - "description": "Specify your publications through your career", "items": { "type": "object", - "additionalProperties": true, "properties": { "name": { "type": "string", - "description": "e.g. The World Wide Web" + "description": "Title of the publication" + }, + "authors": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of authors with highlighting markers" }, - "publisher": { + "venue": { "type": "string", - "description": "e.g. IEEE, Computer Magazine" + "description": "Journal or conference name" }, "releaseDate": { - "$ref": "#/definitions/iso8601" + "type": "string", + "description": "Publication year" }, "url": { "type": "string", - "description": "e.g. http://www.computer.org.example.com/csdl/mags/co/1996/10/rx069-abs.html", - "format": "uri" + "format": "uri", + "description": "DOI or URL to the publication" }, "summary": { "type": "string", - "description": "Short summary of publication. e.g. Discussion of the World Wide Web, HTTP, HTML." + "description": "Abstract or summary" + }, + "citation": { + "type": "string", + "description": "Pre-formatted citation" + }, + "formattedHTML": { + "type": "string", + "description": "HTML-formatted citation" + }, + "type": { + "type": "string", + "enum": ["journal", "conference", "other"], + "description": "Type of publication" + }, + "highlightInfo": { + "type": "object", + "properties": { + "hasOwner": { + "type": "boolean" + }, + "hasMentee": { + "type": "boolean" + } + } } } } @@ -516,11 +546,27 @@ }, "version": { "type": "string", - "description": "A version field which follows semver - e.g. v1.0.0" + "description": "Version of the CV" }, "lastModified": { "type": "string", - "description": "Using ISO 8601 with YYYY-MM-DDThh:mm:ss" + "description": "Last modified time of the CV" + }, + "colorPrimary": { + "type": "string", + "description": "Primary color for the CV theme" + }, + "pageSize": { + "type": "string", + "description": "Page size for the CV (A4 or letter)", + "enum": ["A4", "letter"], + "default": "A4" + }, + "theme": { + "type": "string", + "description": "Theme for the CV", + "enum": ["reorx", "cuiv"], + "default": "reorx" } } } diff --git a/schema/jsoncv.schema.prof.json b/schema/jsoncv.schema.prof.json new file mode 100644 index 0000000..8028fb9 --- /dev/null +++ b/schema/jsoncv.schema.prof.json @@ -0,0 +1,1046 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "additionalProperties": false, + "definitions": { + "iso8601": { + "type": "string", + "description": "e.g. 2014-06-29", + "format": "date", + "pattern": "^([1-2][0-9]{3}-[0-1][0-9]-[0-3][0-9]|[1-2][0-9]{3}-[0-1][0-9]|[1-2][0-9]{3})$" + } + }, + "properties": { + "$schema": { + "type": "string", + "description": "link to the version of the schema that can validate the CV", + "format": "uri" + }, + "basics": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "type": "string" + }, + "label": { + "type": "string", + "description": "e.g. Web Developer" + }, + "image": { + "type": "string", + "description": "URL (as per RFC 3986) to a image in JPEG or PNG format" + }, + "email": { + "type": "string", + "description": "e.g. thomas@gmail.com", + "format": "email" + }, + "phone": { + "type": "string", + "description": "Phone numbers are stored as strings so use any format you like, e.g. 712-117-2923" + }, + "url": { + "type": "string", + "description": "URL (as per RFC 3986) to your website, e.g. personal homepage", + "format": "uri" + }, + "summary": { + "type": "string", + "description": "Write a short 2-3 sentence biography about yourself" + }, + "location": { + "type": "object", + "additionalProperties": true, + "properties": { + "address": { + "type": "string", + "description": "To add multiple address lines, use \n. For example, 1234 Glücklichkeit Straße\nHinterhaus 5. Etage li." + }, + "postalCode": { + "type": "string" + }, + "city": { + "type": "string" + }, + "countryCode": { + "type": "string", + "description": "code as per ISO-3166-1 ALPHA-2, e.g. US, AU, IN" + }, + "region": { + "type": "string", + "description": "The general region where you live. Can be a US state, or a province, for instance." + } + } + }, + "profiles": { + "type": "array", + "description": "Specify any number of social networks that you participate in", + "items": { + "type": "object", + "properties": { + "network": { + "type": "string", + "description": "The name of the social network (e.g. LinkedIn, Google Scholar, Scopus)", + "enum": ["LinkedIn", "Google Scholar", "Scopus"] + }, + "username": { + "type": "string", + "description": "e.g. your-profile-name" + }, + "url": { + "type": "string", + "description": "Your profile URL on this network", + "format": "uri" + } + }, + "required": ["network", "url"] + } + } + } + }, + "work": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "description": "e.g. Facebook" + }, + "location": { + "type": "string", + "description": "e.g. Menlo Park, CA" + }, + "description": { + "type": "string", + "description": "e.g. Social Media Company" + }, + "position": { + "type": "string", + "description": "e.g. Software Engineer" + }, + "url": { + "type": "string", + "description": "e.g. http://facebook.example.com", + "format": "uri" + }, + "startDate": { + "$ref": "#/definitions/iso8601" + }, + "endDate": { + "$ref": "#/definitions/iso8601" + }, + "summary": { + "type": "string", + "description": "Give an overview of your responsibilities at the company" + }, + "highlights": { + "type": "array", + "description": "Specify multiple accomplishments", + "items": { + "type": "string", + "description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising" + } + } + } + } + }, + "volunteer": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "organization": { + "type": "string", + "description": "e.g. Facebook" + }, + "position": { + "type": "string", + "description": "e.g. Software Engineer" + }, + "url": { + "type": "string", + "description": "e.g. http://facebook.example.com", + "format": "uri" + }, + "startDate": { + "$ref": "#/definitions/iso8601" + }, + "endDate": { + "$ref": "#/definitions/iso8601" + }, + "summary": { + "type": "string", + "description": "Give an overview of your responsibilities at the company" + }, + "highlights": { + "type": "array", + "description": "Specify accomplishments and achievements", + "items": { + "type": "string", + "description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising" + } + } + } + } + }, + "education": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "institution": { + "type": "string", + "description": "e.g. Massachusetts Institute of Technology" + }, + "url": { + "type": "string", + "description": "e.g. http://facebook.example.com", + "format": "uri" + }, + "area": { + "type": "string", + "description": "e.g. Arts" + }, + "studyType": { + "type": "string", + "description": "e.g. Bachelor" + }, + "startDate": { + "$ref": "#/definitions/iso8601" + }, + "endDate": { + "$ref": "#/definitions/iso8601" + }, + "score": { + "type": "string", + "description": "grade point average, e.g. 3.67/4.0" + }, + "thesis": { + "type": "string", + "description": "Title of the thesis for Master's degree" + }, + "dissertation": { + "type": "string", + "description": "Title of the dissertation for Doctoral degree" + }, + "courses": { + "type": "array", + "description": "List notable courses/subjects", + "items": { + "type": "string", + "description": "e.g. H1302 - Introduction to American history" + } + } + } + } + }, + "awards": { + "type": "array", + "description": "Specify any awards you have received throughout your professional career", + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "title": { + "type": "string", + "description": "e.g. One of the 100 greatest minds of the century" + }, + "date": { + "$ref": "#/definitions/iso8601" + }, + "awarder": { + "type": "string", + "description": "e.g. Time Magazine" + }, + "summary": { + "type": "string", + "description": "e.g. Received for my work with Quantum Physics" + } + } + } + }, + "certificates": { + "type": "array", + "description": "Specify any certificates you have received throughout your professional career", + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "description": "e.g. Certified Kubernetes Administrator" + }, + "date": { + "type": "string", + "description": "e.g. 1989-06-12", + "format": "date" + }, + "url": { + "type": "string", + "description": "e.g. http://example.com", + "format": "uri" + }, + "issuer": { + "type": "string", + "description": "e.g. CNCF" + } + } + } + }, + "publications": { + "type": "array", + "description": "Publications from both BibTeX and manual entry", + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "description": "Title of the publication" + }, + "type": { + "type": "string", + "enum": ["journal", "conference", "other"], + "description": "Type of publication" + }, + "url": { + "type": "string", + "format": "uri", + "description": "DOI or URL to the publication" + }, + "formattedHTML": { + "type": "string", + "description": "Pre-formatted citation HTML (from BibTeX or manual)" + }, + "source": { + "type": "string", + "enum": ["bibtex", "manual"], + "description": "Source of this publication entry" + }, + "authors": { + "type": "array", + "items": { "type": "string" }, + "description": "List of authors" + }, + "publisher": { + "type": "string", + "description": "Journal/conference name or publisher" + }, + "releaseDate": { + "type": "string", + "description": "Publication date" + }, + "citationKey": { + "type": "string", + "description": "BibTeX citation key (for BibTeX entries)" + }, + "bibtexEntry": { + "type": "object", + "description": "Full BibTeX entry data (for BibTeX entries)" + } + }, + "required": ["name", "type", "formattedHTML", "source"] + } + }, + "skills": { + "type": "array", + "description": "List out your professional skill-set", + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "description": "e.g. Web Development" + }, + "level": { + "type": "string", + "description": "e.g. Master" + }, + "summary": { + "type": "string", + "description": "e.g. Expert level proficiency in web development with extensive knowledge and experience in multiple programming languages, frameworks and web technologies." + }, + "keywords": { + "type": "array", + "description": "List some keywords pertaining to this skill", + "items": { + "type": "string", + "description": "e.g. HTML" + } + } + } + } + }, + "languages": { + "type": "array", + "description": "List any other languages you speak", + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "language": { + "type": "string", + "description": "e.g. English, Spanish" + }, + "fluency": { + "type": "string", + "description": "e.g. Fluent, Beginner" + }, + "summary": { + "type": "string", + "description": "e.g. Proficient in English with a pretty good level of speaking, writing and comprehension abilities." + } + } + } + }, + "interests": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "description": "e.g. Philosophy" + }, + "keywords": { + "type": "array", + "items": { + "type": "string", + "description": "e.g. Friedrich Nietzsche" + } + } + } + } + }, + "references": { + "type": "array", + "description": "List references you have received", + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "description": "e.g. Timothy Cook" + }, + "reference": { + "type": "string", + "description": "e.g. Joe blogs was a great employee, who turned up to work at least once a week. He exceeded my expectations when it came to doing nothing." + } + } + } + }, + "projects": { + "type": "array", + "description": "Specify career projects and research grants", + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "description": "e.g. The World Wide Web" + }, + "description": { + "type": "string", + "description": "Short summary of project. e.g. Collated works of 2017." + }, + "highlights": { + "type": "array", + "description": "Specify multiple features", + "items": { + "type": "string", + "description": "e.g. Directs you close but not quite there" + } + }, + "keywords": { + "type": "array", + "description": "Specify special elements involved", + "items": { + "type": "string", + "description": "e.g. AngularJS" + } + }, + "startDate": { + "type": "string", + "description": "e.g. 2024-02-01" + }, + "endDate": { + "type": "string", + "description": "e.g. 2024-02-01" + }, + "url": { + "type": "string", + "format": "uri", + "description": "e.g. http://www.computer.org/csdl/mags/co/1996/10/rx069-abs.html" + }, + "roles": { + "type": "array", + "description": "Specify your role on this project or in company", + "items": { + "type": "string", + "description": "e.g. Team Lead, Speaker, Writer" + } + }, + "entity": { + "type": "string", + "description": "Specify the relevant company/entity affiliations e.g. 'greenpeace', 'corporationXYZ'" + }, + "type": { + "type": "string", + "description": " e.g. 'volunteering', 'presentation', 'talk', 'application', 'conference'" + }, + "projectType": { + "type": "string", + "description": "Type of project", + "enum": ["research", "software", "education", "grant", "other"] + }, + "fundingDetails": { + "type": "object", + "description": "Funding information if project is a grant", + "properties": { + "role": { + "type": "string", + "description": "e.g. PI, co-PI" + }, + "fundingAgency": { + "type": "string", + "description": "e.g. National Science Foundation" + }, + "amount": { + "type": "string", + "description": "e.g. $500,000" + }, + "status": { + "type": "string", + "enum": ["active", "completed", "pending"] + } + } + } + } + } + }, + "sideProjects": { + "type": "array", + "description": "Specify side projects", + "items": { + "type": "object", + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "description": "e.g. The World Wide Web" + }, + "url": { + "type": "string", + "format": "uri", + "description": "e.g. http://www.computer.org/csdl/mags/co/1996/10/rx069-abs.html" + }, + "description": { + "type": "string", + "description": "Short summary of project. e.g. Collated works of 2017." + }, + "keywords": { + "type": "array", + "description": "Specify special elements involved", + "items": { + "type": "string", + "description": "e.g. AngularJS" + } + }, + "startDate": { + "type": "string", + "description": "e.g. 2024-02-01" + }, + "endDate": { + "type": "string", + "description": "e.g. 2024-02-01" + } + } + } + }, + "meta": { + "type": "object", + "description": "The schema version and any other tooling configuration lives here", + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "description": "The name of this JSONCV data, will be used as part of the filename when downloading." + }, + "canonical": { + "type": "string", + "description": "URL (as per RFC 3986) to latest version of this document", + "format": "uri" + }, + "version": { + "type": "string", + "description": "A version field which follows semver - e.g. v1.0.0" + }, + "lastModified": { + "type": "string", + "description": "Using ISO 8601 with YYYY-MM-DDThh:mm:ss" + }, + "colorPrimary": { + "type": "string", + "description": "Primary color in hex format (e.g. #2A3FFB) to be used for styling the CV", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "colorSecondary": { + "type": "string", + "description": "Secondary color in hex format (e.g. #777777) to be used for dimmed text in the CV", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "citationStyles": { + "type": "object", + "description": "Styles for highlighting names in citations", + "properties": { + "owner": { + "type": "string", + "description": "Style for owner's name in citations", + "enum": ["bold", "asterisk", "plus"] + }, + "mentee": { + "type": "string", + "description": "Style for mentee names in citations", + "enum": ["bold", "asterisk", "plus"] + } + }, + "default": { + "owner": "bold", + "mentee": "plus" + } + }, + "pageSize": { + "type": "string", + "description": "Page size for the CV (A4 or letter)", + "enum": ["A4", "letter"], + "default": "A4" + }, + "theme": { + "type": "string", + "description": "Theme for the CV", + "enum": ["reorx", "cuiv"], + "default": "reorx" + } + } + }, + "teaching": { + "type": "array", + "description": "Courses taught and teaching experience", + "items": { + "type": "object", + "properties": { + "courseCode": { + "type": "string", + "description": "e.g. ECE 4153" + }, + "courseName": { + "type": "string", + "description": "e.g. Power System Analysis and Design" + }, + "description": { + "type": "string", + "description": "Course description and topics covered" + }, + "institution": { + "type": "string" + }, + "terms": { + "type": "array", + "items": { + "type": "string", + "description": "e.g. F'22, S'24" + } + } + } + } + }, + "mentoring": { + "type": "object", + "description": "Students mentored/advised by category", + "properties": { + "current_students": { + "type": "array", + "description": "Current students at my institution", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "level": { + "type": "string", + "enum": ["Ph.D.", "M.S.", "Undergraduate"], + "description": "Academic level" + }, + "startDate": { + "type": "string", + "description": "Start date in any format" + }, + "url": { + "type": "string", + "description": "URL to student's website or profile", + "format": "uri" + }, + "dissertation": { + "type": "string", + "description": "Dissertation topic or title (for PhD students)" + }, + "type": { + "type": "string", + "enum": ["Thesis", "Course-based"], + "description": "Type of Master's program (for MS students)" + }, + "thesis_title": { + "type": "string", + "description": "Thesis title (for thesis-based MS students)" + }, + "program": { + "type": "string", + "description": "Program name (for undergraduate students)" + }, + "period": { + "type": "string", + "description": "Academic period (for undergraduate students)" + } + }, + "required": ["name", "level"] + } + }, + "committee_service": { + "type": "array", + "description": "PhD committee service at other institutions", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "institution": { + "type": "string", + "description": "Institution where the student is/was enrolled" + }, + "dissertation": { + "type": "string", + "description": "Dissertation title" + }, + "status": { + "type": "string", + "enum": ["Current", "Completed"], + "description": "Current status of the student" + }, + "completionDate": { + "type": "string", + "description": "Date of completion in any format" + } + }, + "required": ["name", "institution", "dissertation", "status"] + } + }, + "past_students": { + "type": "array", + "description": "Past students who have completed their degrees", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "level": { + "type": "string", + "enum": ["Ph.D.", "M.S.", "Undergraduate"], + "description": "Academic level" + }, + "institution": { + "type": "string", + "description": "Institution where the student was enrolled" + }, + "startDate": { + "type": "string", + "description": "Start date in any format" + }, + "endDate": { + "type": "string", + "description": "End date in any format" + }, + "url": { + "type": "string", + "description": "URL to student's website or profile", + "format": "uri" + }, + "dissertation": { + "type": "string", + "description": "Dissertation title (for PhD students)" + }, + "type": { + "type": "string", + "enum": ["Thesis", "Course-based"], + "description": "Type of Master's program (for MS students)" + }, + "thesis_title": { + "type": "string", + "description": "Thesis title (for thesis-based MS students)" + }, + "program": { + "type": "string", + "description": "Program name (for undergraduate students)" + }, + "period": { + "type": "string", + "description": "Academic period (for undergraduate students)" + }, + "placement": { + "type": "object", + "properties": { + "position": { + "type": "string", + "description": "Job title or position" + }, + "organization": { + "type": "string", + "description": "Company or institution name" + }, + "location": { + "type": "string", + "description": "Location of the position" + } + } + } + }, + "required": ["name", "level", "institution"] + } + } + } + }, + "software": { + "type": "array", + "description": "Software projects and tools developed", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "metrics": { + "type": "object", + "properties": { + "stars": { + "type": "integer" + }, + "forks": { + "type": "integer" + } + } + } + } + } + }, + "researchAreas": { + "type": "array", + "description": "Research areas and contributions", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the research area" + }, + "contributions": { + "type": "array", + "description": "List of contributions in this area", + "items": { + "type": "string" + } + } + } + } + }, + "services": { + "type": "object", + "description": "Professional service activities", + "properties": { + "editorialBoards": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string", + "description": "e.g. Associate Editor, Guest Editor" + }, + "journal": { + "type": "string" + }, + "period": { + "type": "string", + "description": "e.g. 2012-Present" + }, + "details": { + "type": "string", + "description": "Additional details about the role" + } + } + } + }, + "societyServices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string" + }, + "organization": { + "type": "string" + }, + "period": { + "type": "string" + } + } + } + }, + "reviewServices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "e.g. Proposal Review, Journal Review" + }, + "organization": { + "type": "string" + }, + "year": { + "type": "string" + }, + "details": { + "type": "string" + } + } + } + }, + "departmental": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string" + }, + "committee": { + "type": "string" + }, + "period": { + "type": "string" + } + } + } + } + } + }, + "presentations": { + "type": "object", + "description": "Presentations and talks", + "properties": { + "invitedTalks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "venue": { + "type": "string" + }, + "type": { + "type": "string", + "description": "e.g. Webinar, Seminar" + }, + "date": { + "type": "string" + }, + "title": { + "type": "string" + }, + "url": { + "type": "string", + "description": "URL to presentation materials", + "format": "uri" + } + } + } + }, + "conferences": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "venue": { + "type": "string" + }, + "location": { + "type": "string" + }, + "date": { + "type": "string" + }, + "type": { + "type": "string" + }, + "description": { + "type": "string", + "description": "Description of the presentation content and significance" + }, + "url": { + "type": "string", + "description": "URL to presentation materials", + "format": "uri" + } + } + } + }, + "outreach": { + "type": "array", + "items": { + "type": "object", + "properties": { + "activity": { + "type": "string" + }, + "date": { + "type": "string" + }, + "description": { + "type": "string", + "description": "Detailed description of the outreach activity and its impact" + }, + "url": { + "type": "string", + "description": "URL to related materials", + "format": "uri" + } + } + } + } + } + } + }, + "title": "CV Schema", + "type": "object" +} \ No newline at end of file diff --git a/src/editor/index.html b/src/editor/index.html index 50ab87a..1f6ce1c 100644 --- a/src/editor/index.html +++ b/src/editor/index.html @@ -26,13 +26,29 @@ #aaaaaa + - + + +
+
BibTeX
+ + +
+ + +
+
+ + diff --git a/src/editor/main.js b/src/editor/main.js index a001ae9..de2f052 100644 --- a/src/editor/main.js +++ b/src/editor/main.js @@ -1,18 +1,21 @@ -import 'iconify-icon'; // import only - +// Core dependencies first +import { JSONEditor } from '@json-editor/json-editor/dist/jsoneditor'; import $ from 'cash-dom'; import dayjs from 'dayjs'; import objectPath from 'object-path'; -import { JSONEditor } from '@json-editor/json-editor/dist/jsoneditor'; - -import * as sampleModule from '../../sample.cv.json'; +// Local imports +import * as sampleModule from '../../sample/sample.cv.prof.json'; import * as jsoncvSchemaModule from '../../schema/jsoncv.schema.json'; +import { processBibTeX, updatePublications, getLastBibTeX } from '../lib/bibtex'; import { getCVData, getPrimaryColor, saveCVJSON, savePrimaryColor, + savePageSize, + getTheme, + saveTheme, } from '../lib/store'; import { createElement, @@ -22,139 +25,222 @@ import { traverseDownObject, } from '../lib/utils'; import { getCVTitle } from '../themes/data'; +import { getAvailableThemes, getThemeDefaultColor } from '../themes'; import { registerIconLib } from './je-iconlib'; import { registerTheme } from './je-theme'; -const propertiesInOrder = ['basics', 'education', 'work', 'projects', 'sideProjects', 'skills', 'languages', 'interests', 'references', 'awards', 'publications', 'volunteer', 'certificates', 'meta'] -const basicsPropertiesInOrder = ['name', 'label', 'email', 'phone', 'url', 'summary', 'image', 'location', 'profiles'] +// Load iconify last as it's optional for UI +import 'iconify-icon'; // toc elements const elToc = document.querySelector('.editor-toc') -const tocUl = createElement('ul', { - parent: elToc -}) -const basicsUl = createElement('ul', { - parent: tocUl -}) +const tocUl = createElement('ul', { parent: elToc }) +const basicsUl = createElement('ul', { parent: tocUl }) +// init data +let data = getCVData() +if (!data) data = sampleModule.default -// copy the object to remove the readonly restriction on module -const jsoncvSchema = {...jsoncvSchemaModule.default} +// Initialize editor with schema and data +async function initializeEditor() { + // Get schema (remote or local) + let schema + if (data.$schema) { + try { + const response = await fetch(data.$schema) + schema = await response.json() + } catch (error) { + console.error('Error fetching schema:', error) + schema = {...jsoncvSchemaModule.default} + } + } else { + schema = {...jsoncvSchemaModule.default} + } -// add propertyOrder to schema, and add links to toc -propertiesInOrder.forEach((name, index) => { - jsoncvSchema.properties[name].propertyOrder = index + // Validate schema structure + if (!schema || !schema.properties) { + schema = {...jsoncvSchemaModule.default} + } - const li = createElement('li', {parent: tocUl}) - createElement('a', { - text: name, - attrs: { - href: `#root.${name}` - }, - parent: li, + // Add property order and build TOC + const excludeFromTOC = ['$schema', 'sideProjects', 'languages', 'interests'] + + Object.keys(schema.properties).forEach((name, index) => { + // Add order + schema.properties[name].propertyOrder = index + + // Skip excluded sections in TOC + if (excludeFromTOC.includes(name)) { + return + } + + // Create TOC entry + const li = createElement('li', { parent: tocUl }) + createElement('a', { + text: name, + attrs: { href: `#root.${name}` }, + parent: li, + }) + + // Special handling for basics section + if (name === 'basics' && schema.properties[name].properties) { + // only add location and profiles to basics toc + ['location', 'profiles'].forEach(subName => { + if (schema.properties[name].properties[subName]) { + const subLi = createElement('li', { parent: basicsUl }) + createElement('a', { + text: subName, + attrs: { href: `#root.basics.${subName}` }, + parent: subLi, + }) + } + }) + li.appendChild(basicsUl) + } }) - if (name === 'basics') { - li.appendChild(basicsUl) - } -}) -basicsPropertiesInOrder.forEach((name, index) => { - jsoncvSchema.properties.basics.properties[name].propertyOrder = index - // only add location and profiles to basics toc - if (!['location', 'profiles'].includes(name)) return - const li = createElement('li', {parent: basicsUl}) - createElement('a', { - text: name, - attrs: { - href: `#root.basics.${name}` - }, - parent: li, + // Add headerTemplate for arrays + traverseDownObject(schema, (key, obj) => { + let noun = key + if (noun.endsWith('s')) noun = noun.slice(0, -1) + if (obj.type === 'array' && obj.items) { + obj.items.headerTemplate = `${noun} {{i1}}` + } }) -}) -// add headerTemplate for each type:array in schema -traverseDownObject(jsoncvSchema, (key, obj) => { - let noun = key - if (noun.endsWith('s')) noun = noun.slice(0, -1) - if (obj.type === 'array' && obj.items) { - obj.items.headerTemplate = `${noun} {{i1}}` - } -}) + // Add format to schema + const keyFormatMap = { + // Basic fields + 'basics.properties.summary': 'textarea', -// add format to schema -const keyFormatMap = { - 'basics.properties.summary': 'textarea', - 'work.items.properties.description': 'textarea', - 'work.items.properties.summary': 'textarea', - 'work.items.properties.highlights.items': 'textarea', - 'projects.items.properties.description': 'textarea', - 'projects.items.properties.highlights.items': 'textarea', - 'sideProjects.items.properties.description': 'textarea', - 'skills.items.properties.summary': 'textarea', - 'languages.items.properties.summary': 'textarea', - 'references.items.properties.reference': 'textarea', - 'awards.items.properties.summary': 'textarea', - 'publications.items.properties.summary': 'textarea', - 'volunteer.items.properties.summary': 'textarea', - 'volunteer.items.properties.highlights.items': 'textarea', -} -for (const [key, format] of Object.entries(keyFormatMap)) { - objectPath.get(jsoncvSchema.properties, key).format = format -} + // Work and projects + 'work.items.properties': { + 'description': 'textarea', + 'summary': 'textarea', + 'highlights.items': 'textarea' + }, + 'projects.items.properties': { + 'description': 'textarea', + 'highlights.items': 'textarea' + }, + 'sideProjects.items.properties.description': 'textarea', + + // Skills and qualifications + 'skills.items.properties.summary': 'textarea', + 'languages.items.properties.summary': 'textarea', + 'references.items.properties.reference': 'textarea', + + // Awards and recognition + 'awards.items.properties.summary': 'textarea', + 'publications.items.properties.summary': 'textarea', + 'volunteer.items.properties': { + 'summary': 'textarea', + 'highlights.items': 'textarea' + }, -// change schema title -jsoncvSchema.title = 'CV Schema' + // Academic fields + 'researchAreas.items.properties': { + 'description': 'textarea', + 'contributions.items': 'textarea' + }, + 'academicAppointments.items.properties.summary': 'textarea', + 'teaching.items.properties.description': 'textarea', + 'grants.items.properties.summary': 'textarea', + 'mentoring.items.properties.summary': 'textarea', + 'professionalServices.items.properties.details': 'textarea', + 'presentations.items.properties.description': 'textarea', + 'software.items.properties.description': 'textarea' + } -// change some descriptions -jsoncvSchema.properties.meta.properties.lastModified.description += '. This will be automatically updated when downloading.' + // Process nested format mappings + for (const [key, value] of Object.entries(keyFormatMap)) { + if (typeof value === 'string') { + // Direct format mapping + const property = objectPath.get(schema.properties, key) + if (property) { + property.format = value + } + } else { + // Nested format mappings + for (const [subKey, format] of Object.entries(value)) { + const fullKey = `${key}.${subKey}` + const property = objectPath.get(schema.properties, fullKey) + if (property) { + property.format = format + } + } + } + } + // Set schema title and update descriptions + schema.title = 'CV Schema' + if (schema.properties.meta?.properties?.lastModified) { + schema.properties.meta.properties.lastModified.description += '. This will be automatically updated when downloading.' + } -// init data -let data = getCVData() -if (!data) data = sampleModule.default + // Initialize editor + registerTheme(JSONEditor) + registerIconLib(JSONEditor) + const elEditorContainer = document.querySelector('.editor-container') + const editor = new JSONEditor(elEditorContainer, { + schema: schema, + theme: 'mytheme', + iconlib: 'myiconlib', + disable_array_delete_all_rows: true, + no_additional_properties: true, + startval: data, + }); + + return editor; +} -// initialize editor -registerTheme(JSONEditor) -registerIconLib(JSONEditor) -const elEditorContainer = document.querySelector('.editor-container') -const editor = new JSONEditor(elEditorContainer, { - schema: jsoncvSchema, - theme: 'mytheme', - iconlib: 'myiconlib', - disable_array_delete_all_rows: true, - no_additional_properties: true, - startval: data, +// Initialize the editor +let editor; +initializeEditor().then(e => { + editor = e; + // Add ready handler + editor.on('ready',() => { + // add anchor to each schema element + document.querySelectorAll('[data-schemapath]').forEach(el => { + const schemapath = el.getAttribute('data-schemapath') + el.id = schemapath + }) + + // Initialize color picker from meta or localStorage + const initialData = editor.getValue() + const primaryColor = initialData.meta?.colorPrimary || getPrimaryColor() + $colorValue.text(primaryColor) + $inputColorPicker.val(primaryColor) + + // Set initial theme from meta or storage + const initialTheme = initialData.meta?.theme || getTheme() + $themePicker.val(initialTheme) + saveTheme(initialTheme) // Ensure storage is synced + }); + + // Add change handler + editor.on('change', () => { + const data = editor.getValue() + const json = JSON.stringify(data, null, 2) + $outputJSON.text(json) + + // save to localstorage + saveCVJSON(json) + + // sync page size with localStorage + const pageSize = data.meta?.pageSize || 'A4' + savePageSize(pageSize) + }); }); -editor.on('ready',() => { - // add anchor to each schema element - document.querySelectorAll('[data-schemapath]').forEach(el => { - const schemapath = el.getAttribute('data-schemapath') - el.id = schemapath - }) -}) function getEditorData() { - const data = editor.getValue() - return { - data, - json: JSON.stringify(data, null, 2), - } + return editor.getValue() } const $outputJSON = $('.output-json') const $outputHTML = $('.output-html') const outputHTMLIframe = $outputHTML.get(0) -// listen to change -editor.on('change', () => { - console.log('on editor change') - const {json} = getEditorData() - $outputJSON.text(json) - - // save to localstorage - saveCVJSON(json) -}) - // actions const $btnTogglePreview = $('#fn-toggle-preview') const $btnNewData = $('#fn-new-data') @@ -256,11 +342,93 @@ $btnPrintPreview.on('click', () => { $inputColorPicker.on('change', (e) => { const color = e.target.value - console.log('color', color) - $colorValue.text(color) + console.log('color changed to:', color) + + // Update both meta and localStorage + const editorData = editor.getValue() + if (!editorData.meta) editorData.meta = {} + editorData.meta.colorPrimary = color + editor.setValue(editorData) savePrimaryColor(color) + $colorValue.text(color) }) -const primaryColor = getPrimaryColor() -$colorValue.text(primaryColor) -$inputColorPicker.val(primaryColor) +// Initialize theme picker +const $themePicker = $('#fn-theme-picker') +const themes = getAvailableThemes() +themes.forEach(theme => { + const option = createElement('option', { + text: theme, + attrs: { value: theme } + }) + $themePicker.append(option) +}) + +// Theme colors +const themeColors = { + reorx: '#aaaaaa', + cuiv: '#cc0000' +} + +// Handle theme changes +$themePicker.on('change', (e) => { + const selectedTheme = e.target.value + console.log('theme changed to:', selectedTheme) + + // Update both theme and color in one operation + const editorData = editor.getValue() + if (!editorData.meta) editorData.meta = {} + editorData.meta.theme = selectedTheme + editorData.meta.colorPrimary = getThemeDefaultColor(selectedTheme) + + // Update editor and storage + editor.setValue(editorData) // triggers preview refresh + saveTheme(selectedTheme) + savePrimaryColor(editorData.meta.colorPrimary) + + // Update color picker UI + $colorValue.text(editorData.meta.colorPrimary) + $inputColorPicker.val(editorData.meta.colorPrimary) +}) + +// BibTeX handling +const $btnUploadBibTeX = $('#fn-upload-bibtex') +const $btnDownloadBibTeX = $('#fn-download-bibtex') +const $inputUploadBibTeX = $('input[name=upload-bibtex]') + +$btnUploadBibTeX.on('click', () => { + $inputUploadBibTeX.trigger('click') +}) + +$inputUploadBibTeX.on('change', async () => { + const files = $inputUploadBibTeX.get(0).files + if (files.length === 0) return + + const reader = new FileReader() + reader.onload = async (e) => { + try { + const bibtexStr = e.target.result + const currentData = editor.getValue() + const publications = await processBibTeX(bibtexStr, currentData) + + // Get current mode from radio buttons + const shouldReplace = $('input[name="bibtex-mode"]:checked').val() === 'replace' + const newData = updatePublications(currentData, publications, shouldReplace) + editor.setValue(newData) + } catch (e) { + console.error('Error processing BibTeX:', e) + alert('Error processing BibTeX file. Please check the console for details.') + } + } + + reader.readAsText(files[0]) +}) + +$btnDownloadBibTeX.on('click', () => { + const bibtexStr = getLastBibTeX() + if (!bibtexStr) { + alert('No BibTeX data available. Please upload a BibTeX file first.') + return + } + downloadContent('references.bib', bibtexStr) +}) diff --git a/src/lib/bibtex.js b/src/lib/bibtex.js new file mode 100644 index 0000000..27f4a56 --- /dev/null +++ b/src/lib/bibtex.js @@ -0,0 +1,167 @@ +import { Cite } from '@citation-js/core' +import '@citation-js/plugin-bibtex' +import '@citation-js/plugin-csl' +import { plugins } from '@citation-js/core' + +// Fetch and register IEEE CSL template +fetch('https://raw.githubusercontent.com/citation-style-language/styles/master/ieee.csl') + .then(response => response.text()) + .then(template => { + plugins.config.get('@csl').templates.add('ieee', template) + console.log('[Debug] Successfully registered IEEE CSL template') + }) + .catch(error => { + console.error('[Error] Failed to fetch IEEE CSL template:', error) + }) + +/** + * Map BibTeX entry types to CV publication types + */ +const PUBLICATION_TYPE_MAP = { + // Journal types + 'article': 'journal', + 'article-journal': 'journal', + 'journal-article': 'journal', + + // Conference types + 'paper-conference': 'conference', + 'inproceedings': 'conference', + 'conference': 'conference', + 'conference-paper': 'conference', + 'proceedings-article': 'conference', + + // Keep other types mapped to 'other' by default +} + +// Store the last uploaded BibTeX +let lastBibTeX = null + +/** + * Process BibTeX string and return publications in JSONCV format + */ +export function processBibTeX(bibtexStr, cvData, shouldReplace = false) { + // Store raw BibTeX + lastBibTeX = bibtexStr + + try { + // Parse BibTeX + const cite = new Cite(bibtexStr) + console.log('[Debug] Total entries in BibTeX:', cite.data.length) + + // Format all entries together to get correct numbering + const formattedHTML = cite.format('bibliography', { + format: 'html', + template: 'ieee', + lang: 'en-US' + }) + + // Clean up the HTML structure while preserving content + const processedHTML = formattedHTML + .replace(/\r?\n|\r/g, '') // Remove all newlines + .replace(/>\s+<') // Remove whitespace between tags + .replace(/\s+/g, ' ') // Replace multiple spaces with single space + .replace(/, doi: .*?(?=<\/div>)/g, '') // Remove DOI from citation text + .trim() // Remove leading/trailing whitespace + + // Extract individual citations from the processed HTML + const citationDivs = processedHTML.match(/
.*?<\/div>/g) || [] + + // Process each entry with its formatted citation + const publications = cite.data.map((entry, index) => { + console.log(`[Debug] Entry ${index} type:`, entry.type) + + // Format individual citation + const singleCite = new Cite(entry) + const formattedHTML = singleCite.format('bibliography', { + format: 'html', + template: 'ieee', + lang: 'en-US' + }) + + // Clean up the HTML structure + const processedHTML = formattedHTML + .replace(/\r?\n|\r/g, '') // Remove all newlines + .replace(/>\s+<') // Remove whitespace between tags + .replace(/\s+/g, ' ') // Replace multiple spaces with single space + .replace(/, doi: .*?(?=<\/div>)/g, '') // Remove DOI from citation text + .trim() // Remove leading/trailing whitespace + + const pubType = PUBLICATION_TYPE_MAP[entry.type] || 'other' + const pub = { + name: entry.title, + publisher: entry['container-title'] || entry.journal || '', + releaseDate: entry.issued?.['date-parts']?.[0]?.[0]?.toString() || entry.year?.toString() || '', + url: entry.DOI ? `https://doi.org/${entry.DOI}` : entry.URL || '', + type: pubType, + formattedHTML: `
${processedHTML}
`, + citationKey: entry.id, + bibtexEntry: entry, + source: 'bibtex' + } + return pub + }) + + // Create a new CV data object with updated publications + const newCvData = { ...cvData } + + if (shouldReplace) { + // In replace mode, use only BibTeX publications + newCvData.publications = publications + } else { + // In merge mode, combine with existing publications + newCvData.publications = [...(cvData.publications || []), ...publications] + } + + // Sort all publications by release date (newest first) + newCvData.publications.sort((a, b) => { + const dateA = a.releaseDate ? new Date(a.releaseDate) : new Date(0); + const dateB = b.releaseDate ? new Date(b.releaseDate) : new Date(0); + return dateB - dateA; + }); + + console.log('[Debug] CV publications after update:', { + mode: shouldReplace ? 'replace' : 'merge', + total: newCvData.publications.length, + fromBibtex: publications.length, + }) + console.log(newCvData) + + return newCvData + } catch (error) { + console.error('[Error] Failed to process BibTeX:', error) + return cvData + } +} + +/** + * Get the last uploaded BibTeX data + */ +export function getLastBibTeX() { + return lastBibTeX +} + +/** + * Parse stringified publication if needed + */ +function _parsePublication(pub) { + if (typeof pub === 'string') { + try { + return JSON.parse(pub) + } catch (e) { + console.error('Failed to parse publication:', e) + return pub + } + } + return pub +} + +/** + * Update publications in CV data (for backward compatibility) + */ +export function updatePublications(cvData, publications, shouldReplace = false) { + // Just pass through to processBibTeX if we have BibTeX data + if (lastBibTeX) { + return processBibTeX(lastBibTeX, cvData, shouldReplace) + } + return cvData +} \ No newline at end of file diff --git a/src/lib/date.js b/src/lib/date.js index a0b4068..c67df5f 100644 --- a/src/lib/date.js +++ b/src/lib/date.js @@ -1,5 +1,8 @@ import dayjs from 'dayjs'; export function reformatDate(dateStr, toFormat, fromFormat = 'YYYY-MM-DD') { + if (!dateStr) return '' + const lower = dateStr.toLowerCase() + if (lower === 'present' || lower === 'now') return 'Present' return dayjs(dateStr).format(toFormat) } diff --git a/src/lib/store.js b/src/lib/store.js index 90a7c0f..76ecffd 100644 --- a/src/lib/store.js +++ b/src/lib/store.js @@ -1,35 +1,44 @@ -export const storeKeys = { - cvJSON: 'cvJSON', - cvSavedTime: 'cvSavedTime', - primaryColor: 'primary-color', -} - -const defaultPrimaryColor = '#2A3FFB' - -function updateSavedTime() { - localStorage.setItem(storeKeys.cvSavedTime, Date.now()) -} +const storeKeyCV = 'cv-data' +const storeKeyCVSavedTime = 'cv-data-saved-time' +const storeKeyPrimaryColor = 'primary-color' +const storeKeyPageSize = 'page-size' +const storeKeyTheme = 'theme' -export function saveCVJSON(str) { - localStorage.setItem(storeKeys.cvJSON, str) - updateSavedTime() +export function saveCVJSON(json) { + localStorage.setItem(storeKeyCV, json) + localStorage.setItem(storeKeyCVSavedTime, Date.now().toString()) } export function getCVData() { - const v = localStorage.getItem(storeKeys.cvJSON) - if (!v) return - return JSON.parse(v) + const json = localStorage.getItem(storeKeyCV) + if (!json) return null + return JSON.parse(json) } export function getCVSavedTime() { - return localStorage.getItem(storeKeys.cvSavedTime) + return localStorage.getItem(storeKeyCVSavedTime) } export function savePrimaryColor(color) { - localStorage.setItem(storeKeys.primaryColor, color) - updateSavedTime() + localStorage.setItem(storeKeyPrimaryColor, color) } export function getPrimaryColor() { - return localStorage.getItem(storeKeys.primaryColor) || defaultPrimaryColor + return localStorage.getItem(storeKeyPrimaryColor) || '#aaaaaa' +} + +export function savePageSize(size) { + localStorage.setItem(storeKeyPageSize, size) +} + +export function getPageSize() { + return localStorage.getItem(storeKeyPageSize) || 'A4' +} + +export function saveTheme(theme) { + localStorage.setItem(storeKeyTheme, theme) +} + +export function getTheme() { + return localStorage.getItem(storeKeyTheme) || 'reorx' } diff --git a/src/preview/main.js b/src/preview/main.js index cbcaeb2..b392f1a 100644 --- a/src/preview/main.js +++ b/src/preview/main.js @@ -2,6 +2,8 @@ import { getCVData, getCVSavedTime, getPrimaryColor, + getPageSize, + getTheme, } from '../lib/store'; import { upsertStyleTag } from '../lib/utils'; import cvBaseStyle from '../scss/cv-base.css?inline'; @@ -36,9 +38,14 @@ const restoreScrollPosition = () => { // Render CV const data = getCVData() if (data) { + // Set page size + const pageSize = data.meta?.pageSize || getPageSize() + document.documentElement.dataset.pageSize = pageSize.toLowerCase() upsertStyleTag('base-style', cvBaseStyle) - renderThemeOn(themeName, elCV, data, getPrimaryColor()) + // Use theme from meta or storage + const theme = data.meta?.theme || getTheme() + renderThemeOn(theme, elCV, data, getPrimaryColor()) // change document title document.title = getCVTitle(data) diff --git a/src/scss/cv-base.css b/src/scss/cv-base.css index 84fc2f5..0b828ce 100644 --- a/src/scss/cv-base.css +++ b/src/scss/cv-base.css @@ -1,23 +1,73 @@ /* Mandatory page setup for all themes */ +:root { + /* A4: 210mm x 297mm */ + /* Letter: 215.9mm x 279.4mm */ + --page-width: 210mm; /* default A4 */ + --page-height: 297mm; /* default A4 */ + --page-margin: 10mm; +} + +:root[data-page-size="letter"] { + --page-width: 215.9mm; + --page-height: 279.4mm; +} + @page { - /* A4 */ - /* size: 210mm 297mm; */ - size: A4; - margin: 10mm; /* approximately 0.4in */ + size: var(--page-width) var(--page-height); + margin: var(--page-margin); } /* simulate print page size */ .cv-container { - width: calc(210mm - 20mm); - padding: 10mm; + width: calc(var(--page-width) - 2 * var(--page-margin)); + min-height: calc(var(--page-height) - 2 * var(--page-margin)); + padding: var(--page-margin); + margin: 0 auto; /* center the container */ + background: white; + box-sizing: border-box; + position: relative; +} + +/* Add page size indicator */ +.cv-container::before { + content: "A4"; + position: fixed; + top: 5px; + right: 5px; + background: #f0f0f0; + padding: 2px 6px; + border-radius: 3px; + font-size: 12px; + opacity: 0.7; +} + +:root[data-page-size="letter"] .cv-container::before { + content: "Letter"; } + @media print { - /* remove the printing simulations and common decorations , let @page decide the size and margin */ - .cv-container { + .cv-container::before { + display: none; + } + + @page { + size: var(--page-width) var(--page-height); + margin: var(--page-margin); + } + + html, body { + width: var(--page-width); + height: var(--page-height); margin: 0; padding: 0; + } + + .cv-container { + width: 100%; + min-height: 100%; + margin: 0; + padding: var(--page-margin); border: 0; - width: auto; } } diff --git a/src/scss/editor/index.scss b/src/scss/editor/index.scss index f6b2c4e..30e6a88 100644 --- a/src/scss/editor/index.scss +++ b/src/scss/editor/index.scss @@ -112,14 +112,32 @@ button { } } } + + .theme-picker { + @include vars.button-base; + border: 1px solid var(--grey-2); + border-radius: 2px; + padding: 3px 8px; + font-size: 13px; + display: block; + width: 85px; + + select { + width: 100%; + margin-top: 4px; + padding: 2px; + } + } } .output-html { // set a fixed size so that responsive styles won't be applied in the iframe - width: 795px; // 210mm ≈ 793px + width: 875px; // 795px * 1.1 ≈ 875px height: 100%; - margin: 0; padding: 0; border: 0; + margin: 0; // remove auto margin since we're in a grid + padding: 0; border: 0; border-right: 1px solid var(--grey-2); + display: block; } .output-json { @@ -150,3 +168,65 @@ dialog { background-color: transparent; } } + +.bibtex-controls { + margin: 5px 0; + + .bibtex-mode { + margin-top: 4px; + display: flex; + gap: 12px; + justify-content: center; + font-size: 0.9em; + + label { + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + + input[type="radio"] { + margin: 0; + cursor: pointer; + } + } + } +} + +.publications-box { + margin: 12px 0; + padding: 6px 8px; + border: 1px solid var(--grey-2); + border-radius: 4px; + + .box-title { + font-weight: 500; + margin-bottom: 6px; + padding-bottom: 3px; + border-bottom: 1px solid var(--grey-2); + } + + button { + margin-bottom: 4px; + } + + .bibtex-mode { + display: flex; + flex-direction: column; + gap: 3px; + margin-top: 2px; + font-size: 0.9em; + + label { + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + + input[type="radio"] { + margin: 0; + cursor: pointer; + } + } + } +} diff --git a/src/themes/cuiv/index.ejs b/src/themes/cuiv/index.ejs new file mode 100644 index 0000000..f3bdaac --- /dev/null +++ b/src/themes/cuiv/index.ejs @@ -0,0 +1,804 @@ +<% +function linkInDiv(url) { + if (!url) return '' + return `
${fn.noSchemaURL(url)}
` +} + +// Format citation from structured data +function formatCitation(pub, index) { + // If we have pre-formatted HTML (from BibTeX), use it but replace the number + if (pub.formattedHTML) { + // Replace any number in [x] with current index + 1 + return pub.formattedHTML.replace(/\[\d+\]/, `[${index + 1}]`); + } + + // Otherwise format from structured data + const parts = []; + if (pub.authors && pub.authors.length > 0) { + parts.push(pub.authors.join(', ')); + } + if (pub.name) parts.push(`"${pub.name}"`); + if (pub.publisher) parts.push(pub.publisher); + if (pub.releaseDate) parts.push(pub.releaseDate); + + // Return formatted HTML with the same structure as Citation.js output + return `
[${index + 1}]
${parts.join(', ')}
`; +} + +function formatLocation(loc) { + const cityToCountry = ['city', 'region', 'postalCode', 'countryCode'].map(key => loc[key]).filter(v => v).join(', ') + if (!loc.address) return cityToCountry + return `${loc.address}, ${cityToCountry}` +} + +function hasItems(arr) { + return Array.isArray(arr) && arr.length > 0 +} + +function dateRange(item, level) { + let {startDate, endDate} = item + if (!startDate && !endDate) return '' + let format + // level: 1: year, 2: month, 3: day + switch (level) { + case 1: + format = 'YYYY' + break; + case 2: + format = 'MMM YYYY' + break; + } + if (format) { + startDate = startDate ? fn.reformatDate(startDate, format) : '' + endDate = endDate ? fn.reformatDate(endDate, format) : '' + } + let result = '' + if (startDate && endDate) { + result = `${startDate} – ${endDate}` + } else if (startDate) { + result = `${startDate} – Present` + } else if (endDate) { + result = `Until ${endDate}` + } + return `
${result}
` +} + +function date(item, level) { + let {date} = item + if (!date) return '' + let format + // level: 1: year, 2: month, 3: day + switch (level) { + case 1: + format = 'YYYY' + break; + case 2: + format = 'MMM YYYY' + break; + } + if (format) { + date = date ? fn.reformatDate(date, format) : '' + } + return `
${date}
` +} +%> + +
+ ☀️ + 🌙 +
+ + +
+

<%= cv.basics.name %>

+ <% if (cv.basics.label) { %> +
<%= cv.basics.label %>
+ <% } %> +
+ <% if (cv.basics.email) { %> +
+ <%- fn.getIconSVG('mdi:email') %> + <%= cv.basics.email %> +
+ <% } %> + <% if (cv.basics.url) { %> +
+ <%- fn.getIconSVG('mdi:link-variant') %> + <%= fn.noSchemaURL(cv.basics.url) %> +
+ <% } %> + <% if (cv.basics.profiles) { %> + <% for (const profile of cv.basics.profiles) { %> +
+ <% if (profile.network === 'LinkedIn') { %> + <%- fn.getIconSVG('mdi:linkedin') %> + <% } else if (profile.network === 'Google Scholar') { %> + <%- fn.getIconSVG('mdi:school') %> + <% } else if (profile.network === 'Scopus') { %> + <%- fn.getIconSVG('mdi:database') %> + <% } %> + <%= profile.network %> +
+ <% } %> + <% } %> +
+
+ <% if (cv.basics.phone) { %> +
+ <%- fn.getIconSVG('mdi:phone') %> + <%= cv.basics.phone %> +
+ <% } %> + <% if (cv.basics.location) { %> +
+ <%- fn.getIconSVG('mdi:location') %> + <%= formatLocation(cv.basics.location) %> +
+ <% } %> +
+ <% if (cv.basics.summary) { %> +
<%- fn.renderMarkdown(cv.basics.summary) %>
+ <% } %> +
+ + + <% if (hasItems(cv.education)) { %> +
+
+

Education

+
+
+ <% for (const item of cv.education) { %> +
+
+

<%= item.institution %>

+
+
+
+ <%= item.studyType %> <%= item.area %> +
+
+ <% + const startYear = item.startDate ? item.startDate.split('-')[0] : ''; + const endYear = item.endDate === 'Present' ? 'Present' : + item.endDate ? item.endDate.split('-')[0] : ''; + %> + <%= startYear %><% if (startYear && endYear) { %> - <% } %><%= endYear %> +
+
+ <% if (item.score) { %>
Overall GPA: <%= item.score %>
<% } %> + <% if (item.dissertation) { %> +
Dissertation: <%= item.dissertation %>
+ <% } %> + <% if (item.thesis) { %> +
Thesis: <%= item.thesis %>
+ <% } %> + <% if (item.courses && item.courses.length > 0) { %> +
+ Courses: <%= item.courses.join('; ') %> +
+ <% } %> +
+ <% } %> +
+ <% } %> + + + <% if (hasItems(cv.work)) { %> +
+
+

Work Experience

+
+
+ <% for (const item of cv.work) { %> +
+ <% if (item.name !== (cv.work[cv.work.indexOf(item) - 1] || {}).name) { %> +
+

<%= item.name %>

+
+ <% } %> +
+
<%= item.position %>
+
+ <% + const startYear = item.startDate ? item.startDate.split('-')[0] : ''; + const endYear = item.endDate === 'Present' ? 'Present' : + item.endDate ? item.endDate.split('-')[0] : 'Present'; + %> + <%= startYear %><% if (startYear && endYear) { %> - <% } %><%= endYear %> +
+
+
<%- fn.renderMarkdown(item.summary) %>
+ <% if (item.highlights && item.highlights.length > 0) { %> +
+
    + <% for (const hl of item.highlights) { %> +
  • <%- fn.renderMarkdown(hl, true) %>
  • + <% } %> +
+
+ <% } %> +
+ <% } %> +
+ <% } %> + + + <% if (hasItems(cv.researchAreas)) { %> +
+
+

Research Areas

+
+
+ <% for (const item of cv.researchAreas) { %> +
+

<%= item.name %>

+ <% if (item.description) { %> +
<%- fn.renderMarkdown(item.description) %>
+ <% } %> + <% if (item.keywords && Array.isArray(item.keywords) && item.keywords.length > 0) { %> +
+ <% for (const kw of item.keywords) { %> + <%= kw %> + <% } %> +
+ <% } %> +
+ <% } %> +
+ <% } %> + + + <% + // Group publications by type, defaulting to 'other' if type not specified + const journalPubs = cv.publications?.filter(p => p.type === 'journal') || [] + const conferencePubs = cv.publications?.filter(p => p.type === 'conference') || [] + const otherPubs = cv.publications?.filter(p => !p.type || p.type === 'other') || [] + + // Only show section if there are publications + if (hasItems(journalPubs) || hasItems(conferencePubs) || hasItems(otherPubs)) { + %> +
+
+

Publications

+
+
+ + <% if (hasItems(journalPubs)) { %> +
+

Journal Papers

+ <% for (let i = 0; i < journalPubs.length; i++) { %> +
+
<%- formatCitation(journalPubs[i], i) %>
+ <%- linkInDiv(journalPubs[i].url) %> +
+ <% } %> +
+ <% } %> + + <% if (hasItems(conferencePubs)) { %> +
+

Conference Papers

+ <% for (let i = 0; i < conferencePubs.length; i++) { %> +
+
<%- formatCitation(conferencePubs[i], i) %>
+ <%- linkInDiv(conferencePubs[i].url) %> +
+ <% } %> +
+ <% } %> + + <% if (hasItems(otherPubs)) { %> +
+

Other Publications

+ <% for (let i = 0; i < otherPubs.length; i++) { %> +
+
<%- formatCitation(otherPubs[i], i) %>
+ <%- linkInDiv(otherPubs[i].url) %> +
+ <% } %> +
+ <% } %> +
+ <% } %> + + + <% if (hasItems(cv.projects)) { %> +
+
+

Projects

+
+
+ <% for (const item of cv.projects) { %> +
+
+

<%= item.name %>

+ <%- linkInDiv(item.url) %> +
+
+ <% if (item.roles && item.roles.length > 0) { %> +
<%= item.roles.join(', ') %>
+ <% } %> + <%- dateRange(item, 2) %> +
+
<%- fn.renderMarkdown(item.description, true) %>
+ <% if (item.highlights && item.highlights.length > 0) { %> + <% if (item.highlights.length === 1) { %> +
<%- fn.renderMarkdown(item.highlights[0], true) %>
+ <% } else { %> +
+
    + <% for (const hl of item.highlights) { %> +
  • <%- fn.renderMarkdown(hl, true) %>
  • + <% } %> +
+
+ <% } %> + <% } %> + <% if (item.keywords && Array.isArray(item.keywords) && item.keywords.length > 0) { %> +
+ <% for (const kw of item.keywords) { %> + <%= kw %> + <% } %> +
+ <% } %> +
+ <% } %> +
+ <% } %> + + + <% if (hasItems(cv.teaching)) { %> +
+
+

Teaching Experience

+
+
+ <% for (const item of cv.teaching) { %> +
+
+
+
<%= item.courseCode %>
+
<%= item.courseName %>
+
+
<%= item.terms.join(', ') %>
+
+ <% if (item.description) { %> +
<%- fn.renderMarkdown(item.description) %>
+ <% } %> +
+ <% } %> +
+ <% } %> + + + <% if (cv.mentoring) { %> +
+
+

Mentoring

+
+
+ + <% if (hasItems(cv.mentoring.current_students)) { %> +

Current Students

+ + + <% const phd_students = cv.mentoring.current_students.filter(s => s.level === 'Ph.D.') %> + <% if (phd_students.length > 0) { %> +

Ph.D. Students

+ <% for (const student of phd_students) { %> +
+
+
+ <%= student.name %> + <% if (student.url) { %> + <%- fn.getIconSVG('mdi:link-variant') %> + <% } %> +
+ <% if (student.startDate) { %> +
Since <%= student.startDate %>
+ <% } %> +
+ <% if (student.dissertation) { %> +
+
Dissertation: <%= student.dissertation %>
+
+ <% } %> +
+ <% } %> + <% } %> + + + <% const ms_students = cv.mentoring.current_students.filter(s => s.level === 'M.S.') %> + <% if (ms_students.length > 0) { %> +

Master's Students

+ <% for (const student of ms_students) { %> +
+
+
+ <%= student.name %> + <% if (student.url) { %> + <%- fn.getIconSVG('mdi:link-variant') %> + <% } %> +
+ <% if (student.startDate) { %> +
Since <%= student.startDate %>
+ <% } %> +
+
+ <% if (student.type) { %> +
<%= student.type %> Program
+ <% } %> + <% if (student.type === 'Thesis' && student.thesis_title) { %> +
Thesis: <%= student.thesis_title %>
+ <% } %> +
+
+ <% } %> + <% } %> + + + <% const ug_students = cv.mentoring.current_students.filter(s => s.level === 'Undergraduate') %> + <% if (ug_students.length > 0) { %> +

Undergraduate Researchers

+ <% for (const student of ug_students) { %> +
+
+
+ <%= student.name %> + <% if (student.url) { %> + <%- fn.getIconSVG('mdi:link-variant') %> + <% } %> +
+ <% if (student.period) { %> +
<%= student.period %>
+ <% } %> +
+ <% if (student.program) { %> +
+
<%= student.program %>
+
+ <% } %> +
+ <% } %> + <% } %> + <% } %> + + <% if (hasItems(cv.mentoring.committee_service)) { %> +

Ph.D. Committee Service

+ <% const current = cv.mentoring.committee_service.filter(s => s.status === 'Current') %> + <% const completed = cv.mentoring.committee_service.filter(s => s.status === 'Completed') %> + + <% if (current.length > 0) { %> +

Current

+ <% for (const student of current) { %> +
+
+
<%= student.name %>
+
<%= student.institution %>
+
+
+
Dissertation: <%= student.dissertation %>
+
+
+ <% } %> + <% } %> + + <% if (completed.length > 0) { %> +

Completed

+ <% for (const student of completed) { %> +
+
+
<%= student.name %>
+
<%= student.institution %>
+
+
+
Dissertation: <%= student.dissertation %>
+ <% if (student.completionDate) { %> +
Completed: <%= student.completionDate %>
+ <% } %> +
+
+ <% } %> + <% } %> + <% } %> + + <% if (hasItems(cv.mentoring.past_students)) { %> +

Past Students

+ <% for (const student of cv.mentoring.past_students) { %> +
+
+
+ <%= student.name %> + <% if (student.url) { %> + <%- fn.getIconSVG('mdi:link-variant') %> + <% } %> +
+
<%= student.level %>
+
+
+
<%= student.institution %>
+ <% if (student.startDate || student.endDate) { %> +
+ <%= student.startDate || '' %><% if (student.startDate && student.endDate) { %> - <% } %><%= student.endDate || '' %> +
+ <% } %> + <% if (student.level === 'Ph.D.' && student.dissertation) { %> +
Dissertation: <%= student.dissertation %>
+ <% } %> + <% if (student.level === 'M.S.') { %> + <% if (student.type) { %> +
<%= student.type %> Program
+ <% } %> + <% if (student.type === 'Thesis' && student.thesis_title) { %> +
Thesis: <%= student.thesis_title %>
+ <% } %> + <% } %> + <% if (student.level === 'Undergraduate' && student.program) { %> +
<%= student.program %>
+ <% } %> + <% if (student.placement) { %> +
+ <% if (student.placement.position && student.placement.organization) { %> + Currently <%= student.placement.position %> at <%= student.placement.organization %> + <% if (student.placement.location) { %> + , <%= student.placement.location %> + <% } %> + <% } else { %> + <% if (student.placement.position) { %> + Currently <%= student.placement.position %> + <% } %> + <% if (student.placement.organization) { %> + at <%= student.placement.organization %> + <% } %> + <% if (student.placement.location) { %> + , <%= student.placement.location %> + <% } %> + <% } %> +
+ <% } %> +
+
+ <% } %> + <% } %> +
+ <% } %> + + + <% if (cv.presentations) { %> +
+
+

Presentations

+
+
+ + <% if (hasItems(cv.presentations.invitedTalks)) { %> +

Invited Talks

+ <% for (const item of cv.presentations.invitedTalks) { %> +
+
+

<%= item.title || 'TBD' %>

+ <%- date(item, 2) %> +
+
+ <%= item.venue %> (<%= item.type %>) +
+ <% if (item.description) { %> +
<%- fn.renderMarkdown(item.description) %>
+ <% } %> +
+ <% } %> + <% } %> + + <% if (hasItems(cv.presentations.conferences)) { %> +

Conference Presentations

+ <% for (const item of cv.presentations.conferences) { %> +
+
+

<%= item.title %>

+ <%- date(item, 2) %> +
+
+ <%= item.venue %> (<%= item.type %>) + <% if (item.location) { %> - <%= item.location %><% } %> +
+ <% if (item.description) { %> +
<%- fn.renderMarkdown(item.description) %>
+ <% } %> + <%- linkInDiv(item.url) %> +
+ <% } %> + <% } %> + + <% if (hasItems(cv.presentations.outreach)) { %> +

Outreach Activities

+ <% for (const item of cv.presentations.outreach) { %> +
+
+

<%= item.activity %>

+ <%- date(item, 2) %> +
+ <% if (item.description) { %> +
<%- fn.renderMarkdown(item.description) %>
+ <% } %> + <%- linkInDiv(item.url) %> +
+ <% } %> + <% } %> +
+ <% } %> + + + <% if (hasItems(cv.awards)) { %> +
+
+

Awards

+
+
+ <% for (const item of cv.awards) { %> +
+
+

<%= item.title %>

+ <%- date(item, 2) %> +
+
<%= item.awarder %>
+ <% if (item.summary) { %> +
<%- fn.renderMarkdown(item.summary) %>
+ <% } %> +
+ <% } %> +
+ <% } %> + + + <% if (cv.services) { %> +
+
+

Professional Services

+
+
+ + + <% if (hasItems(cv.services.editorialBoards)) { %> +

Editorial Service

+ <% for (const item of cv.services.editorialBoards) { %> +
+
+
<%= item.role %>
+
<%= item.period %>
+
+
<%= item.journal %>
+ <% if (item.details) { %> +
<%- fn.renderMarkdown(item.details) %>
+ <% } %> +
+ <% } %> + <% } %> + + + <% if (hasItems(cv.services.societyServices)) { %> +

Society Service

+ <% for (const item of cv.services.societyServices) { %> +
+
+
<%= item.role %>
+
<%= item.period %>
+
+
<%= item.organization %>
+
+ <% } %> + <% } %> + + + <% if (hasItems(cv.services.reviewServices)) { %> +

Review Service

+ <% for (const item of cv.services.reviewServices) { %> +
+
+
<%= item.type %>
+
<%= item.year %>
+
+
<%= item.organization %>
+ <% if (item.details) { %> +
<%- fn.renderMarkdown(item.details) %>
+ <% } %> +
+ <% } %> + <% } %> + + + <% if (hasItems(cv.services.departmental)) { %> +

Department Service

+ <% for (const item of cv.services.departmental) { %> +
+
+
<%= item.role %>
+
<%= item.period %>
+
+
<%= item.committee %>
+
+ <% } %> + <% } %> +
+ <% } %> + + + <% if (hasItems(cv.certificates)) { %> +
+
+

Certificates

+
+
+ <% for (const item of cv.certificates) { %> +
+
+

<%= item.name %>

+ <%- date(item, 2) %> +
+
<%= item.issuer %>
+ <%- linkInDiv(item.url) %> +
+ <% } %> +
+ <% } %> + + + <% if (hasItems(cv.skills)) { %> +
+
+

Skills

+
+
+
+ <% for (const item of cv.skills) { %> +
+

<%= item.name %>

+
<%= item.level %>
+ <% if (item.summary) { %> +
<%- fn.renderMarkdown(item.summary) %>
+ <% } %> + <% if (item.keywords && Array.isArray(item.keywords) && item.keywords.length > 0) { %> +
+ <% for (const kw of item.keywords) { %> + <%= kw %> + <% } %> +
+ <% } %> +
+ <% } %> +
+
+ <% } %> + + + <% if (hasItems(cv.languages)) { %> +
+
+

Languages

+
+
+
+ <% for (const item of cv.languages) { %> +
+

<%= item.language %>

+
<%= item.fluency %>
+ <% if (item.summary) { %> +
<%- fn.renderMarkdown(item.summary) %>
+ <% } %> +
+ <% } %> +
+
+ <% } %> + + + <% if (cv.meta) { %> + + <% } %> \ No newline at end of file diff --git a/src/themes/cuiv/index.js b/src/themes/cuiv/index.js new file mode 100644 index 0000000..82f8416 --- /dev/null +++ b/src/themes/cuiv/index.js @@ -0,0 +1,81 @@ +import ejs from 'ejs'; +import { upsertStyleTag } from '../../lib/utils'; +import { getRenderData, varNamePrimaryColor } from '../data'; +import template from './index.ejs'; +import style from './index.scss?inline'; + +export const defaultColor = '#cc0000' // matches :root { --color-primary } in index.scss + +// Get system color scheme +function getSystemColorMode() { + const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + console.log('System color mode:', isDark ? 'dark' : 'light'); + return isDark ? 'dark' : 'light'; +} + +// Make toggleTheme available globally +window.toggleTheme = function() { + const cvContainer = document.querySelector('.cv-container'); + const themeToggle = document.querySelector('.theme-toggle'); + if (!cvContainer || !themeToggle) { + console.log('Toggle failed: Elements not found', { cvContainer, themeToggle }); + return; + } + + const currentMode = cvContainer.getAttribute('data-color-mode') || getSystemColorMode(); + console.log('Current color mode before toggle:', currentMode); + console.log('Current attributes:', { + container: cvContainer.getAttribute('data-color-mode'), + toggle: themeToggle.getAttribute('data-mode') + }); + + const newMode = currentMode === 'light' ? 'dark' : 'light'; + console.log('New color mode:', newMode); + + cvContainer.setAttribute('data-color-mode', newMode); + themeToggle.setAttribute('data-mode', newMode); + localStorage.setItem('colorMode', newMode); + + console.log('After toggle attributes:', { + container: cvContainer.getAttribute('data-color-mode'), + toggle: themeToggle.getAttribute('data-mode'), + stored: localStorage.getItem('colorMode') + }); +} + +export function renderTheme(el, data, primaryColor) { + console.log('Rendering theme...'); + el.innerHTML = ejs.render(template, getRenderData(data)); + upsertStyleTag('cv-style', style); + document.documentElement.style.setProperty(varNamePrimaryColor, primaryColor); + + // Initialize color mode based on stored preference or system preference + const storedMode = localStorage.getItem('colorMode'); + console.log('Stored color mode:', storedMode); + const initialMode = storedMode || getSystemColorMode(); + console.log('Initial color mode:', initialMode); + + const cvContainer = document.querySelector('.cv-container'); + const themeToggle = document.querySelector('.theme-toggle'); + if (cvContainer && themeToggle) { + console.log('Setting initial color mode...'); + cvContainer.setAttribute('data-color-mode', initialMode); + themeToggle.setAttribute('data-mode', initialMode); + console.log('Initial attributes set:', { + container: cvContainer.getAttribute('data-color-mode'), + toggle: themeToggle.getAttribute('data-mode') + }); + } else { + console.log('Elements not found during initialization:', { cvContainer, themeToggle }); + } + + // Listen for system theme changes + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { + if (!localStorage.getItem('colorMode')) { + const newMode = e.matches ? 'dark' : 'light'; + console.log('System color mode changed:', newMode); + cvContainer.setAttribute('data-color-mode', newMode); + themeToggle.setAttribute('data-mode', newMode); + } + }); +} \ No newline at end of file diff --git a/src/themes/cuiv/index.scss b/src/themes/cuiv/index.scss new file mode 100644 index 0000000..5c71557 --- /dev/null +++ b/src/themes/cuiv/index.scss @@ -0,0 +1,721 @@ +/* Naming convention: https://ricostacruz.com/rscss/ */ + +// Colors +$color-black: #000; +$color-text-dim: #777; +$color-text-dimmer: #999; +$color-border-dim: #aaa; +$color-border-dimmer: #ccc; + +// Font sizes +$fz-1: 32px; +$fz-2: 20px; +$fz-3: 16px; +$fz-4: 14px; +$fz-5: 13px; +$fz-6: 12px; +$lh-p: 1.3; + +// Layout system +:root { + // Theme colors + --bg-color: #fff; + --text-color: #{$color-black}; + --text-dim: #{$color-text-dim}; + --text-dimmer: #{$color-text-dimmer}; + --border-dim: #{$color-border-dim}; + --border-dimmer: #{$color-border-dimmer}; + --keyword-bg: #f1f1f1; + + // Default color that can be overridden by meta.colorPrimary + --color-primary: #aaa; + + // Time display properties + --time-color: var(--text-color); + --time-font-style: italic; + --time-min-width: 7em; + --time-text-align: right; + --time-content-ratio: 75%; + + // Spacing system + --content-indent: 10em; // Left indentation for content + --content-padding: 1em; // Right padding for content + --section-gap-large: 1.2em; // Large gaps between sections + --section-gap-medium: 1em; // Medium gaps for major subsections + --section-gap: 0.8em; // Normal gaps between sections + --section-gap-small: 0.4em; // Small gaps between items + --section-gap-tiny: 0.2em; // Tiny gaps for tight spacing + + // Print settings + --print-margin-top: 0.5in; // Standard Letter margins + --print-margin-bottom: 0.5in; + --print-margin-left: 0.5in; + --print-margin-right: 0.5in; +} + +// Dark mode +:root[data-theme="dark"] { + --bg-color: #1a1a1a; +} + +body { + margin: 0; + padding: 1rem; + min-height: 100vh; +} + +.theme-toggle { + position: absolute; + top: 1rem; + right: 1rem; + background: #fff; + border: 1px solid var(--border-dimmer); + border-radius: 20px; + padding: 0.5rem; + cursor: pointer; + z-index: 1000; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + + @media print { + display: none; + } + + .theme-icon { + display: none; + font-size: 1.2rem; + line-height: 1; + + &.light { + display: inline; // Show sun icon by default + } + } + + // Override default when dark mode is explicitly set + &[data-mode="dark"] { + .theme-icon.light { display: none; } + .theme-icon.dark { display: inline; } + } +} + +.cv-container { + background: #fff; + color: var(--text-color); + font-size: 14px; + font-family: system-ui, sans-serif; + max-width: 8.5in; // Letter width + margin: 0 auto; // Center on screen + padding: 2rem; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + transition: background-color 0.3s, color 0.3s; + + &[data-color-mode="dark"] { + background: #1a1a1a; + color: #fff; + --text-color: #fff; + --text-dim: #aaa; + --text-dimmer: #888; + --border-dim: #666; + --border-dimmer: #444; + --keyword-bg: #2a2a2a; + } + + @media print { + width: 100%; + max-width: none; + margin: 0; + padding: 0; + box-shadow: none; + background: #fff !important; + color: #{$color-black} !important; + --text-color: #{$color-black} !important; + --text-dim: #{$color-text-dim} !important; + --text-dimmer: #{$color-text-dimmer} !important; + --border-dim: #{$color-border-dim} !important; + --border-dimmer: #{$color-border-dimmer} !important; + + // Only protect header and footer from breaking + .basics-section { + break-inside: avoid; + page-break-inside: avoid; + } + } +} + +.date-range { + font-style: italic; +} + +a { + color: var(--text-dim); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +section { + .section-title { + margin-top: var(--section-gap-large); + margin-bottom: var(--section-gap); + display: flex; + + h2 { + font-size: $fz-2; + font-weight: 600; + color: var(--color-primary); + margin: 0; + } + .line { + flex-grow: 1; + margin: 12px 0 0 1em; + height: 2px; + background-color: var(--color-primary); + } + } + + .section-item { + margin-top: var(--section-gap-small); + margin-left: var(--content-indent); + padding-right: var(--content-padding); + + h3 { + font-size: $fz-3; + line-height: 1.2; + margin: 0; + } + + h4 { + font-size: $fz-4; + line-height: 1.2; + margin: 0; + } + + ul { + margin: .3em 0; + padding-left: 1.5em; + li { + margin-bottom: .15em; + } + } + } + + svg { + vertical-align: bottom; + position: relative; + bottom: 1px; + } + .row { + margin: .15em 0; + } + .p, p { + line-height: $lh-p; + margin: .3em 0; + } + .subtitle { + font-size: $fz-4; + // font-weight: 300; + + @media screen and (max-width: 500px) { + display: flex; + align-items: center; + } + } + + .space-between { + display: flex; + justify-content: space-between; + + a { + color: var(--text-dim); + } + } + + .keywords { + display: flex; + flex-wrap: wrap; + gap: 6px 8px; + margin-top: .8em; + + .item { + font-size: $fz-5; + color: var(--text-color); + padding: 2px 8px; + background: var(--keyword-bg); + border-radius: 4px; + } + } + + .two-columns { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0 1.5em; + + @media screen and (max-width: 500px) { + grid-template-columns: 1fr; + } + } + + /* helpers */ + ._fz-4 { + font-size: $fz-4; + } + ._ul { + text-decoration: underline; + } + ._text-dim { + color: var(--text-dim); + } +} + +@media print { + section { + .section-title { + margin-top: var(--section-gap); + margin-bottom: var(--section-gap-small); + } + .section-item { + margin-top: var(--section-gap-small); + margin-left: var(--content-indent); + padding-right: var(--content-padding); + } + } +} + +.basics-section { + h1 { + font-size: $fz-1; + font-weight: 600; + margin: .1em 0; + } + .subtitle { + font-size: $fz-2; + } + .contact { + color: var(--text-dim); + display: flex; + flex-wrap: wrap; + gap: .4em 1em; + + @media screen and (max-width: 500px) { + flex-direction: column; + } + + .item { + a { + color: var(--color-primary); + &.colored-link { + color: var(--color-primary); + text-decoration: none; + &:hover { + text-decoration: underline; + } + } + } + } + } + .colored-links { + color: var(--color-primary); + display: flex; + flex-wrap: nowrap; + gap: 1em; + + .item { + white-space: nowrap; + } + + a { + color: inherit; + text-decoration: none; + &:hover { + text-decoration: underline; + } + } + } +} + +// Shared styles for sections with smaller text +%academic-section { + font-size: $fz-5; + + .section-item { + line-height: 1.4; + + h3 { + font-size: $fz-4; + } + + h4 { + font-size: $fz-5; + } + + .subtitle { + font-size: $fz-5; + } + } +} + +// Shared time display style and layout +%time-display { + color: var(--text-color); + font-style: var(--time-font-style); + min-width: var(--time-min-width); + text-align: var(--time-text-align); +} + +%time-container { + display: flex; + justify-content: space-between; + + // Content side + > *:first-child { + max-width: var(--time-content-ratio); + } + + // Time side - include all possible time-related classes + > .year, + > .date-range, + > .period, + > .terms { + @extend %time-display; + } +} + +.education-section { + @extend %academic-section; + .space-between { + @extend %time-container; + } +} + +.work-section { + @extend %academic-section; + .space-between { + @extend %time-container; + } +} + +.projects-section { + @extend %academic-section; + .space-between { + @extend %time-container; + } +} + +.sideprojects-section { + .description p:last-of-type { + margin-bottom: 0; + } +} + +.research-metrics-section { + .metrics { + display: flex; + gap: 2em; + font-size: $fz-4; + + .item { + color: var(--text-dim); + } + } +} + +.academic-appointments-section { + .appointment { + .department { + color: var(--text-dim); + } + .institution { + font-style: italic; + } + } +} + +.teaching-section { + .course { + .row { + @extend %time-container; + } + .course-info { + display: flex; + gap: 1em; + } + .code { + font-weight: 600; + } + .terms { + @extend %time-display; + } + } +} + +.grants-section { + .grant { + .amount { + font-weight: 600; + color: var(--color-primary); + } + .agency { + font-style: italic; + color: var(--text-dim); + } + } +} + +.mentoring-section { + @extend %academic-section; + + h3 { + margin-top: var(--section-gap-medium); + margin-bottom: var(--section-gap-small); + color: var(--color-primary); + font-size: $fz-4; + } + + h4 { + margin-top: 0.3em; + margin-bottom: 0.2em; + color: var(--text-dim); + font-size: $fz-5; + } + + .student { + margin-top: 0.2em; + + .row { + @extend %time-container; + } + + .name { + font-weight: 600; + } + + .details { + margin-top: 0.1em; + } + + .program, .period { + color: var(--text-dim); + } + + .dissertation, .thesis { + margin-top: 0.1em; + } + } +} + +.editorial-positions-section { + .position { + .role { + font-weight: 600; + } + .journal { + font-style: italic; + color: var(--text-dim); + } + } +} + +.professional-services-section { + @extend %academic-section; + + h3 { + margin-top: var(--section-gap-medium); + margin-bottom: var(--section-gap-small); + color: var(--color-primary); + font-size: $fz-4; + } + + // Editorial Boards + .service { + margin-top: var(--section-gap-tiny); + + .space-between { + @extend %time-container; + } + + .role { + font-weight: 600; + } + + .journal, .organization, .committee { + font-style: italic; + color: var(--text-dim); + } + + .details { + margin: var(--section-gap-tiny) 0; + font-size: $fz-5; + } + } +} + +.presentations-section { + @extend %academic-section; + + h3 { + margin-top: var(--section-gap-medium); + margin-bottom: var(--section-gap-small); + color: var(--color-primary); + font-size: $fz-4; + } + + .talk, .presentation, .outreach { + margin-top: var(--section-gap-tiny); + + .space-between { + @extend %time-container; + } + + .venue { + margin-top: var(--section-gap-tiny); + font-size: $fz-5; + } + + .description { + margin: var(--section-gap-tiny) 0; + font-size: $fz-5; + } + } +} + +.awards-section { + @extend %academic-section; + .space-between { + @extend %time-container; + } +} + +.certificates-section { + @extend %academic-section; + .space-between { + @extend %time-container; + } +} + +.publications-section { + @extend %academic-section; + + .subsection { + margin-bottom: var(--section-gap-medium); + + h3 { + color: var(--color-primary); + margin-bottom: var(--section-gap-small); + font-size: $fz-3; + } + } + + .publication { + margin-bottom: var(--section-gap-small); + + .citation { + line-height: $lh-p; + + strong { // For CV owner highlighting + color: var(--color-primary); + } + + em { // For mentee highlighting + font-style: italic; + color: var(--text-dim); + } + + // Make citation number and content appear inline + .csl-bib-body { + display: flex; + align-items: flex-start; + } + + .csl-entry { + display: flex; + align-items: flex-start; + flex: 1; + + .csl-left-margin { + min-width: 2.5em; + padding-right: 0.5em; + } + + .csl-right-inline { + flex: 1; + } + } + } + + .url { + margin-top: var(--section-gap-tiny); + font-size: $fz-5; + margin-left: 3em; // Match citation content alignment (2.5em + 0.5em padding) + } + + .summary { + margin-top: var(--section-gap-tiny); + color: var(--text-dim); + font-size: $fz-5; + } + } + + .url a { + color: var(--text-dim); + &:hover { + color: var(--text-dim); + } + } +} + +.research-areas-section { + @extend %academic-section; + + .section-item { + h3 { + font-size: $fz-4; + margin-bottom: 0.2em; + } + + .description { + font-size: $fz-5; + line-height: 1.3; + } + + .keywords { + margin-top: 0.4em; + } + } +} + +// Update all other hardcoded color references +.research-metrics-section .metrics .item, +.academic-appointments-section .department, +.teaching-section .terms, +.grants-section .agency, +.mentoring-section h4, +.mentoring-section .program, +.mentoring-section .period, +.editorial-positions-section .journal, +.professional-services-section .journal, +.professional-services-section .organization, +.professional-services-section .committee, +.publications-section .citation em, +.publications-section .summary, +.publications-section .url a { + color: var(--text-dim); +} + +footer { + margin-top: var(--section-gap-large); + padding-top: var(--section-gap); + border-top: 1px solid var(--border-dimmer); + color: var(--text-dimmer); + font-size: $fz-5; + + display: flex; + gap: 1em; +} + +// Dark mode override +.cv-container[data-color-mode="dark"] { + .keywords .item { + background: #2a2a2a; + } +} diff --git a/src/themes/index.js b/src/themes/index.js index 3bb80c8..d927617 100644 --- a/src/themes/index.js +++ b/src/themes/index.js @@ -6,45 +6,32 @@ import { varNamePrimaryColor, } from './data'; -const themes = {} +import { renderTheme as renderReorxTheme, defaultColor as reorxColor } from './reorx/index.js' +import { renderTheme as renderCuivTheme, defaultColor as cuivColor } from './cuiv/index.js' -const themeNames = ['reorx'] - -// https://vitejs.dev/guide/features.html#disabling-css-injection-into-the-page -// note that `?raw` (https://vitejs.dev/guide/assets.html#importing-asset-as-string) -// cannot be used because we need vite to transform scss into css -const styleMoudules = import.meta.glob("./*/index.scss", { "query": "?inline" }) - -for (const name of themeNames) { - const templateModule = await import(`./${name}/index.ejs`) - - // https://vitejs.dev/guide/features.html#glob-import - const styleModule = await styleMoudules[`./${name}/index.scss`]() - - themes[name] = { - template: templateModule.default, - style: styleModule.default, - } +const themes = { + reorx: renderReorxTheme, + cuiv: renderCuivTheme, } -// set default -themes.default = themes.reorx - -export function getTheme(name) { - return themes[name] +const themeColors = { + reorx: reorxColor, + cuiv: cuivColor, } -export function renderTheme(template, cvData, options) { - return ejs.render(template, getRenderData(cvData), options) +export function getAvailableThemes() { + return Object.keys(themes) } -const cvStyleId = 'cv-style' - -export function renderThemeOn(name, el, data, primaryColor) { - const theme = getTheme(name) - el.innerHTML = renderTheme(theme.template, data) - - upsertStyleTag(cvStyleId, theme.style) +export function getThemeDefaultColor(themeName) { + return themeColors[themeName] +} - document.documentElement.style.setProperty(varNamePrimaryColor, primaryColor) +export function renderThemeOn(themeName, el, data, primaryColor) { + const renderFn = themes[themeName] + if (!renderFn) { + console.error(`Theme ${themeName} not found`) + return + } + renderFn(el, data, primaryColor) } diff --git a/src/themes/reorx/index.ejs b/src/themes/reorx/index.ejs index 2a30151..7490f8e 100644 --- a/src/themes/reorx/index.ejs +++ b/src/themes/reorx/index.ejs @@ -372,25 +372,32 @@ function date(item, level) { -<% if (hasItems(cv.publications)) { %> -
-
-

Publications

-
-
- <% for (const item of cv.publications) { %> -
-
-

<%= item.name %>

- <%- date(item, 2) %> +<% if (cv.publications?.length) { %> +
+

Publications

+ <% cv.publications.forEach(function(pub) { %> + <% console.log('[Debug] Template rendering publication:', pub) %> +
+
<%= pub.name %>
+ <% if (pub.authors?.length) { %> + <% console.log('[Debug] Template rendering authors:', pub.authors) %> +
+ <%= pub.authors.join('; ') %> +
+ <% } %> +
+ <% if (pub.venue || pub.releaseDate) { %> + <%= pub.venue %><%= pub.releaseDate ? (pub.venue ? ', ' : '') + fn.reformatDate(pub.releaseDate, 'YYYY') : '' %> + <% } %>
-
<%= item.publisher %>
- <%- linkInDiv(item.url) %> - <% if (item.summary) { %> -
<%- fn.renderMarkdown(item.summary) %>
+ <% if (pub.summary) { %> +
<%- fn.renderMarkdown(pub.summary) %>
+ <% } %> + <% if (pub.url) { %> + <% } %>
- <% } %> + <% }) %>
<% } %> diff --git a/src/themes/reorx/index.js b/src/themes/reorx/index.js new file mode 100644 index 0000000..a6c8cb6 --- /dev/null +++ b/src/themes/reorx/index.js @@ -0,0 +1,13 @@ +import ejs from 'ejs'; +import { upsertStyleTag } from '../../lib/utils'; +import { getRenderData, varNamePrimaryColor } from '../data'; +import template from './index.ejs'; +import style from './index.scss?inline'; + +export const defaultColor = '#aaaaaa' // matches :root { --color-primary } in index.scss + +export function renderTheme(el, data, primaryColor) { + el.innerHTML = ejs.render(template, getRenderData(data)) + upsertStyleTag('cv-style', style) + document.documentElement.style.setProperty(varNamePrimaryColor, primaryColor) +} \ No newline at end of file diff --git a/src/themes/reorx/index.scss b/src/themes/reorx/index.scss index 88212d1..7705f81 100644 --- a/src/themes/reorx/index.scss +++ b/src/themes/reorx/index.scss @@ -224,3 +224,45 @@ section { margin-bottom: 0; } } + +.publications { + .item { + margin-bottom: 1.5em; + + .title { + font-weight: bold; + margin-bottom: 0.3em; + } + + .authors { + color: var(--color-primary); + margin-bottom: 0.3em; + } + + .publisher { + font-style: italic; + margin-bottom: 0.2em; + } + + .date { + color: #666; + font-size: 0.9em; + margin-bottom: 0.2em; + } + + .summary { + margin: 0.5em 0; + } + + .url { + font-size: 0.9em; + a { + color: var(--color-primary); + text-decoration: none; + &:hover { + text-decoration: underline; + } + } + } + } +} diff --git a/vite.config.js b/vite.config.js index d83935c..17a34e9 100644 --- a/vite.config.js +++ b/vite.config.js @@ -18,7 +18,6 @@ renderData.meta = { description: data.basics.summary.replace('\n', ' '), } - export default defineConfig({ build: { outDir: outDir, diff --git a/vite.config.site.js b/vite.config.site.js index 2d5f344..28f6a5e 100644 --- a/vite.config.site.js +++ b/vite.config.site.js @@ -28,6 +28,14 @@ const renderData = { export default defineConfig({ root: 'src', + optimizeDeps: { + include: [ + '@json-editor/json-editor/dist/jsoneditor', + 'cash-dom', + 'object-path', + 'iconify-icon' + ] + }, build: { // allows 'import.meta.glob' to work target: 'esnext',